diff --git a/link.go b/link.go index 5951506..ef5679c 100644 --- a/link.go +++ b/link.go @@ -76,22 +76,20 @@ func (n *Nodes) getDirectLinks() []*Link { } seenMACs[mac] = true - lastHop, lastPort := n.findLastHop(target, mac, macToNode) - if lastHop == nil { - continue - } - - targetIface := n.findTargetInterface(target, lastHop, macToNode) - key := makeLinkKey(lastHop, lastPort, target, targetIface) - if !seen[key] { - seen[key] = true - links = append(links, &Link{ - TypeID: newTypeID("link"), - NodeA: lastHop, - InterfaceA: lastPort, - NodeB: target, - InterfaceB: targetIface, - }) + lastHops := n.findAllLastHops(target, mac, macToNode) + for _, lh := range lastHops { + targetIface := n.findTargetInterface(target, lh.node, macToNode) + key := makeLinkKey(lh.node, lh.port, target, targetIface) + if !seen[key] { + seen[key] = true + links = append(links, &Link{ + TypeID: newTypeID("link"), + NodeA: lh.node, + InterfaceA: lh.port, + NodeB: target, + InterfaceB: targetIface, + }) + } } } } @@ -99,7 +97,15 @@ func (n *Nodes) getDirectLinks() []*Link { return links } -func (n *Nodes) findLastHop(target *Node, mac string, macToNode map[string]*Node) (*Node, string) { +func (n *Nodes) findAllLastHops(target *Node, mac string, macToNode map[string]*Node) []struct { + node *Node + port string +} { + var results []struct { + node *Node + port string + } + for _, node := range n.nodes { port, sees := node.MACTable[mac] if !sees || node == target { @@ -107,10 +113,13 @@ func (n *Nodes) findLastHop(target *Node, mac string, macToNode map[string]*Node } if !n.hasCloserNode(node, target, mac, port, macToNode) { - return node, port + results = append(results, struct { + node *Node + port string + }{node, port}) } } - return nil, "" + return results } func (n *Nodes) hasCloserNode(node, target *Node, mac, port string, macToNode map[string]*Node) bool { diff --git a/static/index.html b/static/index.html index d0ce5a3..2ed8084 100644 --- a/static/index.html +++ b/static/index.html @@ -112,6 +112,19 @@ white-space: nowrap; } + .node .root-label { + position: absolute; + top: -8px; + left: 50%; + transform: translateX(-50%); + font-size: 9px; + background: #664; + color: #ffa; + padding: 1px 6px; + border-radius: 8px; + white-space: nowrap; + } + .node:hover { filter: brightness(1.2); } @@ -277,7 +290,12 @@ labelEl.textContent = getLabel(node); div.appendChild(labelEl); - if (isSwitch(node) && uplinkInfo) { + if (isSwitch(node) && uplinkInfo === 'ROOT') { + const rootEl = document.createElement('div'); + rootEl.className = 'root-label'; + rootEl.textContent = 'ROOT'; + div.appendChild(rootEl); + } else if (isSwitch(node) && uplinkInfo) { const uplinkEl = document.createElement('div'); uplinkEl.className = 'uplink'; uplinkEl.textContent = uplinkInfo.localPort + ' → ' + uplinkInfo.parentName + ':' + uplinkInfo.remotePort; @@ -447,10 +465,18 @@ }); }); - let bestRoot = allSwitches[0]; + for (const edges of adjacency.values()) { + edges.sort((a, b) => getLabel(a.neighbor).localeCompare(getLabel(b.neighbor))); + } + + const sortedSwitches = [...allSwitches].sort((a, b) => + getLabel(a).localeCompare(getLabel(b))); + + let bestRoot = sortedSwitches[0]; + let bestReachable = 0; let bestMaxDepth = Infinity; - allSwitches.forEach(candidate => { + for (const candidate of sortedSwitches) { const visited = new Set([candidate.typeid]); const queue = [{ sw: candidate, depth: 0 }]; let maxDepth = 0; @@ -466,11 +492,16 @@ } } - if (maxDepth < bestMaxDepth) { + const reachable = visited.size; + if (reachable > bestReachable || + (reachable === bestReachable && maxDepth < bestMaxDepth)) { + bestReachable = reachable; bestMaxDepth = maxDepth; bestRoot = candidate; } - }); + } + + switchUplinks.set(bestRoot.typeid, 'ROOT'); const visited = new Set([bestRoot.typeid]); const queue = [bestRoot]; @@ -489,6 +520,7 @@ } } } + } const container = document.getElementById('container');