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
128 lines
5.5 KiB
Swift
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
|
|
}
|
|
}
|