feat(rack): render shelf strip, occupant boxes, and mounted occupancy rows
This commit is contained in:
parent
b85479b9a0
commit
aab58e3692
2 changed files with 120 additions and 7 deletions
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue