forked from kay/RouterOS
Übersicht: Interface-Klick hebt transitiv alle zusammenhängenden Linien hervor
OverviewGraph.highlightedNodeIDs(startingAt:) macht für Interface-Knoten eine BFS über alle Kanten statt nur 1-Hop-Matching, damit z.B. "DHCP-Server -> Pool" oder "IP-Adresse -> DHCP-Netzwerk" mit sichtbar werden. Andere Knotentypen bleiben unverändert bei 1-Hop. Logik isoliert unit-getestet (GUI selbst nicht automatisiert klickbar). 61 Tests grün. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
This commit is contained in:
+16
@@ -873,3 +873,19 @@ Test, WLAN/Bonding/PPPoE-Live-Tests).
|
||||
Backend-/Laufzeit-Text von RouterOS oder app-eigenen Fehlerpfaden
|
||||
(technische/englische Ausgaben, keine kuratierten UI-Strings).
|
||||
HANDOFF.md/README.md/Manual.md aktualisiert, Commit + Push.
|
||||
- "nochmal zum Tab Übersicht, nur eine Idee: wenn ich einen Interface-
|
||||
Button klicke, möchte ich ALLE Linien sehen..." → `OverviewGraph.
|
||||
highlightedNodeIDs(startingAt:)` ergänzt: für Interface-Knoten eine
|
||||
BFS über alle Kanten (beide Richtungen), für jeden anderen Knotentyp
|
||||
unverändert nur 1-Hop. "teste die Interface-Verbindung im Übersicht-
|
||||
Tab" → da die laufende GUI nicht selbst klickbar ist, die BFS-Logik in
|
||||
`OverviewGraphTests.swift` isoliert getestet (Interface→Adresse→DHCP-
|
||||
Netzwerk und Interface→DHCP-Server→Pool, inkl. Sanity-Check, dass Pool/
|
||||
DHCP-Netzwerk wirklich zwei Hops entfernt liegen). 61 Tests grün.
|
||||
"ich räume den Router mal auf - werksreset" / "ist durch, funktioniert"
|
||||
→ HANDOFF.md aktualisiert: Router am 2026-09-15 erneut auf
|
||||
Werkseinstellungen, alle alten Config-Stände hinfällig. "alles
|
||||
speichern bitte" → HANDOFF.md/CHATLOG.md aktualisiert, Commit + Push —
|
||||
visuelle Live-Prüfung des Diagramms selbst steht noch aus (nur die
|
||||
Logik ist getestet), auf ausdrücklichen Nutzerwunsch trotzdem
|
||||
gespeichert.
|
||||
|
||||
+29
@@ -1160,6 +1160,35 @@ Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und
|
||||
Knoten zusammen, kein 1:1-Bezug zu einem Router-Item, bräuchte eine
|
||||
andere UI (Liste statt Formular).
|
||||
|
||||
12. **Übersicht-Tab: Interface-Klick hebt jetzt transitiv alle
|
||||
zusammenhängenden Linien hervor, nicht nur die direkt anliegenden**
|
||||
— Nutzeridee: "wenn ich einen Interface-Button klicke, möchte ich
|
||||
ALLE Linien sehen, die mit dem Interface irgendwie zusammenhängen".
|
||||
Bisher galt für jeden Knotentyp dieselbe 1-Hop-Regel (nur Kanten, die
|
||||
den geklickten Knoten selbst berühren) — bei einem Interface reißt
|
||||
das z.B. die Kette "DHCP-Server → sein Pool" oder "IP-Adresse →
|
||||
DHCP-Netzwerk-Optionen" ab, da diese Kanten nicht am Interface selbst
|
||||
hängen. Fix: `OverviewGraph.highlightedNodeIDs(startingAt:)` — für
|
||||
jeden Knotentyp außer Interface unverändert nur der eine Knoten
|
||||
selbst, für ein Interface eine Breitensuche (BFS) über alle Kanten in
|
||||
beide Richtungen, die den kompletten zusammenhängenden Ast einsammelt
|
||||
(Interface → Adresse/DHCP-Server/Route/Firewall-Regel, und von dort
|
||||
weiter → Pool bzw. DHCP-Netzwerk). Ausdrücklich nur für Interfaces so
|
||||
gewollt — alle anderen Knotentypen bleiben bei der bisherigen 1-Hop-
|
||||
Regel. Da ich die laufende App-GUI nicht selbst anklicken kann, die
|
||||
BFS-Logik stattdessen isoliert in `OverviewGraphTests.swift` getestet
|
||||
(`testHighlightedNodeIDsExpandsTransitivelyOnlyForInterfaces`):
|
||||
baut einen Graphen mit Interface→Adresse→DHCP-Netzwerk und
|
||||
Interface→DHCP-Server→Pool auf, bestätigt zunächst per Sanity-Check,
|
||||
dass Pool und DHCP-Netzwerk tatsächlich zwei Hops entfernt sind (kein
|
||||
direkter Edge zum Interface), und prüft dann, dass
|
||||
`highlightedNodeIDs(startingAt:)` vom Interface aus alle fünf Knoten
|
||||
liefert, aber vom Pool oder von der Adresse aus nur sich selbst. 61
|
||||
Tests grün (60 + 1 neu). **Visuelle Live-Prüfung im laufenden
|
||||
Diagramm steht noch aus** — vom Nutzer noch nicht bestätigt, nur die
|
||||
Logik selbst ist getestet; auf Nutzerwunsch trotzdem bereits
|
||||
committed+gepusht ("alles speichern bitte").
|
||||
|
||||
Gitea-Remote `origin` ist eingerichtet und wird laufend gepusht (siehe
|
||||
oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15.
|
||||
|
||||
|
||||
@@ -95,4 +95,33 @@ struct OverviewGraph: Equatable {
|
||||
"System (Name/Uhrzeit/Scheduler/Skripte/Benutzerkonten)", "Werkzeuge (Netwatch/E-Mail)",
|
||||
"Firewall: Mangle- und Raw-Regeln"
|
||||
]
|
||||
|
||||
/// Node IDs whose edges the Übersicht diagram should draw at full strength when `nodeID` is
|
||||
/// selected/hovered. Normally just `nodeID` itself (one-hop matching against `edges`) — but
|
||||
/// for an Interface node specifically, the user asked to see *every* line that traces back to
|
||||
/// it, not just the ones directly touching it (e.g. a DHCP server's own pool, or an IP
|
||||
/// address's DHCP-network options, sit a second hop away from the interface itself). Confirmed
|
||||
/// scope (2026-09-15): only interfaces expand this way — every other node kind keeps the
|
||||
/// original one-hop highlight.
|
||||
func highlightedNodeIDs(startingAt nodeID: String) -> Set<String> {
|
||||
guard let node = nodes.first(where: { $0.id == nodeID }), node.category == .interface else {
|
||||
return [nodeID]
|
||||
}
|
||||
var visited: Set<String> = [nodeID]
|
||||
var frontier = [nodeID]
|
||||
while !frontier.isEmpty {
|
||||
var next: [String] = []
|
||||
for current in frontier {
|
||||
for edge in edges {
|
||||
if edge.from == current, visited.insert(edge.to).inserted {
|
||||
next.append(edge.to)
|
||||
} else if edge.to == current, visited.insert(edge.from).inserted {
|
||||
next.append(edge.from)
|
||||
}
|
||||
}
|
||||
}
|
||||
frontier = next
|
||||
}
|
||||
return visited
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ struct OverviewView: View {
|
||||
/// wins" order made the highlight jump to whatever card the cursor grazed en route.
|
||||
private var highlightNodeID: String? { viewModel.selectedNodeID ?? hoveredNodeID }
|
||||
|
||||
/// Node IDs whose edges should draw at full strength — see `OverviewGraph.highlightedNodeIDs`
|
||||
/// for the one-hop-vs-interface-transitive-closure logic itself.
|
||||
private var highlightedNodeIDs: Set<String> {
|
||||
guard let id = highlightNodeID else { return [] }
|
||||
return viewModel.graph.highlightedNodeIDs(startingAt: id)
|
||||
}
|
||||
|
||||
init(connectionService: ConnectionService) {
|
||||
self.connectionService = connectionService
|
||||
_viewModel = StateObject(wrappedValue: OverviewViewModel(connectionService: connectionService))
|
||||
@@ -143,7 +150,7 @@ struct OverviewView: View {
|
||||
EdgesCanvas(
|
||||
graph: viewModel.graph,
|
||||
positions: layout.positions,
|
||||
highlightNodeID: highlightNodeID,
|
||||
highlightedNodeIDs: highlightedNodeIDs,
|
||||
hoveredEdge: $hoveredEdge,
|
||||
hoverPoint: $hoverPoint,
|
||||
selectedEdge: $selectedEdge
|
||||
@@ -223,7 +230,7 @@ struct OverviewView: View {
|
||||
private struct EdgesCanvas: View {
|
||||
let graph: OverviewGraph
|
||||
let positions: [String: CGPoint]
|
||||
let highlightNodeID: String?
|
||||
let highlightedNodeIDs: Set<String>
|
||||
@Binding var hoveredEdge: OverviewEdge?
|
||||
@Binding var hoverPoint: CGPoint
|
||||
@Binding var selectedEdge: OverviewEdge?
|
||||
@@ -296,8 +303,8 @@ private struct EdgesCanvas: View {
|
||||
Canvas { context, _ in
|
||||
for geometry in geometries {
|
||||
let edge = geometry.edge
|
||||
let isConnected = highlightNodeID != nil && (edge.from == highlightNodeID || edge.to == highlightNodeID)
|
||||
let isDimmed = highlightNodeID != nil && !isConnected
|
||||
let isConnected = highlightedNodeIDs.contains(edge.from) || highlightedNodeIDs.contains(edge.to)
|
||||
let isDimmed = !highlightedNodeIDs.isEmpty && !isConnected
|
||||
let isHovered = hoveredEdge?.id == edge.id || selectedEdge?.id == edge.id
|
||||
var path = Path()
|
||||
path.move(to: geometry.start)
|
||||
@@ -335,8 +342,8 @@ private struct EdgesCanvas: View {
|
||||
/// hovering feel like it kept jumping to the wrong line. Once the user has already narrowed
|
||||
/// things down to one node's connections, hover only needs to disambiguate between those.
|
||||
private var candidateGeometries: [EdgeGeometry] {
|
||||
guard let highlightNodeID else { return geometries }
|
||||
return geometries.filter { $0.edge.from == highlightNodeID || $0.edge.to == highlightNodeID }
|
||||
guard !highlightedNodeIDs.isEmpty else { return geometries }
|
||||
return geometries.filter { highlightedNodeIDs.contains($0.edge.from) || highlightedNodeIDs.contains($0.edge.to) }
|
||||
}
|
||||
|
||||
/// Several edges often fan out from the exact same point (e.g. every edge leaving one
|
||||
|
||||
@@ -173,4 +173,40 @@ final class OverviewGraphTests: XCTestCase {
|
||||
let route = graph.nodes.first { $0.id == "route:0" }
|
||||
XCTAssertNil(route?.editTarget)
|
||||
}
|
||||
|
||||
/// Clicking an Interface should highlight every line that traces back to it, not just the
|
||||
/// ones directly touching it — e.g. a DHCP server's own pool, or an IP address's DHCP-network
|
||||
/// options, sit a second hop away from the interface itself. Confirmed scope (2026-09-15):
|
||||
/// only interfaces expand this way — every other node kind stays one-hop-only.
|
||||
func testHighlightedNodeIDsExpandsTransitivelyOnlyForInterfaces() {
|
||||
let graph = OverviewViewModel.buildGraph(
|
||||
interfaces: [item(["name": "ether1", "type": "ether"])],
|
||||
bridgePorts: [], vlans: [], wireguardPeers: [],
|
||||
addresses: [item(["address": "192.168.88.1/24", "interface": "ether1", "network": "192.168.88.0"])],
|
||||
pools: [item(["name": "pool1", "ranges": "192.168.88.10-192.168.88.254"])],
|
||||
dhcpServers: [item(["name": "dhcp1", "interface": "ether1", "address-pool": "pool1"])],
|
||||
dhcpNetworks: [item(["address": "192.168.88.0/24", "gateway": "192.168.88.1"])],
|
||||
dhcpClients: [], routes: [], filterRules: [], natRules: [], addressLists: []
|
||||
)
|
||||
|
||||
let interfaceID = "Interfaces:ether1"
|
||||
let addressID = "IP-Adressen:192.168.88.1/24"
|
||||
let dhcpServerID = "Pools & DHCP:dhcp:dhcp1"
|
||||
let poolID = "Pools & DHCP:pool:pool1"
|
||||
let dhcpNetworkID = "Pools & DHCP:dhcpnet:192.168.88.0/24"
|
||||
|
||||
// Sanity: the pool and the DHCP-network options really are two hops away from the
|
||||
// interface (no direct edge to either) — otherwise this test wouldn't distinguish
|
||||
// transitive-closure behavior from plain one-hop matching.
|
||||
XCTAssertFalse(graph.edges.contains { $0.from == interfaceID && $0.to == poolID })
|
||||
XCTAssertFalse(graph.edges.contains { $0.from == interfaceID && $0.to == dhcpNetworkID })
|
||||
|
||||
let fromInterface = graph.highlightedNodeIDs(startingAt: interfaceID)
|
||||
XCTAssertEqual(fromInterface, [interfaceID, addressID, dhcpServerID, poolID, dhcpNetworkID])
|
||||
|
||||
// A non-interface node (e.g. the pool) stays one-hop-only: just itself, regardless of
|
||||
// what it's connected to.
|
||||
XCTAssertEqual(graph.highlightedNodeIDs(startingAt: poolID), [poolID])
|
||||
XCTAssertEqual(graph.highlightedNodeIDs(startingAt: addressID), [addressID])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user