forked from kay/RouterOS
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
82 lines
3.4 KiB
Swift
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)
|
|
}
|
|
}
|