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