diff --git a/notes/dev/specs/2026-06-24-rack-documentation-design.md b/notes/dev/specs/2026-06-24-rack-documentation-design.md new file mode 100644 index 0000000..48be986 --- /dev/null +++ b/notes/dev/specs/2026-06-24-rack-documentation-design.md @@ -0,0 +1,168 @@ +# Rack Documentation Design + +**Date:** 2026-06-24 +**Status:** Approved + +## Goal + +Document the 48U server rack as hand-edited Markdown that a CI pass turns into a +clear visual presentation, mirroring the existing `docs/hardware/*.md` → +`gen_overview.py` → generated-index pattern. The rendered output covers three +views: a physical rack **elevation** (SVG), a **power** distribution graph +(mermaid), and a **network cabling** graph (mermaid). + +Authors (operator or AI) edit frontmatter; a push regenerates the artifacts and +CI fails on drift, so the published page at `docs.makerfloss.eu` is always in +sync with the source. + +## Context + +- Per-item frontmatter is already the norm: `docs/hardware/*.md` and + `docs/services/*.md` carry YAML frontmatter validated by + `scripts/gen_overview.py` against schemas in `scripts/overview_config.yml`. +- The generator writes a grouped/sorted `index.md`; CI (`.forgejo/workflows/docs.yml`) + regenerates it and runs `git diff --exit-code` to **fail on drift**. +- Mermaid already renders in the Marp slide pipeline; it is **not yet enabled in + MkDocs**. Enabling it is a small `mkdocs.yml` change (superfences custom fence; + Material ships mermaid.js). +- The rack contains devices that already have host pages (`mf01`..`mf04`). +- **The physical rack is labeled U1 at the top**, descending to U48 at the + bottom (non-standard; standard racks number U1 at the bottom). The elevation + must match the physical labels. + +## Data model + +Rack data is added to **host frontmatter** (decision: extend existing files +rather than introduce a separate layout file). Rack-mounted items that are not +hosts (PDUs, patch panels, shelves, blank panels, UPS, KVM) each get their own +lightweight file in `docs/hardware/` using new `kind` values. + +### Frontmatter fields + +```yaml +# placement (Phase 1) +rack: rack01 # rack identifier; one rack today, field enables future racks +rack_u: 12 # lowest U occupied (1–48) +u_height: 2 # number of U occupied +rack_face: front # front | rear | both | left | right (0U PDUs use left/right rails) + +# power (Phase 2) — on each powered device +power: + - { pdu: pdu01, outlet: 3 } + - { pdu: pdu02, outlet: 3 } # a second entry expresses a redundant PSU feed + +# network (Phase 3) — on each device, one entry per cable end originating here +links: + - { local: eth0, peer: sw01, peer_port: 12, speed_gbps: 1 } +``` + +### New `kind` enum values + +Extend the `hardware` enum in `overview_config.yml` with: `pdu`, `patch-panel`, +`shelf`, `blank`, `ups`, `kvm` (joining the existing `server`, `laptop`, `sbc`, +`switch`, `ap`, `desktop`). + +Non-host item files declare their own capacity where relevant: + +```yaml +# docs/hardware/pdu01.md +hostname: pdu01 +kind: pdu +status: in-use +rack: rack01 +rack_face: left +outlets: 8 +``` + +```yaml +# docs/hardware/pp01.md +hostname: pp01 +kind: patch-panel +status: in-use +rack: rack01 +rack_u: 24 +u_height: 1 +rack_face: front +ports: 24 +``` + +## Generator: `scripts/gen_rack.py` + +A sibling to `gen_overview.py`, sharing its style (stdlib + PyYAML, deterministic, +offline, `SchemaError` → non-zero exit). It reads every `docs/hardware/*.md` with +a `rack:` field, validates the rack schema, groups by `rack`, and writes +generated artifacts per rack. + +### Outputs (do-not-edit, generated) + +- `docs/infrastructure/racks/rack01-elevation.svg` — the elevation picture. +- `docs/infrastructure/racks/rack01.md` — generated page embedding, in order: + 1. the elevation SVG (`![Rack rack01 elevation](rack01-elevation.svg)`), + 2. a mermaid **power** graph (Phase 2), + 3. a mermaid **network** graph (Phase 3), + 4. an occupancy table (U-range, hostname/link, kind, face, status). + +Each generated file carries the same "Auto-generated … do not edit by hand" +banner the existing indices use. + +### Rendering + +- **Elevation (SVG):** two side-by-side columns, **front** and **rear**. 48 U + rows numbered **U1 at the top → U48 at the bottom** to match the physical + rack. Each device is a rectangle spanning its true `u_height`, filled by a + per-`kind` color, labeled with hostname and U-range. Empty U slots drawn + faintly. 0U side-rail items (`rack_face: left|right`) drawn as thin vertical + bars beside the columns. Plain hand-written SVG strings — no external drawing + library. +- **Power (mermaid):** `flowchart` of `pdu → (outlet) → device`; redundant feeds + appear as multiple edges into one device. +- **Network (mermaid):** `flowchart` of `device[:local] -- speed --> peer[:port]` + edges built from `links`. + +### Validation rules (CI-enforced, fail with a clear message) + +1. `rack_u` in 1–48; `rack_u + u_height − 1 ≤ 48`. +2. No two items overlap the same U range on the same `rack_face` within a rack. +3. Every `power[].pdu` resolves to a file whose `kind: pdu`; `outlet` within that + PDU's `outlets`. +4. Every `links[].peer` resolves to a real file; `peer_port` within the peer's + declared `ports`/port count where the peer declares one. +5. Items with `rack_face: left|right` (0U) must omit `rack_u`/`u_height`; + all other rack items must include them. + +## Integration + +| File | Change | +|------|--------| +| `scripts/gen_rack.py` | New generator (SVG + mermaid + table + validation) | +| `scripts/overview_config.yml` | Extend `hardware` `kind` enum with new values; optional `rack` config block if reusing config-driven validation | +| `docs/hardware/*.md` | Add placement (then power, then links) fields to rack occupants; add new non-host item files | +| `docs/infrastructure/racks/` | New dir holding generated `rack01.md` + `rack01-elevation.svg` | +| `Makefile` | `docs-index`/`docs-check` gain a `gen_rack.py` step | +| `.forgejo/workflows/docs.yml` | Run `gen_rack.py`; extend the drift `git diff` guard to the generated rack artifacts | +| `mkdocs.yml` | Enable mermaid (superfences custom fence) [Phase 3]; add the rack page to `nav` | + +## Phasing + +Each phase is independently shippable as its own PR. + +1. **Phase 1 — Elevation.** Placement schema + new kinds + `gen_rack.py` + producing the SVG elevation and occupancy table + CI drift check + nav entry. + Populate `rack01` with the current devices. +2. **Phase 2 — Power.** `power` fields + PDU files + generated mermaid power + graph + validation rules 3. +3. **Phase 3 — Network.** `links` fields + patch-panel files + generated mermaid + network graph + validation rule 4 + enable mermaid in `mkdocs.yml`. + +## Test plan + +- **Unit (per phase):** run `python3 scripts/gen_rack.py`; assert it writes the + expected artifacts and exits 0 on a valid fixture set. +- **Validation:** craft fixtures that violate each rule (U overflow, overlap, + dangling `pdu`/`peer`, bad outlet/port, 0U with U fields) and assert non-zero + exit with the right message. +- **Drift:** run the generator, confirm `git diff --exit-code` is clean; mutate a + source file without regenerating and confirm CI's guard fails. +- **Visual:** `make docs-build` (or `docs-serve`), open the rack page, confirm + the SVG shows U1 at the top, devices at correct positions/faces, and (Phase + 2/3) the mermaid graphs render rather than appearing as code blocks.