MakerFLOSS/notes/dev/specs/2026-06-24-rack-shelves-design.md

155 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Shelf-Mounted Devices Design
**Date:** 2026-06-24
**Status:** Approved
**Parent spec:** `notes/dev/specs/2026-06-24-rack-documentation-design.md` (extension)
## Goal
Let the rack model represent cabinet/tower-style PCs that sit on a rack shelf
rather than bolting into the rails. Several PCs share one shelf, side by side and
front/back, and the assembly spans a tall U-range (e.g. a shelf at U46 with three
towers standing up to U37 = a reserved block U37U46). The current model cannot
express this: two PCs in the same U-range on the same face trip `check_overlaps`.
## Context
- Placement today (`scripts/gen_rack.py`): a rack item declares `rack_u` (lowest
U), `u_height`, and `rack_face` ∈ {front, rear, both, left, right}. `left`/`right`
are 0U side-rail items that omit `rack_u`/`u_height` and attach to a rail.
`check_overlaps` rejects two items sharing a U on the same face; it already
skips 0U rail items.
- This gives a precedent: **some items don't claim a U-range directly** (0U rails
attach to a rail). Shelf-mounted PCs are a third placement style — they attach
to a shelf.
- The physical rack is labeled U1 at the top → U48 at the bottom, so a shelf at
U46 (near the bottom) with towers standing upward to U37 reserves U37U46.
- `shelf` is already a valid `kind` (abbrev `shf`) with a color in `KIND_COLORS`.
## Decisions
### 1. Container model
The **shelf** is the rack-placed item; tower PCs are **contained by** it.
- A shelf (`kind: shelf`) is placed normally: `rack_u`, `u_height`,
`rack_face: both` (full depth). Its `rack_u`/`u_height` **reserve the whole
assembly's U-range** (the shelf plus the towers standing on it), e.g.
`rack_u: 37, u_height: 10`.
- A **mounted device** declares, instead of `rack_u`/`u_height`/`rack_face`:
- `mounted_on: <shelf hostname>` — the shelf it sits on.
- `shelf_face: front | rear` — which side of the shelf.
- `shelf_slot: <integer ≥ 1>` — left-to-right position within that face.
Mounted items still declare `rack: <rack>` (so they load as rack items) and may
carry `cluster:`, `power:`, `links:` unchanged — those key off hostname, not
placement.
No `scripts/overview_config.yml` change: `shelf` and `server` kinds already
exist; `mounted_on`/`shelf_face`/`shelf_slot` are extra fields `gen_overview.py`
ignores and `gen_rack.py` validates.
### 2. Validation (in `gen_rack.py`)
`validate_item` gains a placement branch, checked before the existing rail logic:
- If `mounted_on` is present, the item is **mounted**:
- require `shelf_face ∈ {front, rear}`;
- require `shelf_slot` to be an integer ≥ 1;
- forbid `rack_u`, `u_height`, and `rack_face` (mutually exclusive with
mounting, mirroring the 0U rule).
- Otherwise the existing rules apply unchanged (rail item: `rack_face` ∈ FACES,
0U rules for left/right, U-range for the rest).
`check_overlaps` **skips mounted items** (they claim no U-range; their shelf
reserves the block) — added alongside the existing 0U skip. The shelf itself is
a normal placed item, so it still cannot overlap rail gear.
New `check_shelves(items: list[dict]) -> None` (called per rack from `generate`,
alongside `check_overlaps`/`validate_power`/`validate_links`):
- every `mounted_on` resolves to an item **in the same rack** whose `kind` is
`shelf` and which is itself placed (has integer `rack_u`/`u_height`);
- `(mounted_on, shelf_face, shelf_slot)` is unique — no two devices in the same
spot on the same shelf.
### 3. Rendering (`gen_rack.py`)
**Elevation SVG (`render_svg`):**
- The shelf draws as a thin **shelf strip** (shelf-colored rect) at the bottom 1U
of its reserved U-range, in both columns, labeled with the shelf hostname.
- Mounted occupants draw inside the reserved range, above the strip: for each
face (`front` → front column, `rear` → rear column), gather that shelf's
occupants for that face, order them by `shelf_slot` ascending, and subdivide
the column width by the **number of occupants on that face** — one labeled box
per tower (hostname), drawn side by side. This produces the approved
"two front, one back" picture.
- Determinism: occupants ordered by `(shelf_slot, hostname)`; shelves processed
in hostname order.
**Occupancy table (`render_page`):**
- Mounted devices list the **shelf's U-range** in the U column (e.g. `U37U46`)
and a `\<shelf_face\> · \<mounted_on\>/\<shelf_slot\>` note in the Face column
(e.g. `front · shf01/1`).
- Ordering: rail items by U; each shelf's mounted devices appear immediately
after the shelf, ordered by `(shelf_face, shelf_slot)`; 0U rail items last.
**Power/network graphs:** unchanged — mounted PCs appear by hostname exactly as
rail-mounted devices do.
## Provisional demo data
Applies the worked example to the existing TaPPaaS nodes (user-confirmed):
- New `docs/hardware/shf01.md`: `kind: shelf`, `status: in-use`, `rack: rack01`,
`rack_u: 37`, `u_height: 10`, `rack_face: both`, `cluster: tappaas`.
- `srv01`: drop `rack_u`/`u_height`/`rack_face`; add `mounted_on: shf01`,
`shelf_face: front`, `shelf_slot: 1`. (Keeps `cluster: tappaas`, `power:`,
`links:`.)
- `srv02`: same, `shelf_face: front`, `shelf_slot: 2`; add `cluster: tappaas`.
- `srv03`: same, `shelf_face: rear`, `shelf_slot: 1`; add `cluster: tappaas`.
- `srv04`, `srv05` stay rail-mounted at U5U6 (unchanged). The shelf block
U37U46 does not overlap any existing rail item (sw01 U10, pp01 U24, srv04/05
U5U6, pdu rails).
All placements remain provisional placeholders; `check_shelves`/`check_overlaps`
reject inconsistent data loudly.
## Integration
| File | Change |
|------|--------|
| `scripts/gen_rack.py` | `validate_item` mounted branch; `check_overlaps` skip mounted; new `check_shelves`; `render_svg` shelf strip + subdivided occupant boxes; `render_page` occupancy rows for mounted items |
| `tests/test_gen_rack.py` | mounted-validation, `check_shelves`, SVG, and occupancy cases |
| `docs/hardware/shf01.md` | New shelf file |
| `docs/hardware/srv01.md`..`srv03.md` | Convert to mounted; `srv02`/`srv03` gain `cluster: tappaas` |
| `docs/hardware/index.md`, `docs/infrastructure/racks/rack01.*` | Regenerated |
No `mkdocs.yml`, `Makefile`, `.forgejo/workflows/docs.yml`, or
`overview_config.yml` changes.
## Test plan
- **Unit — `validate_item`:** accept a valid mounted item; reject a mounted item
that also has `rack_u`/`u_height`/`rack_face`, a bad `shelf_face`, or a
non-integer/<1 `shelf_slot`.
- **Unit `check_overlaps`:** two mounted items sharing their shelf's U-range do
not raise (mounted items are skipped).
- **Unit `check_shelves`:** accept valid mounts; reject `mounted_on` pointing at
a missing item, at a non-`shelf` kind, or at an unplaced shelf; reject two
occupants sharing `(shelf, face, slot)`.
- **Unit `render_svg`:** a shelf with two front + one rear occupant produces a
shelf strip and three labeled occupant boxes; front occupants share the front
column side by side; deterministic for reordered input.
- **Unit `render_page`:** mounted devices appear in the occupancy table with the
shelf's U-range and the `face · shelf/slot` note.
- **Integration `generate`:** valid fixtures write a page; a dangling
`mounted_on` returns `1` and writes nothing.
- **Drift / visual:** `make docs-check` exits 0; `mkdocs build --strict` renders
the rack page with the shelf block showing srv01/srv02 front and srv03 rear.
## Out of scope
- Shelf capacity limits (max slots per face) slot-collision detection suffices.
- Modeling individual tower height separately from the shelf's reserved block.
- Nested shelves or shelves spanning racks.