Add TP-Link AP support with wireless client sub-locations

- Add NodeType enum (switch, ap, wireless_client, wired_client)
- Poll SNMPv2c and SNMPv3 in parallel to win race with ping
- Render APs with bordered sub-locations containing wireless clients
- Fall back to parent interface stats when child lacks them
- Log when unreachable nodes become reachable via merge

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-02-02 20:33:42 -08:00
parent bd829eb888
commit 92ab5d8a6e
11 changed files with 261 additions and 23 deletions

View File

@@ -1,4 +1,4 @@
import { getLabel, getShortLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getInterfaceUptime, getInterfaceLastError, getFirstName } from './nodes.js';
import { getLabel, getShortLabel, isSwitch, isAP, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getInterfaceUptime, getInterfaceLastError, getFirstName } from './nodes.js';
import { buildSwitchUplinks, buildLocationTree, buildNodeIndex, findLocationForNode, findEffectiveSwitch } from './topology.js';
import { formatUniverse } from './format.js';
import { createNodeElement, renderLocation } from './components.js';
@@ -48,17 +48,74 @@ export function render(data, config) {
}
});
const apClients = new Map();
links.forEach(link => {
const nodeA = nodesByTypeId.get(link.node_a_id);
const nodeB = nodesByTypeId.get(link.node_b_id);
if (!nodeA || !nodeB) return;
if (isAP(nodeA) && link.interface_a === 'wifi' && !isSwitch(nodeB) && !isAP(nodeB)) {
if (!apClients.has(nodeA)) apClients.set(nodeA, []);
apClients.get(nodeA).push(nodeB);
} else if (isAP(nodeB) && link.interface_b === 'wifi' && !isSwitch(nodeA) && !isAP(nodeA)) {
if (!apClients.has(nodeB)) apClients.set(nodeB, []);
apClients.get(nodeB).push(nodeA);
}
});
apClients.forEach((clients, ap) => {
const apLoc = nodeLocations.get(ap.id);
const apSubLoc = {
id: 'ap_' + ap.id,
name: '',
anonymous: true,
isAPLocation: true,
direction: 'horizontal',
nodeRefs: [],
parent: apLoc || null,
children: []
};
if (apLoc) {
const apLocNodes = assignedNodes.get(apLoc) || [];
const idx = apLocNodes.indexOf(ap);
if (idx !== -1) apLocNodes.splice(idx, 1);
apLoc.children.push(apSubLoc);
} else {
const idx = unassignedNodes.indexOf(ap);
if (idx !== -1) unassignedNodes.splice(idx, 1);
locationTree.push(apSubLoc);
}
assignedNodes.set(apSubLoc, [ap]);
nodeLocations.set(ap.id, apSubLoc);
clients.forEach(client => {
const clientLoc = nodeLocations.get(client.id);
if (clientLoc && clientLoc !== apSubLoc) {
const clientLocNodes = assignedNodes.get(clientLoc) || [];
const idx = clientLocNodes.indexOf(client);
if (idx !== -1) clientLocNodes.splice(idx, 1);
} else {
const idx = unassignedNodes.indexOf(client);
if (idx !== -1) unassignedNodes.splice(idx, 1);
}
assignedNodes.get(apSubLoc).push(client);
nodeLocations.set(client.id, apSubLoc);
});
});
const switchConnections = new Map();
const switchLinks = [];
const allSwitches = nodes.filter(n => isSwitch(n));
const allSwitches = nodes.filter(n => isSwitch(n) || isAP(n));
links.forEach(link => {
const nodeA = nodesByTypeId.get(link.node_a_id);
const nodeB = nodesByTypeId.get(link.node_b_id);
if (!nodeA || !nodeB) return;
const aIsSwitch = isSwitch(nodeA);
const bIsSwitch = isSwitch(nodeB);
const aIsSwitch = isSwitch(nodeA) || isAP(nodeA);
const bIsSwitch = isSwitch(nodeB) || isAP(nodeB);
if (aIsSwitch && bIsSwitch) {
switchLinks.push({