diff --git a/scripts/gen_rack.py b/scripts/gen_rack.py
index dec52d0..9996e8e 100644
--- a/scripts/gen_rack.py
+++ b/scripts/gen_rack.py
@@ -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''
+ )
+ p.append(
+ f'{_esc(mname)}'
+ )
+ p.append(
+ f''
+ )
+ p.append(
+ f'{_esc(sname)}'
+ )
+
+ 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,20 +577,49 @@ 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)"
- face = fm.get("rack_face", "")
- if face in ZERO_U_FACES:
- urange = "0U"
+ 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:
- 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(
+ face = fm.get("rack_face", "")
+ if face in ZERO_U_FACES:
+ urange = "0U"
+ else:
+ u = fm["rack_u"]
+ h = fm["u_height"]
+ urange = f"U{u}" if h == 1 else f"U{u}–U{u + h - 1}"
+ 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"
diff --git a/tests/test_gen_rack.py b/tests/test_gen_rack.py
index a874675..b3a208b 100644
--- a/tests/test_gen_rack.py
+++ b/tests/test_gen_rack.py
@@ -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 "U37–U46" in page # mounted device shows its shelf's U-range