M17: "Bekannte Router" im Verbinden-Tab (SavedRouter/SavedRoutersStore), Standort-Freitextfeld, Scroll-Cap ab 4 Einträgen. Bugfix: Umbenennen- TextField steckte in einem sich selbst deaktivierenden Button. M18: Live-Traffic-Punkt an Interfaces (InterfaceTrafficMonitor, eigene SSH-Verbindung, monitor-traffic-Polling). Dabei zwei reale CLI-Parser-Bugs gefunden und gefixt: running/disabled-Flags werden als Buchstaben vor dem ersten Feld codiert, nicht als key=value; monitor-traffic liefert "50.7kbps" statt einer reinen Zahl. M19: Übersicht-Tab — animierte Flussrichtung auf allen Verbindungslinien (TimelineView+dashPhase), frei verschiebbare Knoten mit Live-folgenden Linien, Zurücksetzen-Button. Zusätzlich (noch nicht live getestet, nur Build+Unit-Tests grün): LAN-Port-Konflikt-Prüfung im Einrichten-Assistenten mit doppelter Sicherheitsbestätigung, "Fertig"-Button nach erfolgreichem Anwenden. 82 Tests grün. HANDOFF.md/README.md/Manual.md/CHATLOG.md aktualisiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
47 lines
2.1 KiB
Swift
47 lines
2.1 KiB
Swift
import Foundation
|
|
|
|
/// Polls live per-interface throughput so the Verbinden-Tab's interface list can show which
|
|
/// ports are actually carrying traffic right now, not just "link up" (`NetworkInterface.running`
|
|
/// stays true as soon as a link is negotiated, even sitting completely idle).
|
|
///
|
|
/// Always opens its own dedicated SSH connection, independent of whichever transport (REST or
|
|
/// SSH) the live session is actually using — same reasoning as `BackupService`/`UpdateService`:
|
|
/// `/interface monitor-traffic` is a CLI-only RouterOS command with no REST equivalent. The
|
|
/// connection is kept open across repeated `fetchTraffic` calls (one new SSH connection per
|
|
/// poll tick would be far too slow for a few-times-a-second-feeling refresh) and only reopened
|
|
/// if the credentials change or a previous attempt failed.
|
|
actor InterfaceTrafficMonitor {
|
|
private var transport: SSHTransport?
|
|
private var connectedCredentials: RouterOSCredentials?
|
|
|
|
/// Best-effort per interface — one failing/renamed-mid-session interface doesn't take down
|
|
/// the whole poll, it's just missing from the result (callers treat "no entry" as "unknown",
|
|
/// not "idle").
|
|
func fetchTraffic(interfaceNames: [String], for credentials: RouterOSCredentials) async -> [String: InterfaceTraffic] {
|
|
if connectedCredentials != credentials {
|
|
await disconnect()
|
|
}
|
|
if transport == nil {
|
|
let newTransport = SSHTransport(credentials: credentials)
|
|
guard (try? await newTransport.connect()) != nil else { return [:] }
|
|
transport = newTransport
|
|
connectedCredentials = credentials
|
|
}
|
|
guard let transport else { return [:] }
|
|
|
|
var result: [String: InterfaceTraffic] = [:]
|
|
for name in interfaceNames {
|
|
if let traffic = try? await transport.fetchInterfaceTraffic(interfaceName: name) {
|
|
result[name] = traffic
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func disconnect() async {
|
|
await transport?.disconnect()
|
|
transport = nil
|
|
connectedCredentials = nil
|
|
}
|
|
}
|