MakerFLOSS/notes/dev/plans/2026-06-24-rack-presentation.md

25 KiB
Raw Permalink Blame History

Rack Presentation Improvements Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make the generated rack elevation interactive (inline SVG, clickable boxes, hover tooltips) with status-encoded borders and a legend, and theme the mermaid power/network graphs (colour-by-kind + clickable nodes).

Architecture: Pure rendering upgrade in scripts/gen_rack.py. New module-level helpers (_host_url, _status_stroke, _stroke_attrs, _placement, _tooltip); render_svg wraps each device in <a>+<title>, encodes status via border, adds a legend, both-gutter U-numbers, and column frames; render_page inlines the SVG (via md_in_html) with a download link; render_power/render_network add per-node style + click. Each task regenerates the artifacts so the drift guard stays green.

Tech Stack: Python 3 (stdlib + PyYAML only), pytest, MkDocs Material (md_in_html, mermaid), Forgejo Actions CI.

Spec: notes/dev/specs/2026-06-24-rack-presentation-design.md.

Global Constraints

  • Scripts use stdlib + PyYAML only; deterministic and offline. re/yaml already imported — no new imports.
  • Links/clicks use root-relative final URLs /hardware/<host>/ (mkdocs does not rewrite raw hrefs inside inline SVG or mermaid; use_directory_urls defaults true; site at domain root).
  • Status border mapping (verbatim): in-use #333333/1.5/solid; staging #333333/1.5/dash 4 2; broken #e15759/3/solid; spare #bbbbbb/1.5/solid; donated #bbbbbb/1.5/solid; other → #333333/1.5/solid. Fill stays the kind colour.
  • The inline elevation block is <div class="rack-elevation"> + the SVG markup (no blank lines inside) + </div>; no markdown attribute (raw passthrough). The standalone rack01-elevation.svg is still generated, plus a [Download SVG](rack01-elevation.svg) link.
  • Tooltip text: "<host> · <kind> · <status> · cluster: <cluster|—> · <placement>", _esc-applied; placement = U-range / 0U <face> / <shelf>/<face>/slot <slot>.
  • Reuse item()/_write_item()/shelf() test helpers; do not redefine them.
  • Each task ends with regenerated docs/infrastructure/racks/rack01.{md,svg}, make test green, mkdocs build --strict passing, and make docs-check exit 0.
  • No edits to mkdocs.yml, Makefile, .forgejo/workflows/docs.yml, or scripts/overview_config.yml.

Files:

  • Modify: scripts/gen_rack.py (helpers; render_svg <a>/<title>/status borders + style= on <svg>; render_page inline + download link)
  • Modify: tests/test_gen_rack.py (append tests)
  • Regenerate: docs/infrastructure/racks/rack01.md, rack01-elevation.svg

Interfaces:

  • Produces (module-level, used by Task 2): _host_url(host) -> str, _status_stroke(status) -> tuple[str, float, str], _stroke_attrs(status) -> str, _placement(fm) -> str, _tooltip(fm) -> str.

  • Step 1: Append failing tests to tests/test_gen_rack.py

def test_svg_boxes_link_to_host_pages():
    items = [item(hostname="srv04", rack_u=5, u_height=1, rack_face="front")]
    svg = gen_rack.render_svg("rack01", items)
    assert '<a href="/hardware/srv04/">' in svg
    assert "<title>" in svg


def test_svg_status_border_styles():
    staging = gen_rack.render_svg("rack01", [
        item(hostname="a", rack_u=1, u_height=1, rack_face="front",
             status="staging")])
    broken = gen_rack.render_svg("rack01", [
        item(hostname="b", rack_u=1, u_height=1, rack_face="front",
             status="broken")])
    assert 'stroke-dasharray="4 2"' in staging
    assert 'stroke="#e15759"' in broken and 'stroke-width="3"' in broken


def test_svg_tooltip_has_cluster_and_placement():
    items = [item(hostname="srv01", rack_u=1, u_height=1, rack_face="front",
                  status="staging", cluster="tappaas")]
    svg = gen_rack.render_svg("rack01", items)
    assert "cluster: tappaas" in svg
    assert "U1" in svg


def test_svg_has_responsive_style():
    svg = gen_rack.render_svg("rack01", [])
    assert "max-width:100%" in svg


def test_render_page_inlines_svg_with_download_link():
    items = [item(hostname="srv04", rack_u=5, u_height=1, rack_face="front")]
    page = gen_rack.render_page("rack01", items)
    assert '<div class="rack-elevation">' in page
    assert "<svg" in page
    assert "[Download SVG](rack01-elevation.svg)" in page
    assert "![Rack rack01 elevation]" not in page
  • Step 2: Run to verify failure

Run: pytest tests/test_gen_rack.py -q Expected: FAIL — the new assertions fail (<a href / <title> / max-width / inline <div> not present yet).

  • Step 3: Add the helper functions after _esc in scripts/gen_rack.py

Insert immediately after the _esc function (before _sorted_items):

STATUS_STROKE: dict[str, tuple[str, float, str]] = {
    "in-use": ("#333333", 1.5, ""),
    "staging": ("#333333", 1.5, "4 2"),
    "broken": ("#e15759", 3, ""),
    "spare": ("#bbbbbb", 1.5, ""),
    "donated": ("#bbbbbb", 1.5, ""),
}
DEFAULT_STATUS_STROKE: tuple[str, float, str] = ("#333333", 1.5, "")


def _status_stroke(status: object) -> tuple[str, float, str]:
    return STATUS_STROKE.get(status, DEFAULT_STATUS_STROKE)


def _stroke_attrs(status: object) -> str:
    stroke, sw, dash = _status_stroke(status)
    dash_attr = f' stroke-dasharray="{dash}"' if dash else ""
    return f'stroke="{stroke}" stroke-width="{sw}"{dash_attr}'


def _host_url(host: object) -> str:
    return f"/hardware/{host}/"


def _placement(fm: dict) -> str:
    if "mounted_on" in fm:
        return (
            f"{fm.get('mounted_on', '?')}/{fm.get('shelf_face', '')}/"
            f"slot {fm.get('shelf_slot', '')}"
        )
    face = fm.get("rack_face")
    if face in ZERO_U_FACES:
        return f"0U {face}"
    u = fm.get("rack_u")
    h = fm.get("u_height")
    if isinstance(u, int) and isinstance(h, int):
        return f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
    return "?"


def _tooltip(fm: dict) -> str:
    host = fm.get("hostname", "?")
    return _esc(
        f"{host} · {fm.get('kind', '')} · {fm.get('status', '')} · "
        f"cluster: {fm.get('cluster', '—')} · {_placement(fm)}"
    )
  • Step 4: Add the responsive style to the <svg> tag in render_svg

Replace:

    p.append(
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" '
        f'height="{height}" viewBox="0 0 {width} {height}" '
        f'font-family="sans-serif" font-size="11">'
    )

with:

    p.append(
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" '
        f'height="{height}" viewBox="0 0 {width} {height}" '
        f'style="max-width:100%;height:auto" '
        f'font-family="sans-serif" font-size="11">'
    )
  • Step 5: Wrap draw_device in a link + title and use the status border

Replace the whole draw_device function with:

    def draw_device(fm: dict, col_x: int) -> None:
        u = fm["rack_u"]
        h = fm["u_height"]
        y = u_y(u)
        box_h = h * U_H
        color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR)
        name = fm.get("hostname", "?")
        urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
        p.append(f'<a href="{_host_url(name)}">')
        p.append(f"<title>{_tooltip(fm)}</title>")
        p.append(
            f'<rect x="{col_x + 1}" y="{y + 1}" width="{COL_W - 2}" '
            f'height="{box_h - 2}" rx="3" fill="{color}" '
            f"{_stroke_attrs(fm.get('status'))}/>"
        )
        p.append(
            f'<text x="{col_x + COL_W // 2}" y="{y + box_h // 2 + 4}" '
            f'text-anchor="middle" fill="#ffffff">'
            f"{_esc(name)} ({urange})</text>"
        )
        p.append("</a>")
  • Step 6: Wrap draw_rail in a link + title and use the status border

Replace the whole draw_rail function with:

    def draw_rail(fm: dict, x: int) -> None:
        color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR)
        name = fm.get("hostname", "?")
        cx = x + RAIL_W // 2
        cy = top + body_h // 2
        p.append(f'<a href="{_host_url(name)}">')
        p.append(f"<title>{_tooltip(fm)}</title>")
        p.append(
            f'<rect x="{x}" y="{top}" width="{RAIL_W}" height="{body_h}" '
            f"fill=\"{color}\" {_stroke_attrs(fm.get('status'))}/>"
        )
        p.append(
            f'<text x="{cx}" y="{cy}" text-anchor="middle" fill="#ffffff" '
            f'transform="rotate(-90 {cx} {cy})">{_esc(name)}</text>'
        )
        p.append("</a>")
  • Step 7: Make shelf occupants and the shelf strip links in draw_shelf

Replace the whole draw_shelf function with this version (occupant boxes each link to their host; the two shelf strips + label are one link to the shelf host):

    def draw_shelf(fm: dict) -> None:
        u = fm["rack_u"]
        h = fm["u_height"]
        y = u_y(u)
        block_h = h * U_H
        strip_y = y + block_h - SHELF_STRIP_H
        avail_h = block_h - SHELF_STRIP_H
        shelf_color = KIND_COLORS.get("shelf", DEFAULT_COLOR)
        sname = fm.get("hostname", "?")
        for col_x, sface in ((front_x, "front"), (rear_x, "rear")):
            occ = sorted(
                (m for m in mounted
                 if m.get("mounted_on") == sname
                 and m.get("shelf_face") == sface),
                key=lambda m: (m.get("shelf_slot", 0), m.get("hostname", "")),
            )
            n = len(occ)
            for idx, m in enumerate(occ):
                sub_w = COL_W // n
                bx = col_x + idx * sub_w
                bw = (COL_W - idx * sub_w) if idx == n - 1 else sub_w
                mcolor = KIND_COLORS.get(m.get("kind", ""), DEFAULT_COLOR)
                mname = m.get("hostname", "?")
                p.append(f'<a href="{_host_url(mname)}">')
                p.append(f"<title>{_tooltip(m)}</title>")
                p.append(
                    f'<rect x="{bx + 1}" y="{y + 1}" width="{bw - 2}" '
                    f'height="{avail_h - 2}" rx="3" fill="{mcolor}" '
                    f"{_stroke_attrs(m.get('status'))}/>"
                )
                p.append(
                    f'<text x="{bx + bw // 2}" y="{y + avail_h // 2 + 4}" '
                    f'text-anchor="middle" fill="#ffffff">{_esc(mname)}</text>'
                )
                p.append("</a>")
        p.append(f'<a href="{_host_url(sname)}">')
        p.append(f"<title>{_tooltip(fm)}</title>")
        for col_x in (front_x, rear_x):
            p.append(
                f'<rect x="{col_x}" y="{strip_y}" width="{COL_W}" '
                f'height="{SHELF_STRIP_H}" fill="{shelf_color}" stroke="#333"/>'
            )
        p.append(
            f'<text x="{front_x + COL_W // 2}" y="{strip_y + SHELF_STRIP_H - 1}" '
            f'text-anchor="middle" fill="#333" font-size="9">{_esc(sname)}</text>'
        )
        p.append("</a>")
  • Step 8: Inline the SVG in render_page with a download link

In render_page, replace:

    lines.append("## Elevation")
    lines.append("")
    lines.append(f"![Rack {rack} elevation]({rack}-elevation.svg)")
    lines.append("")

with:

    lines.append("## Elevation")
    lines.append("")
    lines.append('<div class="rack-elevation">')
    lines.append(render_svg(rack, items).rstrip())
    lines.append("</div>")
    lines.append("")
    lines.append(f"[Download SVG]({rack}-elevation.svg)")
    lines.append("")
  • Step 9: Run to verify pass

Run: pytest tests/test_gen_rack.py -q Expected: PASS (all prior tests + 5 new).

  • Step 10: Regenerate and verify build + drift

Run: make docs-index Expected: Wrote rack01.md + rack01-elevation.svg (10 item(s)).

Run: grep -c 'href="/hardware/' docs/infrastructure/racks/rack01.md Expected: ≥ 10 (one link per device box + the shelf).

Run: make test Expected: PASS.

Run: mkdocs build --strict (or python3 -m mkdocs build --strict) Expected: build succeeds, no warnings-as-errors.

Run: grep -c '<svg' site/infrastructure/racks/rack01/index.html && grep -c 'href="/hardware/' site/infrastructure/racks/rack01/index.html Expected: both ≥ 1 — confirms the inline SVG and its links survived md_in_html into the built HTML (not escaped).

Run: make docs-check Expected: exit 0.

  • Step 11: Commit
git add scripts/gen_rack.py tests/test_gen_rack.py docs/infrastructure/racks/
git commit -m "feat(rack): inline interactive elevation with links, tooltips, status borders"

Task 2: Legend, both-gutter U-numbers, column frames

Files:

  • Modify: scripts/gen_rack.py (render_svg layout + legend)
  • Modify: tests/test_gen_rack.py (append tests)
  • Regenerate: docs/infrastructure/racks/rack01.md, rack01-elevation.svg

Interfaces:

  • Consumes: _status_stroke (Task 1), KIND_COLORS, DEFAULT_COLOR, _esc.

  • Step 1: Append failing tests to tests/test_gen_rack.py

def test_svg_legend_shows_present_kinds():
    items = [item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
                  rack_face="front")]
    svg = gen_rack.render_svg("rack01", items)
    assert ">Legend<" in svg
    assert ">switch<" in svg


def test_svg_legend_omits_absent_kinds():
    items = [item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
                  rack_face="front")]
    svg = gen_rack.render_svg("rack01", items)
    assert ">ups<" not in svg


def test_svg_u_numbers_in_both_gutters():
    svg = gen_rack.render_svg("rack01", [])
    assert 'text-anchor="end"' in svg   # left gutter
    assert 'text-anchor="start"' in svg  # right gutter


def test_svg_has_column_frames():
    svg = gen_rack.render_svg("rack01", [])
    assert svg.count('fill="none"') >= 2  # one frame per column
  • Step 2: Run to verify failure

Run: pytest tests/test_gen_rack.py -q Expected: FAIL — >Legend<, right-gutter text-anchor="start", and fill="none" frames are not present yet.

  • Step 3: Add the LEGEND_H constant and grow the canvas

In render_svg, the constants block currently ends with TITLE_H = 28. Add a legend-band constant right after it:

    TITLE_H = 28
    LEGEND_H = 56

Then replace the height line:

    height = PAD + TITLE_H + body_h + PAD

with:

    height = PAD + TITLE_H + body_h + PAD + LEGEND_H
  • Step 4: Add a right-side U-number gutter to the layout

Replace:

    front_x = PAD + len(left_items) * RAIL_W + LABEL_W
    rear_x = front_x + COL_W + GAP
    width = rear_x + COL_W + len(right_items) * RAIL_W + PAD

with:

    front_x = PAD + len(left_items) * RAIL_W + LABEL_W
    rear_x = front_x + COL_W + GAP
    right_gutter_x = rear_x + COL_W
    width = right_gutter_x + LABEL_W + len(right_items) * RAIL_W + PAD

And replace the right-rail drawing loop:

    for idx, fm in enumerate(right_items):
        draw_rail(fm, rear_x + COL_W + idx * RAIL_W)

with:

    for idx, fm in enumerate(right_items):
        draw_rail(fm, right_gutter_x + LABEL_W + idx * RAIL_W)
  • Step 5: Draw the column frames and the right-gutter U-numbers

The left-gutter U-number loop currently reads:

    # U numbers in the gutter left of the front column.
    for u in range(1, RACK_UNITS + 1):
        y = u_y(u)
        p.append(
            f'<text x="{front_x - 4}" y="{y + 14}" text-anchor="end" '
            f'fill="#999">{u}</text>'
        )

Immediately after that loop, add the right-gutter numbers and the two column frames:

    for u in range(1, RACK_UNITS + 1):
        y = u_y(u)
        p.append(
            f'<text x="{right_gutter_x + 4}" y="{y + 14}" text-anchor="start" '
            f'fill="#999">{u}</text>'
        )
    for col_x in (front_x, rear_x):
        p.append(
            f'<rect x="{col_x}" y="{top}" width="{COL_W}" height="{body_h}" '
            f'fill="none" stroke="#999" stroke-width="1.5"/>'
        )
  • Step 6: Draw the legend before </svg>

The function currently ends:

    for fm in sorted(shelves, key=lambda s: s.get("hostname", "")):
        draw_shelf(fm)

    p.append("</svg>")
    return "\n".join(p) + "\n"

Insert the legend between the shelf loop and p.append("</svg>"):

    for fm in sorted(shelves, key=lambda s: s.get("hostname", "")):
        draw_shelf(fm)

    legend_y = top + body_h + PAD + 8
    p.append(
        f'<text x="{front_x}" y="{legend_y}" font-weight="bold">Legend</text>'
    )
    present_kinds = sorted({i.get("kind", "") for i in items if i.get("kind")})
    kx = front_x
    ky = legend_y + 18
    for kind in present_kinds:
        color = KIND_COLORS.get(kind, DEFAULT_COLOR)
        p.append(
            f'<rect x="{kx}" y="{ky - 10}" width="12" height="12" '
            f'fill="{color}" stroke="#333"/>'
        )
        p.append(f'<text x="{kx + 16}" y="{ky}">{_esc(kind)}</text>')
        kx += 28 + 7 * len(kind)
    sx = front_x
    sy = ky + 18
    for label in ("in-use", "staging", "broken", "spare"):
        stroke, sw, dash = _status_stroke(label)
        dash_attr = f' stroke-dasharray="{dash}"' if dash else ""
        p.append(
            f'<rect x="{sx}" y="{sy - 10}" width="12" height="12" '
            f'fill="#ffffff" stroke="{stroke}" stroke-width="{sw}"{dash_attr}/>'
        )
        p.append(f'<text x="{sx + 16}" y="{sy}">{_esc(label)}</text>')
        sx += 28 + 7 * len(label)

    p.append("</svg>")
    return "\n".join(p) + "\n"
  • Step 7: Run to verify pass

Run: pytest tests/test_gen_rack.py -q Expected: PASS (all prior tests + 4 new).

  • Step 8: Regenerate and verify build + drift

Run: make docs-index Expected: Wrote rack01.md + rack01-elevation.svg (10 item(s)).

Run: grep -c ">Legend<" docs/infrastructure/racks/rack01-elevation.svg Expected: 1.

Run: make test Expected: PASS.

Run: mkdocs build --strict (or python3 -m mkdocs build --strict) Expected: build succeeds.

Run: make docs-check Expected: exit 0.

  • Step 9: Commit
git add scripts/gen_rack.py tests/test_gen_rack.py docs/infrastructure/racks/
git commit -m "feat(rack): add elevation legend, both-gutter U-numbers, column frames"

Task 3: Mermaid node colours + clickable nodes

Files:

  • Modify: scripts/gen_rack.py (render_power, render_network)
  • Modify: tests/test_gen_rack.py (append tests)
  • Regenerate: docs/infrastructure/racks/rack01.md

Interfaces:

  • Consumes: _node_id, _host_url (Task 1), KIND_COLORS, DEFAULT_COLOR.

Note on mermaid click interactivity. Node colouring (style …) always works. Whether a click actually navigates depends on Material's mermaid securityLevel (default strict may render the link inert) — and this phase makes no mkdocs.yml change. The click directive still parses and the graph still renders either way; the tests only assert the directives are emitted. In Step 6, confirm the graphs still render as diagrams (not broken code blocks). If clicks turn out inert in the built site, that is acceptable for this phase (the SVG elevation already provides clickable navigation) — note it in the report rather than adding config.

  • Step 1: Append failing tests to tests/test_gen_rack.py
def test_power_graph_colors_and_links_nodes():
    items = [
        item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
        item(hostname="srv01", rack_u=1, u_height=1, rack_face="front",
             power=[{"pdu": "pdu01", "outlet": 1}]),
    ]
    out = gen_rack.render_power("rack01", items)
    assert "style srv01 fill:" in out
    assert "style pdu01 fill:" in out
    assert 'click srv01 "/hardware/srv01/"' in out


def test_network_graph_colors_and_links_nodes():
    items = [
        item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
             rack_face="front", ports=24),
        item(hostname="srv01", rack_u=1, u_height=1, rack_face="front",
             links=[{"local": "eth0", "peer": "sw01", "peer_port": 1}]),
    ]
    out = gen_rack.render_network("rack01", items)
    assert "style sw01 fill:" in out
    assert 'click sw01 "/hardware/sw01/"' in out
    assert 'click srv01 "/hardware/srv01/"' in out
  • Step 2: Run to verify failure

Run: pytest tests/test_gen_rack.py -q Expected: FAIL — style …/click … lines are not emitted yet.

  • Step 3: Emit node colours + clicks in render_power

In render_power, the function currently ends:

    for pdu, outlet, device in edges:
        lines.append(
            f"    {_node_id(pdu)} -->|outlet {outlet}| {_node_id(device)}"
        )
    lines.append("```")
    return "\n".join(lines) + "\n"

Insert a styling/click block between the edge loop and lines.append("```"):

    for pdu, outlet, device in edges:
        lines.append(
            f"    {_node_id(pdu)} -->|outlet {outlet}| {_node_id(device)}"
        )
    by_host = {fm.get("hostname"): fm for fm in items}
    node_hosts = sorted(set(pdus) | {fm.get("hostname", "?") for fm in powered})
    for host in node_hosts:
        kind = by_host.get(host, {}).get("kind", "")
        color = KIND_COLORS.get(kind, DEFAULT_COLOR)
        nid = _node_id(host)
        lines.append(
            f"    style {nid} fill:{color},stroke:#333,color:#ffffff"
        )
        lines.append(f'    click {nid} "{_host_url(host)}"')
    lines.append("```")
    return "\n".join(lines) + "\n"
  • Step 4: Emit node colours + clicks in render_network

In render_network, the function currently ends:

    for source, local, peer, peer_port, speed in edges:
        label = f"{local} → p{peer_port}"
        if speed is not None:
            label += f" · {speed}G"
        lines.append(f"    {_node_id(source)} -->|{label}| {_node_id(peer)}")
    lines.append("```")
    return "\n".join(lines) + "\n"

Insert a styling/click block between the edge loop and lines.append("```"):

    for source, local, peer, peer_port, speed in edges:
        label = f"{local} → p{peer_port}"
        if speed is not None:
            label += f" · {speed}G"
        lines.append(f"    {_node_id(source)} -->|{label}| {_node_id(peer)}")
    for host in sorted(nodes):
        kind = by_host.get(host, {}).get("kind", "")
        color = KIND_COLORS.get(kind, DEFAULT_COLOR)
        nid = _node_id(host)
        lines.append(
            f"    style {nid} fill:{color},stroke:#333,color:#ffffff"
        )
        if host in by_host:
            lines.append(f'    click {nid} "{_host_url(host)}"')
    lines.append("```")
    return "\n".join(lines) + "\n"

(by_host already exists in render_network; off-rack peers — not in by_host — get a default-coloured node and no click, per the spec.)

  • Step 5: Run to verify pass

Run: pytest tests/test_gen_rack.py -q Expected: PASS (all prior tests + 2 new).

  • Step 6: Regenerate and verify build + drift

Run: make docs-index Expected: Wrote rack01.md + rack01-elevation.svg (10 item(s)).

Run: grep -c "click srv01" docs/infrastructure/racks/rack01.md Expected: 2 (one in the power graph, one in the network graph).

Run: make test Expected: PASS.

Run: mkdocs build --strict (or python3 -m mkdocs build --strict) Expected: build succeeds.

Run: grep -c 'class="mermaid"' site/infrastructure/racks/rack01/index.html Expected: ≥ 2 — confirms both graphs still render as diagrams after adding style/click (i.e. the directives did not break mermaid parsing).

Run: make docs-check Expected: exit 0.

  • Step 7: Commit
git add scripts/gen_rack.py tests/test_gen_rack.py docs/infrastructure/racks/
git commit -m "feat(rack): colour and link mermaid power/network nodes by kind"

Self-Review

Spec coverage (2026-06-24-rack-presentation-design.md):

  • A. Inline SVG + clickable boxes + tooltips + standalone .svg + download link + responsive style — Task 1. ✔
  • B. Status border encoding (table verbatim) — Task 1 (_status_stroke/_stroke_attrs, applied in draw_device/draw_rail/draw_shelf). ✔
  • C. Legend (present kinds + status key) — Task 2. ✔
  • D. U-numbers both gutters, column frame — Task 2; mermaid node colours + clicks — Task 3. ✔
  • Root-relative /hardware/<host>/ URLs — Task 1 (_host_url), used in SVG links and mermaid clicks. ✔
  • md_in_html raw passthrough (<div class="rack-elevation">, no markdown attr) — Task 1 Step 8, verified Task 1 Step 10 (built HTML contains <svg/href). ✔
  • No mkdocs/Makefile/CI/overview_config changes; occupancy + validation unchanged — honored. ✔

Placeholder scan: No "TBD"/"handle edge cases"/"similar to Task N". Legend spacing uses a deterministic width heuristic (28 + 7*len(label)), not a placeholder.

Type consistency: _status_stroke -> tuple[str, float, str]; _stroke_attrs/_host_url/_placement/_tooltip -> str; render_svg/render_page/render_power/render_network -> str. _status_stroke defined in Task 1 is reused by Task 2's legend; _host_url defined in Task 1 is reused by Task 3's clicks. by_host/nodes/pdus/powered names in the mermaid edits match the existing function locals. Each task regenerates the artifacts so drift stays clean at every boundary.