# 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 U37–U46). 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 U37–U46. - `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: ` — the shelf it sits on. - `shelf_face: front | rear` — which side of the shelf. - `shelf_slot: ` — left-to-right position within that face. Mounted items still declare `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. `U37–U46`) and a `\ · \/\` 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 U5–U6 (unchanged). The shelf block U37–U46 does not overlap any existing rail item (sw01 U10, pp01 U24, srv04/05 U5–U6, 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.