Files
RouterOS/RouterOSAssistant/Core/Services/SavedRoutersStore.swift
T
KayandClaude Sonnet 5 31607b7270 M17-M19: Bekannte Router, Live-Traffic-Anzeige, Übersicht-Animation+Drag
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
2026-09-15 21:40:46 +02:00

82 lines
3.4 KiB
Swift

import Foundation
/// Persists the Verbinden-Tab's "known routers" list (host/username/editable name/last-connected)
/// as JSON in UserDefaults — small, structured data, no reason for its own file the way backups
/// get a directory. Passwords themselves stay exactly where they already did: the macOS Keychain
/// via `KeychainService`, keyed by "username@host", untouched by this store.
struct SavedRoutersStore {
private static let key = "RouterOSAssistant.SavedRouters"
private let defaults: UserDefaults
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}
/// Most recently used first.
func load() -> [SavedRouter] {
sortedByRecency(loadRaw())
}
private func loadRaw() -> [SavedRouter] {
guard let data = defaults.data(forKey: Self.key),
let routers = try? JSONDecoder().decode([SavedRouter].self, from: data) else { return [] }
return routers
}
private func sortedByRecency(_ routers: [SavedRouter]) -> [SavedRouter] {
routers.sorted { $0.lastConnectedAt > $1.lastConnectedAt }
}
/// Persists in whatever order given, then returns the recency-sorted view — every public
/// method's return value has the same "most recently used first" order as `load()`, so a
/// caller can always assign it straight to a displayed list without a separate re-sort.
@discardableResult
private func save(_ routers: [SavedRouter]) -> [SavedRouter] {
if let data = try? JSONEncoder().encode(routers) {
defaults.set(data, forKey: Self.key)
}
return sortedByRecency(routers)
}
/// Called after a successful connection: adds a new entry (name defaulting to `defaultName`)
/// if this host/username pair isn't known yet, or just bumps `lastConnectedAt` on the existing
/// one. An existing entry's `name` is never touched here — that's what preserves a user's own
/// rename across later reconnects.
@discardableResult
func recordSuccessfulConnection(host: String, username: String, defaultName: String) -> [SavedRouter] {
var routers = loadRaw()
if let index = routers.firstIndex(where: { $0.host == host && $0.username == username }) {
routers[index].lastConnectedAt = Date()
} else {
routers.append(SavedRouter(host: host, username: username, name: defaultName))
}
return save(routers)
}
@discardableResult
func rename(_ id: SavedRouter.ID, to newName: String) -> [SavedRouter] {
var routers = loadRaw()
guard let index = routers.firstIndex(where: { $0.id == id }) else { return sortedByRecency(routers) }
routers[index].name = newName
return save(routers)
}
/// Free-text location/purpose (e.g. "Keller" or "1. OG") — kept as its own method rather than
/// folded into `rename` since a caller may want to update just one of the two, and it mirrors
/// `rename`'s exact shape.
@discardableResult
func updateLocation(_ id: SavedRouter.ID, to newLocation: String) -> [SavedRouter] {
var routers = loadRaw()
guard let index = routers.firstIndex(where: { $0.id == id }) else { return sortedByRecency(routers) }
routers[index].location = newLocation
return save(routers)
}
@discardableResult
func remove(_ id: SavedRouter.ID) -> [SavedRouter] {
var routers = loadRaw()
routers.removeAll { $0.id == id }
return save(routers)
}
}