# Rack Power (Phase 2) Design **Date:** 2026-06-24 **Status:** Approved **Parent spec:** `notes/dev/specs/2026-06-24-rack-documentation-design.md` (Phase 2) ## Goal Add power-distribution data to the rack documentation pipeline and render it as a mermaid graph on the generated rack page, so the published page at `docs.makerfloss.eu` shows which PDU/outlet feeds each device and which devices have redundant (dual-PSU) feeds. This reuses every Phase 1 mechanism: the same `scripts/gen_rack.py` generator, the same generated files under `docs/infrastructure/racks/`, and the same CI drift guard. ## Context - Phase 1 (rack elevation) is merged. `scripts/gen_rack.py` reads `docs/hardware/*.md` files carrying a `rack:` field, validates placement (U range, overlap, 0U rules), and writes `-elevation.svg` + `.md` per rack. Tests in `tests/test_gen_rack.py`. - The `pdu` value is already in the `hardware` `kind` enum (`scripts/overview_config.yml`), so PDU files already validate and already appear in the hardware index under "PDUs". - Phase 1 already renders 0U `rack_face: left|right` items as side-rails in the SVG, so PDU files need **no new SVG code** to appear in the elevation. - The Makefile `docs-check` target and CI `Fail on drift` step already diff the **entire** `docs/infrastructure/racks/` directory, so a regenerated page with a power graph is already drift-covered — **no Makefile/CI edits required**. - Mermaid is **not yet enabled** in `mkdocs.yml` (superfences has no mermaid custom fence). Enabling it was nominally a Phase 3 item; Phase 2's graph needs it, so it is **pulled forward** into this phase (decided during brainstorming). - The `mfNN` rack positions are fictional placeholders proving the pipeline. The power data added here is **similarly provisional** until real values are given. ## Data model ### Powered devices — `power:` frontmatter Each powered device gains a `power` list; each entry is one feed: ```yaml power: - { pdu: pdu01, outlet: 1 } - { pdu: pdu02, outlet: 1 } # a second entry = a redundant PSU feed ``` `power` is optional — a device with no `power` field simply contributes no power edges. ### PDU files — new lightweight hardware items ```yaml # docs/hardware/pdu01.md hostname: pdu01 kind: pdu status: in-use rack: rack01 rack_face: left outlets: 8 ``` PDUs are 0U side-rail items (`rack_face: left|right`, no `rack_u`/`u_height`), exactly the shape Phase 1's validator and SVG already handle. `outlets` is a new field, validated by `gen_rack.py` (below). No `overview_config.yml` change is needed: `kind: pdu` is already an enum value, and `outlets` is an extra field that `gen_overview.py` ignores. ### Provisional data populated by this phase - `pdu01` — `rack_face: left`, `outlets: 8`. - `pdu02` — `rack_face: right`, `outlets: 8`. - `mf00..mf04` — fed from `pdu01` outlets 1..5 respectively. - `mf00` — additionally fed from `pdu02` outlet 1 (the redundant demonstration). ## Validation (rule 3 from the parent spec) A new `validate_power(items: list[dict]) -> None` in `gen_rack.py`, called from `generate()` after per-item placement validation and before/with overlap checking. It raises `SchemaError` (→ stderr + exit 1, nothing written) when: 1. A `kind: pdu` file does not declare `outlets` as a positive integer. 2. A `power` value is not a list, or an entry is not a mapping. 3. An entry lacks a non-empty string `pdu` or an integer `outlet`. 4. An entry's `pdu` does not resolve to a loaded file whose `kind == pdu`. 5. An entry's `outlet` is outside `1..outlets` of the referenced PDU. PDU resolution is by `hostname` against all loaded rack items (a PDU lookup map `{hostname: fm for fm in items if kind == pdu}`). ## Rendering ### `render_power(rack, items) -> str` Returns a fenced mermaid block, or `""` when no device in the rack has any `power` entry (so the `## Power` section is omitted for power-less racks). - ` ```mermaid ` + `flowchart LR`. - One node per PDU that is referenced or placed in the rack: label `pdu01
8 outlets`. - One node per powered device: label = hostname. - One edge per feed: `pduNode -->|outlet N| deviceNode`. - Node ids are the hostname with non-alphanumeric characters replaced by `_` (display text keeps the real hostname via the quoted label), guarding against ids that mermaid would reject. - Deterministic: PDU nodes sorted by hostname, device nodes by `(rack_u, hostname)`, edges sorted by `(pdu, outlet, device)`. Redundant feeds render naturally as two edges into one device node, from two different PDUs. ### `render_page` change Insert a `## Power` section containing `render_power(...)` **between** the Elevation and Occupancy sections — only when `render_power` returns non-empty. ## mkdocs (pulled forward) Add the mermaid custom fence to the existing `pymdownx.superfences` entry in `mkdocs.yml`: ```yaml - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format ``` Material ships `mermaid.js` and activates it on this fence, so `mkdocs build --strict` renders the graph as a diagram. ## Integration | File | Change | |------|--------| | `scripts/gen_rack.py` | Add `validate_power`; call it in `generate`; add `render_power`; insert `## Power` in `render_page` | | `tests/test_gen_rack.py` | Add `validate_power` + `render_power` + `generate` power cases | | `mkdocs.yml` | Enable mermaid via superfences custom fence | | `docs/hardware/pdu01.md`, `pdu02.md` | New 0U PDU files (`kind: pdu`, `outlets: 8`) | | `docs/hardware/mf00.md`..`mf04.md` | Add `power:` lists | | `docs/hardware/index.md` | Regenerated (PDUs now listed) | | `docs/infrastructure/racks/rack01.md`, `rack01-elevation.svg` | Regenerated (power section + PDU side-rails) | No `Makefile`, `.forgejo/workflows/docs.yml`, or `overview_config.yml` changes. ## Test plan - **Unit — `validate_power`:** accept a valid feed; reject unknown `pdu`, `pdu` pointing at a non-`pdu` kind, `outlet` of 0 / above `outlets`, a malformed entry (non-mapping / missing keys), and a `pdu` file with missing/zero/non-int `outlets`. - **Unit — `render_power`:** PDU and device nodes present; a redundant device has two incoming edges; returns `""` when no device has power; deterministic for reordered input. - **Integration — `generate`:** with valid fixtures the page contains the `## Power` section and the mermaid fence; with a dangling `pdu` reference it returns `1` and writes nothing. - **Drift:** `make docs-check` exits 0 after regeneration (existing guard, unchanged). - **Visual:** `mkdocs build --strict` succeeds and the rack page shows the power graph as a rendered diagram (not a raw code block), with mf00 showing two incoming feeds.