diff --git a/ui/app.js b/ui/app.js
index da6459a..2bea683 100644
--- a/ui/app.js
+++ b/ui/app.js
@@ -222,12 +222,25 @@
const nodes = [];
const hubs = [];
const R = Math.min(w, h) * 0.32;
+ const subnetMeta = new Map((state.data?.subnets || []).map((s) => [s.cidr, s]));
groups.forEach((g, si) => {
const a = (si / Math.max(groups.length, 1)) * Math.PI * 2 - Math.PI / 2;
const hx = cx + Math.cos(a) * R;
const hy = cy + Math.sin(a) * R;
- hubs.push({ x: hx, y: hy, label: g.cidr, color: colorFor(state.subnetColors, g.cidr) });
const localR = 28 + Math.min(90, g.members.length * 4.5);
+ const meta = subnetMeta.get(g.cidr) || {};
+ const vlanBit = meta.vlan_id != null ? `VLAN ${meta.vlan_id}` : null;
+ const label = vlanBit ? `${vlanBit} · ${g.cidr}` : g.cidr;
+ hubs.push({
+ x: hx,
+ y: hy,
+ label,
+ cidr: g.cidr,
+ color: meta.vlan_color || colorFor(state.subnetColors, g.cidr),
+ localR,
+ angle: a,
+ memberCount: g.members.length,
+ });
g.members.forEach((n, i) => {
const la = (i / Math.max(g.members.length, 1)) * Math.PI * 2;
nodes.push({
@@ -492,8 +505,9 @@
}
} else if (mode === "clusters") {
for (const ch of state.clusterHubs || []) {
+ const ring = (ch.localR || 52) + 10;
ctx.beginPath();
- ctx.arc(ch.x, ch.y, 52, 0, Math.PI * 2);
+ ctx.arc(ch.x, ch.y, ring, 0, Math.PI * 2);
ctx.strokeStyle = (ch.color || "#00a8e8") + "33";
ctx.lineWidth = 1.2;
ctx.stroke();
@@ -503,10 +517,6 @@
ctx.globalAlpha = 0.55;
ctx.fill();
ctx.globalAlpha = 1;
- ctx.fillStyle = canvasTheme().clusterLabel;
- ctx.font = "600 9px IBM Plex Mono, monospace";
- ctx.textAlign = "center";
- ctx.fillText((ch.label || "").slice(0, 18), ch.x, ch.y - 18);
drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55);
}
} else if (mode === "lanes") {
@@ -549,6 +559,88 @@
}
}
+ function drawClusterLabelPipes(omeHub) {
+ const hubs = state.clusterHubs || [];
+ if (!hubs.length || !omeHub) return;
+ const th = canvasTheme();
+ const light = isLightTheme();
+
+ for (const ch of hubs) {
+ const label = String(ch.label || ch.cidr || "").trim();
+ if (!label) continue;
+
+ // Radial outward from OME → cluster, then past the spoke ring ("long pipe")
+ let dx = ch.x - omeHub.x;
+ let dy = ch.y - omeHub.y;
+ let len = Math.hypot(dx, dy);
+ if (len < 1) {
+ dx = 0;
+ dy = -1;
+ len = 1;
+ }
+ const ux = dx / len;
+ const uy = dy / len;
+ const ring = ch.localR || 52;
+ const pipeStart = 12;
+ const pipeEnd = ring + 36;
+ const sx = ch.x + ux * pipeStart;
+ const sy = ch.y + uy * pipeStart;
+ const ex = ch.x + ux * pipeEnd;
+ const ey = ch.y + uy * pipeEnd;
+ // Label sits a bit further out past the pipe tip
+ const lx = ch.x + ux * (pipeEnd + 14);
+ const ly = ch.y + uy * (pipeEnd + 14);
+
+ const color = ch.color || "#00a8e8";
+ const rgb = hexToRgb(color);
+
+ // Long pipe / leader
+ ctx.save();
+ ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.55)`;
+ ctx.lineWidth = 1.6;
+ ctx.setLineDash([]);
+ ctx.beginPath();
+ ctx.moveTo(sx, sy);
+ ctx.lineTo(ex, ey);
+ ctx.stroke();
+ // tip tick
+ ctx.beginPath();
+ ctx.arc(ex, ey, 2.4, 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.9)`;
+ ctx.fill();
+ ctx.restore();
+
+ // Pill label (drawn last so it sits above spokes)
+ ctx.font = "600 10px IBM Plex Mono, monospace";
+ const tw = ctx.measureText(label).width;
+ const padX = 8;
+ const padY = 5;
+ const bw = tw + padX * 2;
+ const bh = 18;
+ const bx = lx - bw / 2;
+ const by = ly - bh / 2;
+
+ ctx.beginPath();
+ const r = 6;
+ ctx.moveTo(bx + r, by);
+ ctx.arcTo(bx + bw, by, bx + bw, by + bh, r);
+ ctx.arcTo(bx + bw, by + bh, bx, by + bh, r);
+ ctx.arcTo(bx, by + bh, bx, by, r);
+ ctx.arcTo(bx, by, bx + bw, by, r);
+ ctx.closePath();
+ ctx.fillStyle = light ? "rgba(255,255,255,0.92)" : "rgba(6,14,24,0.88)";
+ ctx.fill();
+ ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.75)`;
+ ctx.lineWidth = 1.2;
+ ctx.stroke();
+
+ ctx.fillStyle = th.clusterLabel;
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(label, lx, ly);
+ }
+ }
+
function drawLinks(hub, t) {
const mode = state.layoutMode;
if (mode === "clusters") {
@@ -672,6 +764,7 @@
drawLinks(hub, t);
drawHub(hub, t);
drawNodes(t);
+ if (state.layoutMode === "clusters") drawClusterLabelPipes(hub);
ctx.restore();
diff --git a/ui/index.html b/ui/index.html
index 58bed49..88e9de7 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -633,7 +633,7 @@
-
+