/** * Which selected devices carry other selected devices' traffic (issue #135). * * Upgrading devices concurrently means several reboot at the same time. That is * fine for independent routers and dangerous for a switch the others are * reached *through*: it takes them offline mid-upgrade, and the manager loses * the path to devices it is in the middle of changing. * * Change Guard does not help here. It protects configuration — a device that * stops answering after a config change reverts itself — but a device that is * unreachable because its uplink rebooted has nothing to revert. * * ## Why this is deliberately conservative * * The obvious implementation — "flag any device another selected device links * to" — reports the entire fleet. Discovered links are bidirectional (both ends * run LLDP), and a single trunk interface frequently resolves to several * neighbours at once, which the topology view already surfaces as ambiguity. On * a real four-device fleet that approach flagged all four. * * A warning that fires on everything is worse than no warning: it trains people * to dismiss it. So two conditions must both hold: * * 1. The link is the reporting device's **root port** — its own path toward * the spanning-tree root, i.e. genuinely upstream rather than merely * adjacent. * 2. That port resolves to exactly **one** neighbour. An ambiguous port tells * us a device is upstream but not which one, and naming the wrong device is * worse than saying nothing. * * The result is a warning that stays quiet unless it is reasonably sure, and is * advisory even then — the operator may know the topology better than the * discovered links do. */ export interface TopologyEdge { from_device_id: number; to_device_id: number | null; from_interface?: string | null; /** RouterOS STP port role: 'root' faces the root bridge, 'designated' faces away. */ stp_role?: string | null; } export function findUpstreamWithinSelection( selectedIds: number[], links: TopologyEdge[] ): number[] { const selected = new Set(selectedIds); if (selected.size < 2) return []; // Group each device's links by the interface they were seen on, so a port // that resolved to several neighbours can be recognised and skipped. const byPort = new Map>(); const rootPorts = new Set(); for (const link of links) { const { from_device_id: from, to_device_id: to } = link; if (to === null || from === to) continue; if (!selected.has(from) || !selected.has(to)) continue; const port = `${from}${link.from_interface ?? ''}`; if (!byPort.has(port)) byPort.set(port, new Set()); byPort.get(port)!.add(to); if ((link.stp_role ?? '').replace(/-port$/, '') === 'root') rootPorts.add(port); } const upstream = new Set(); for (const [port, neighbours] of byPort) { if (!rootPorts.has(port)) continue; // not this device's way up if (neighbours.size !== 1) continue; // ambiguous: we don't know which upstream.add([...neighbours][0]); } // Stable order, so the same selection always reads the same way. return [...upstream].sort((a, b) => a - b); }