diff --git a/scripts/gen_rack.py b/scripts/gen_rack.py
index ac716a8..f950472 100644
--- a/scripts/gen_rack.py
+++ b/scripts/gen_rack.py
@@ -393,6 +393,52 @@ def render_power(rack: str, items: list[dict]) -> str:
return "\n".join(lines) + "\n"
+def render_network(rack: str, items: list[dict]) -> str:
+ """Return a mermaid network-cabling flowchart, or '' if no links.
+
+ Assumes `validate_links` has already passed: every link has a non-empty
+ `local`/`peer` and an integer `peer_port`, and `peer` resolves to a real
+ hardware file. `generate` validates before any render call.
+ """
+ linked = [fm for fm in items if fm.get("links")]
+ if not linked:
+ return ""
+
+ by_host = {fm.get("hostname"): fm for fm in items}
+
+ edges: list[tuple[str, str, str, int, object]] = []
+ nodes: set[str] = set()
+ for fm in linked:
+ source = fm.get("hostname", "?")
+ nodes.add(source)
+ for link in fm["links"]:
+ peer = link["peer"]
+ nodes.add(peer)
+ edges.append(
+ (source, link["local"], peer, link["peer_port"],
+ link.get("speed_gbps"))
+ )
+ edges.sort(key=lambda e: (e[0], e[1], e[2], e[3]))
+
+ def node_label(name: str) -> str:
+ fm = by_host.get(name)
+ kind = fm.get("kind") if fm else None
+ if kind in ("switch", "patch-panel"):
+ return f"{name}
{kind}"
+ return name
+
+ lines: list[str] = ["```mermaid", "flowchart LR"]
+ for name in sorted(nodes):
+ lines.append(f' {_node_id(name)}["{node_label(name)}"]')
+ for source, local, peer, peer_port, speed in edges:
+ label = f"{local} → p{peer_port}"
+ if speed is not None:
+ label += f" · {speed}G"
+ lines.append(f" {_node_id(source)} -->|{label}| {_node_id(peer)}")
+ lines.append("```")
+ return "\n".join(lines) + "\n"
+
+
def render_page(rack: str, items: list[dict]) -> str:
items = _sorted_items(items)
lines: list[str] = []
@@ -414,6 +460,12 @@ def render_page(rack: str, items: list[dict]) -> str:
lines.append("")
lines.append(power.rstrip())
lines.append("")
+ network = render_network(rack, items)
+ if network:
+ lines.append("## Network")
+ lines.append("")
+ lines.append(network.rstrip())
+ lines.append("")
lines.append("## Occupancy")
lines.append("")
lines.append("| U | Device | Kind | Face | Status |")
diff --git a/tests/test_gen_rack.py b/tests/test_gen_rack.py
index f1a5e7e..b061f7f 100644
--- a/tests/test_gen_rack.py
+++ b/tests/test_gen_rack.py
@@ -419,3 +419,86 @@ def test_generate_returns_1_on_bad_link_peer(tmp_path):
rc = gen_rack.generate(hw, out)
assert rc == 1
assert not (out / "rack01.md").exists()
+
+
+def test_render_network_has_nodes_and_edge_labels():
+ items = [
+ item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
+ rack_face="front", ports=24),
+ item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
+ links=[{"local": "eth0", "peer": "sw01",
+ "peer_port": 1, "speed_gbps": 1}]),
+ ]
+ out = gen_rack.render_network("rack01", items)
+ assert "```mermaid" in out
+ assert "flowchart LR" in out
+ assert "sw01
switch" in out
+ assert "mf00" in out
+ assert "eth0" in out
+ assert "p1" in out
+ assert "1G" in out
+
+
+def test_render_network_patch_panel_subtitle():
+ items = [
+ item(hostname="pp01", kind="patch-panel", rack_u=24, u_height=1,
+ rack_face="front", ports=24),
+ item(hostname="mf01", rack_u=2, u_height=1, rack_face="front",
+ links=[{"local": "eth0", "peer": "pp01",
+ "peer_port": 1, "speed_gbps": 1}]),
+ ]
+ out = gen_rack.render_network("rack01", items)
+ assert "pp01
patch-panel" in out
+
+
+def test_render_network_empty_when_no_links():
+ items = [item(hostname="mf00", rack_u=1, u_height=1, rack_face="front")]
+ assert gen_rack.render_network("rack01", items) == ""
+
+
+def test_render_network_omits_speed_when_absent():
+ items = [
+ item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
+ rack_face="front", ports=24),
+ item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
+ links=[{"local": "eth0", "peer": "sw01", "peer_port": 1}]),
+ ]
+ out = gen_rack.render_network("rack01", items)
+ assert "eth0" in out and "p1" in out
+ assert "·" not in out # no speed suffix rendered
+
+
+def test_render_network_is_deterministic():
+ a = item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
+ rack_face="front", ports=24)
+ b = item(hostname="mf01", rack_u=2, u_height=1, rack_face="front",
+ links=[{"local": "eth0", "peer": "sw01",
+ "peer_port": 2, "speed_gbps": 1}])
+ c = item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
+ links=[{"local": "eth0", "peer": "sw01",
+ "peer_port": 1, "speed_gbps": 1}])
+ assert gen_rack.render_network("rack01", [a, b, c]) == \
+ gen_rack.render_network("rack01", [c, b, a])
+
+
+def test_generate_includes_network_section(tmp_path):
+ hw = tmp_path / "hardware"
+ out = tmp_path / "out"
+ hw.mkdir()
+ _write_item(
+ hw, "sw01",
+ "---\nhostname: sw01\nkind: switch\nstatus: in-use\n"
+ "rack: rack01\nrack_u: 10\nu_height: 1\nrack_face: front\nports: 24\n---\n",
+ )
+ _write_item(
+ hw, "mf00",
+ "---\nhostname: mf00\nkind: server\nstatus: in-use\n"
+ "rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n"
+ "links:\n - { local: eth0, peer: sw01, peer_port: 1, speed_gbps: 1 }\n---\n",
+ )
+ rc = gen_rack.generate(hw, out)
+ assert rc == 0
+ page = (out / "rack01.md").read_text()
+ assert "## Network" in page
+ assert "```mermaid" in page
+ assert "eth0" in page