6.7 KiB
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.pyreadsdocs/hardware/*.mdfiles carrying arack:field, validates placement (U range, overlap, 0U rules), and writes<rack>-elevation.svg+<rack>.mdper rack. Tests intests/test_gen_rack.py. - The
pduvalue is already in thehardwarekindenum (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|rightitems as side-rails in the SVG, so PDU files need no new SVG code to appear in the elevation. - The Makefile
docs-checktarget and CIFail on driftstep already diff the entiredocs/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
mfNNrack 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:
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
# 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 frompdu01outlets 1..5 respectively.mf00— additionally fed frompdu02outlet 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:
- A
kind: pdufile does not declareoutletsas a positive integer. - A
powervalue is not a list, or an entry is not a mapping. - An entry lacks a non-empty string
pduor an integeroutlet. - An entry's
pdudoes not resolve to a loaded file whosekind == pdu. - An entry's
outletis outside1..outletsof 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<br/>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:
- 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 unknownpdu,pdupointing at a non-pdukind,outletof 0 / aboveoutlets, a malformed entry (non-mapping / missing keys), and apdufile with missing/zero/non-intoutlets. - 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## Powersection and the mermaid fence; with a danglingpdureference it returns1and writes nothing. - Drift:
make docs-checkexits 0 after regeneration (existing guard, unchanged). - Visual:
mkdocs build --strictsucceeds and the rack page shows the power graph as a rendered diagram (not a raw code block), with mf00 showing two incoming feeds.