docs(rack): Phase 2 power design spec

This commit is contained in:
sjat 2026-06-24 14:28:48 +02:00
parent 9253d1ca0d
commit f4022edf3b

View file

@ -0,0 +1,163 @@
# 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 `<rack>-elevation.svg` +
`<rack>.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<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`:
```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.