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

7.5 KiB
Raw Blame History

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.