diff --git a/scripts/gen_rack.py b/scripts/gen_rack.py
index 1b01fc7..a9d592a 100644
--- a/scripts/gen_rack.py
+++ b/scripts/gen_rack.py
@@ -115,3 +115,123 @@ def check_overlaps(items: list[dict]) -> None:
f"U{uu} {f}: {name} overlaps {occupied[key]}"
)
occupied[key] = name
+
+
+def _esc(s: object) -> str:
+ return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
+
+
+def _sorted_items(items: list[dict]) -> list[dict]:
+ """Deterministic order: faced items by U then hostname, 0U items last."""
+ return sorted(
+ items,
+ key=lambda i: (
+ 0 if i.get("rack_face") not in ZERO_U_FACES else 1,
+ i.get("rack_u", 0) if isinstance(i.get("rack_u"), int) else 0,
+ i.get("hostname", ""),
+ ),
+ )
+
+
+def render_svg(rack: str, items: list[dict]) -> str:
+ U_H = 20
+ COL_W = 240
+ LABEL_W = 30
+ RAIL_W = 16
+ PAD = 12
+ GAP = 50
+ TITLE_H = 28
+
+ items = _sorted_items(items)
+ left_items = [i for i in items if i.get("rack_face") == "left"]
+ right_items = [i for i in items if i.get("rack_face") == "right"]
+
+ body_h = RACK_UNITS * U_H
+ height = PAD + TITLE_H + body_h + PAD
+ front_x = PAD + len(left_items) * RAIL_W + LABEL_W
+ rear_x = front_x + COL_W + GAP
+ width = rear_x + COL_W + len(right_items) * RAIL_W + PAD
+ top = PAD + TITLE_H
+
+ def u_y(u: int) -> int:
+ # U1 at the top; U numbers increase downward.
+ return top + (u - 1) * U_H
+
+ p: list[str] = []
+ p.append(
+ f'")
+ return "\n".join(p) + "\n"
diff --git a/tests/test_gen_rack.py b/tests/test_gen_rack.py
index 3829512..4c073f1 100644
--- a/tests/test_gen_rack.py
+++ b/tests/test_gen_rack.py
@@ -74,3 +74,28 @@ def test_overlaps_ignores_zero_u_rails():
item(hostname="p2", rack_face="left"),
]
gen_rack.check_overlaps(items) # no raise
+
+
+def test_render_svg_has_two_columns_of_48_slots():
+ svg = gen_rack.render_svg("rack01", [])
+ # one faint slot rect per U per column (front + rear)
+ assert svg.count('fill="#f5f5f5"') == 2 * gen_rack.RACK_UNITS
+ assert svg.startswith("