Files
RouterOS/RouterOSAssistant/Core/Models/OverviewGraph.swift
T
KayandClaude Sonnet 5 9b26677dc2 Ü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
2026-09-15 19:58:05 +02:00

128 lines
5.5 KiB
Swift

import Foundation
/// One box in the Übersicht diagram — an interface, IP address, DHCP server, firewall rule etc.
/// `kind` is a finer-grained tag than `category` (e.g. "vlan" vs "bridge" within the interface
/// category) used only to pick an icon/color, never for graph logic.
struct OverviewNode: Identifiable, Equatable {
enum Category: String, CaseIterable, Identifiable {
case interface = "Interfaces"
case ipAddress = "IP-Adressen"
case service = "Pools & DHCP"
case route = "Routen"
case firewall = "Firewall & NAT"
var id: String { rawValue }
}
/// Which live RouterOS item this node was built from, and where to write changes back to —
/// nil for nodes that don't map to exactly one editable item (e.g. an address-list node folds
/// several list entries together). Lets the Übersicht tab open the same edit form the
/// Experte-Tab uses, pre-filled, instead of duplicating its field schemas/write logic.
struct EditTarget: Equatable {
let menuPath: String
let restPath: String
/// RouterOS' own ".id" for this item (e.g. "*7") — required to `set` the right one.
let itemID: String
}
let id: String
let category: Category
let kind: String
let title: String
let subtitle: String?
/// Full raw fields, shown in the detail panel when this node is selected.
let detail: [(key: String, value: String)]
let editTarget: EditTarget?
init(
id: String, category: Category, kind: String, title: String, subtitle: String?,
detail: [(key: String, value: String)], editTarget: EditTarget? = nil
) {
self.id = id
self.category = category
self.kind = kind
self.title = title
self.subtitle = subtitle
self.detail = detail
self.editTarget = editTarget
}
static func == (lhs: OverviewNode, rhs: OverviewNode) -> Bool {
lhs.id == rhs.id && lhs.category == rhs.category && lhs.kind == rhs.kind
&& lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
&& lhs.detail.map(\.key) == rhs.detail.map(\.key) && lhs.detail.map(\.value) == rhs.detail.map(\.value)
&& lhs.editTarget == rhs.editTarget
}
}
/// What kind of dependency an edge represents — drives its color in the diagram, so different
/// relationship types (a VLAN's parent link vs. a firewall rule's interface) are visually
/// distinct at a glance, not just distinguishable by hovering.
enum OverviewEdgeKind: String, CaseIterable, Identifiable {
case vlan = "VLAN → Basis-Interface"
case bridgePort = "Bridge-Port"
case wireguardPeer = "WireGuard-Peer"
case ipAddress = "Interface → IP-Adresse"
case dhcp = "DHCP / Pool"
case route = "Route → Interface"
case firewallInterface = "Firewall/NAT → Interface"
case addressList = "Adress-Liste → Regel"
var id: String { rawValue }
}
/// One line in the diagram, always drawn from an earlier column to a later one (or within a
/// column) — direction follows the actual dependency (e.g. an interface "carries" an IP address,
/// a DHCP server "uses" a pool), not just which menu was read first.
struct OverviewEdge: Identifiable, Equatable {
var id: String { "\(from)->\(to)>\(label ?? "")" }
let from: String
let to: String
let label: String?
let kind: OverviewEdgeKind
}
struct OverviewGraph: Equatable {
var nodes: [OverviewNode] = []
var edges: [OverviewEdge] = []
/// Router areas that exist but aren't part of this topology diagram (VPN-Benutzer, WLAN-
/// Sicherheitsprofile, Queues, System, Werkzeuge — none of them wire into the interface/IP/
/// firewall dependency chain this diagram is about). Listed so nothing is silently hidden;
/// full access to all of them stays in the Experte-Tab.
static let unmappedAreas = [
"VPN: PPP-Benutzer/-Profile", "WLAN-Sicherheitsprofile", "Queues/Bandbreiten-Steuerung",
"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
}
}