feat(rack): render shelf strip, occupant boxes, and mounted occupancy rows

This commit is contained in:
sjat 2026-06-24 17:45:39 +02:00
parent b85479b9a0
commit aab58e3692
2 changed files with 120 additions and 7 deletions

View file

@ -376,12 +376,61 @@ def render_svg(rack: str, items: list[dict]) -> str:
)
for fm in items:
if fm.get("kind") == "shelf" or "mounted_on" in fm:
continue
face = fm.get("rack_face")
if face in ("front", "both"):
draw_device(fm, front_x)
if face in ("rear", "both"):
draw_device(fm, rear_x)
SHELF_STRIP_H = 6
shelves = [i for i in items if i.get("kind") == "shelf"]
mounted = [i for i in items if "mounted_on" in i]
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'<rect x="{bx + 1}" y="{y + 1}" width="{bw - 2}" '
f'height="{avail_h - 2}" rx="3" fill="{mcolor}" stroke="#333"/>'
)
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(
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>'
)
for fm in sorted(shelves, key=lambda s: s.get("hostname", "")):
draw_shelf(fm)
def draw_rail(fm: dict, x: int) -> None:
color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR)
name = fm.get("hostname", "?")
@ -528,9 +577,28 @@ def render_page(rack: str, items: list[dict]) -> str:
lines.append("")
lines.append("| U | Device | Kind | Face | Status |")
lines.append("|---|---|---|---|---|")
by_host = {fm.get("hostname"): fm for fm in items}
mounted_by_shelf: dict[str, list[dict]] = {}
for fm in items:
if "mounted_on" in fm:
mounted_by_shelf.setdefault(fm["mounted_on"], []).append(fm)
def occ_row(fm: dict) -> str:
name = fm.get("hostname", "?")
link = f"[{name}](../../hardware/{name}.md)"
if "mounted_on" in fm:
target = by_host.get(fm["mounted_on"])
if target and isinstance(target.get("rack_u"), int):
su = target["rack_u"]
sh = target["u_height"]
urange = f"U{su}" if sh == 1 else f"U{su}U{su + sh - 1}"
else:
urange = ""
face = (
f"{fm.get('shelf_face', '')} · "
f"{fm['mounted_on']}/{fm.get('shelf_slot', '')}"
)
else:
face = fm.get("rack_face", "")
if face in ZERO_U_FACES:
urange = "0U"
@ -538,10 +606,20 @@ def render_page(rack: str, items: list[dict]) -> str:
u = fm["rack_u"]
h = fm["u_height"]
urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
lines.append(
return (
f"| {urange} | {link} | {fm.get('kind', '')} | {face} "
f"| {fm.get('status', '')} |"
)
for fm in _sorted_items([i for i in items if "mounted_on" not in i]):
lines.append(occ_row(fm))
if fm.get("kind") == "shelf":
occ = sorted(
mounted_by_shelf.get(fm.get("hostname"), []),
key=lambda m: (m.get("shelf_face", ""), m.get("shelf_slot", 0)),
)
for m in occ:
lines.append(occ_row(m))
lines.append("")
return "\n".join(lines).rstrip() + "\n"

View file

@ -597,3 +597,38 @@ def test_generate_returns_1_on_dangling_mount(tmp_path):
rc = gen_rack.generate(hw, out)
assert rc == 1
assert not (out / "rack01.md").exists()
def test_render_svg_draws_shelf_and_occupants():
items = [
shelf(),
item(hostname="srv01", mounted_on="shf01", shelf_face="front", shelf_slot=1),
item(hostname="srv02", mounted_on="shf01", shelf_face="front", shelf_slot=2),
item(hostname="srv03", mounted_on="shf01", shelf_face="rear", shelf_slot=1),
]
svg = gen_rack.render_svg("rack01", items)
assert "shf01" in svg
assert "srv01" in svg and "srv02" in svg and "srv03" in svg
# the shelf is NOT drawn as a generic full-height device box
assert "shf01 (U37" not in svg
def test_render_svg_shelf_is_deterministic():
base = [
shelf(),
item(hostname="srv02", mounted_on="shf01", shelf_face="front", shelf_slot=2),
item(hostname="srv01", mounted_on="shf01", shelf_face="front", shelf_slot=1),
]
assert gen_rack.render_svg("rack01", base) == gen_rack.render_svg(
"rack01", list(reversed(base))
)
def test_render_page_lists_mounted_devices():
items = [shelf(),
item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1)]
page = gen_rack.render_page("rack01", items)
assert "../../hardware/srv01.md" in page
assert "front · shf01/1" in page
assert "U37U46" in page # mounted device shows its shelf's U-range