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

156 lines
7.5 KiB
Markdown
Raw Permalink Normal View 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.