diff --git a/static/index.html b/static/index.html
index edc5f93..d0ce5a3 100644
--- a/static/index.html
+++ b/static/index.html
@@ -13,10 +13,6 @@
background: #111;
color: #eee;
}
- #controls {
- margin-bottom: 10px;
- }
- #stats { margin-left: 10px; }
#error { color: #f66; padding: 20px; }
#container {
@@ -103,6 +99,19 @@
color: #f99;
}
+ .node .uplink {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 9px;
+ background: #446;
+ color: #aaf;
+ padding: 1px 6px;
+ border-radius: 8px;
+ white-space: nowrap;
+ }
+
.node:hover {
filter: brightness(1.2);
}
@@ -138,10 +147,6 @@
-
- Tendrils
-
-
@@ -252,7 +257,7 @@
return null;
}
- function createNodeElement(node, switchConnection, nodeLocation) {
+ function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo) {
const div = document.createElement('div');
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
@@ -272,6 +277,13 @@
labelEl.textContent = getLabel(node);
div.appendChild(labelEl);
+ if (isSwitch(node) && uplinkInfo) {
+ const uplinkEl = document.createElement('div');
+ uplinkEl.className = 'uplink';
+ uplinkEl.textContent = uplinkInfo.localPort + ' → ' + uplinkInfo.parentName + ':' + uplinkInfo.remotePort;
+ div.appendChild(uplinkEl);
+ }
+
div.addEventListener('click', () => {
const json = JSON.stringify(node, null, 2);
navigator.clipboard.writeText(json).then(() => {
@@ -282,12 +294,12 @@
return div;
}
- function renderLocation(loc, assignedNodes, isTopLevel, switchConnections) {
+ function renderLocation(loc, assignedNodes, isTopLevel, switchConnections, switchUplinks) {
const nodes = assignedNodes.get(loc) || [];
const hasNodes = nodes.length > 0;
const childElements = loc.children
- .map(child => renderLocation(child, assignedNodes, false, switchConnections))
+ .map(child => renderLocation(child, assignedNodes, false, switchConnections, switchUplinks))
.filter(el => el !== null);
if (!hasNodes && childElements.length === 0) {
@@ -313,7 +325,8 @@
const switchRow = document.createElement('div');
switchRow.className = 'node-row';
switches.forEach(node => {
- switchRow.appendChild(createNodeElement(node, null, loc));
+ const uplink = switchUplinks.get(node.typeid);
+ switchRow.appendChild(createNodeElement(node, null, loc, uplink));
});
container.appendChild(switchRow);
}
@@ -323,7 +336,7 @@
nodeRow.className = 'node-row';
nonSwitches.forEach(node => {
const conn = switchConnections.get(node.typeid);
- nodeRow.appendChild(createNodeElement(node, conn, loc));
+ nodeRow.appendChild(createNodeElement(node, conn, loc, null));
});
container.appendChild(nodeRow);
}
@@ -351,8 +364,6 @@
const nodes = data.nodes || [];
const links = data.links || [];
- document.getElementById('stats').textContent =
- `${nodes.length} nodes, ${links.length} links`;
const locationTree = buildLocationTree(config.locations || [], null);
const nodeIndex = new Map();
@@ -381,6 +392,9 @@
});
const switchConnections = new Map();
+ const switchLinks = [];
+ const allSwitches = nodes.filter(n => isSwitch(n));
+
links.forEach(link => {
const nodeA = nodesByTypeId.get(link.node_a?.typeid);
const nodeB = nodesByTypeId.get(link.node_b?.typeid);
@@ -389,7 +403,14 @@
const aIsSwitch = isSwitch(nodeA);
const bIsSwitch = isSwitch(nodeB);
- if (aIsSwitch && !bIsSwitch) {
+ if (aIsSwitch && bIsSwitch) {
+ switchLinks.push({
+ switchA: nodeA,
+ switchB: nodeB,
+ portA: link.interface_a || '?',
+ portB: link.interface_b || '?'
+ });
+ } else if (aIsSwitch && !bIsSwitch) {
const nodeLoc = nodeLocations.get(nodeB.typeid);
const effectiveSwitch = findEffectiveSwitch(nodeLoc, assignedNodes);
switchConnections.set(nodeB.typeid, {
@@ -408,11 +429,73 @@
}
});
+ const switchUplinks = new Map();
+ if (allSwitches.length > 0 && switchLinks.length > 0) {
+ const adjacency = new Map();
+ allSwitches.forEach(sw => adjacency.set(sw.typeid, []));
+
+ switchLinks.forEach(link => {
+ adjacency.get(link.switchA.typeid).push({
+ neighbor: link.switchB,
+ localPort: link.portA,
+ remotePort: link.portB
+ });
+ adjacency.get(link.switchB.typeid).push({
+ neighbor: link.switchA,
+ localPort: link.portB,
+ remotePort: link.portA
+ });
+ });
+
+ let bestRoot = allSwitches[0];
+ let bestMaxDepth = Infinity;
+
+ allSwitches.forEach(candidate => {
+ const visited = new Set([candidate.typeid]);
+ const queue = [{ sw: candidate, depth: 0 }];
+ let maxDepth = 0;
+
+ while (queue.length > 0) {
+ const { sw, depth } = queue.shift();
+ maxDepth = Math.max(maxDepth, depth);
+ for (const edge of adjacency.get(sw.typeid) || []) {
+ if (!visited.has(edge.neighbor.typeid)) {
+ visited.add(edge.neighbor.typeid);
+ queue.push({ sw: edge.neighbor, depth: depth + 1 });
+ }
+ }
+ }
+
+ if (maxDepth < bestMaxDepth) {
+ bestMaxDepth = maxDepth;
+ bestRoot = candidate;
+ }
+ });
+
+ const visited = new Set([bestRoot.typeid]);
+ const queue = [bestRoot];
+
+ while (queue.length > 0) {
+ const current = queue.shift();
+ for (const edge of adjacency.get(current.typeid) || []) {
+ if (!visited.has(edge.neighbor.typeid)) {
+ visited.add(edge.neighbor.typeid);
+ switchUplinks.set(edge.neighbor.typeid, {
+ localPort: edge.localPort,
+ remotePort: edge.remotePort,
+ parentName: getLabel(current)
+ });
+ queue.push(edge.neighbor);
+ }
+ }
+ }
+ }
+
const container = document.getElementById('container');
container.innerHTML = '';
locationTree.forEach(loc => {
- const el = renderLocation(loc, assignedNodes, true, switchConnections);
+ const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks);
if (el) container.appendChild(el);
});
@@ -432,7 +515,8 @@
const switchRow = document.createElement('div');
switchRow.className = 'node-row';
switches.forEach(node => {
- switchRow.appendChild(createNodeElement(node, null, null));
+ const uplink = switchUplinks.get(node.typeid);
+ switchRow.appendChild(createNodeElement(node, null, null, uplink));
});
unassignedLoc.appendChild(switchRow);
}
@@ -442,7 +526,7 @@
nodeRow.className = 'node-row';
nonSwitches.forEach(node => {
const conn = switchConnections.get(node.typeid);
- nodeRow.appendChild(createNodeElement(node, conn, null));
+ nodeRow.appendChild(createNodeElement(node, conn, null, null));
});
unassignedLoc.appendChild(nodeRow);
}