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:
|
for fm in items:
|
||||||
|
if fm.get("kind") == "shelf" or "mounted_on" in fm:
|
||||||
|
continue
|
||||||
face = fm.get("rack_face")
|
face = fm.get("rack_face")
|
||||||
if face in ("front", "both"):
|
if face in ("front", "both"):
|
||||||
draw_device(fm, front_x)
|
draw_device(fm, front_x)
|
||||||
if face in ("rear", "both"):
|
if face in ("rear", "both"):
|
||||||
draw_device(fm, rear_x)
|
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:
|
def draw_rail(fm: dict, x: int) -> None:
|
||||||
color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR)
|
color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR)
|
||||||
name = fm.get("hostname", "?")
|
name = fm.get("hostname", "?")
|
||||||
|
|
@ -528,20 +577,49 @@ def render_page(rack: str, items: list[dict]) -> str:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("| U | Device | Kind | Face | Status |")
|
lines.append("| U | Device | Kind | Face | Status |")
|
||||||
lines.append("|---|---|---|---|---|")
|
lines.append("|---|---|---|---|---|")
|
||||||
|
by_host = {fm.get("hostname"): fm for fm in items}
|
||||||
|
mounted_by_shelf: dict[str, list[dict]] = {}
|
||||||
for fm in items:
|
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", "?")
|
name = fm.get("hostname", "?")
|
||||||
link = f"[{name}](../../hardware/{name}.md)"
|
link = f"[{name}](../../hardware/{name}.md)"
|
||||||
face = fm.get("rack_face", "")
|
if "mounted_on" in fm:
|
||||||
if face in ZERO_U_FACES:
|
target = by_host.get(fm["mounted_on"])
|
||||||
urange = "0U"
|
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:
|
else:
|
||||||
u = fm["rack_u"]
|
face = fm.get("rack_face", "")
|
||||||
h = fm["u_height"]
|
if face in ZERO_U_FACES:
|
||||||
urange = f"U{u}" if h == 1 else f"U{u}–U{u + h - 1}"
|
urange = "0U"
|
||||||
lines.append(
|
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"| {urange} | {link} | {fm.get('kind', '')} | {face} "
|
||||||
f"| {fm.get('status', '')} |"
|
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("")
|
lines.append("")
|
||||||
return "\n".join(lines).rstrip() + "\n"
|
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)
|
rc = gen_rack.generate(hw, out)
|
||||||
assert rc == 1
|
assert rc == 1
|
||||||
assert not (out / "rack01.md").exists()
|
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