forked from kay/RouterOS
M26: SavedRouter bekommt ein optionales serialNumber-Feld. Zwei physisch verschiedene Router mit identischem Host+Benutzername (Werks-Adresse) blieben bisher ein gemeinsamer "Bekannte Router"- Eintrag; recordSuccessfulConnection matcht jetzt zusätzlich nach Seriennummer, mit sauberer Migration bestehender Einträge ohne Seriennummer. Drei neue Tests. M27, beim Live-Test von M26 gefunden: - Passwort-Anzeige-Button (Augen-Symbol) im Verbinden-Tab - Bug 35: KeychainService speicherte Passwörter nur nach "username@host" — beide Router teilten sich denselben Schlüsselbund-Eintrag trotz getrennter SavedRouter-Einträge. Fix: optionaler serialNumber-Parameter qualifiziert den Account-Key, mit Fallback auf den alten Key für bereits gespeicherte Passwörter. - Bug 36: neuer Router zeigte keine Routerboard-Infos/Seriennummer — derselbe Root Cause wie der zuvor gemeldete "Jetzt prüfen"-Fehler: /system routerboard und /system package update scheitern auf diesem Router mit "expected end of command" statt dem bisher einzig abgefangenen "bad parameter terse". SSHTransport.fetchMenuItems erkennt jetzt beide Formulierungen. Alles live bestätigt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
66 lines
3.7 KiB
Swift
66 lines
3.7 KiB
Swift
import Foundation
|
|
|
|
/// A router this app has connected to successfully before, remembered in the Verbinden-Tab for
|
|
/// quick reconnect — the app's own "known devices" list. Login credentials stay exactly where
|
|
/// they already did (the macOS Keychain, keyed by "username@host" — see `KeychainService`); this
|
|
/// only remembers which host/username pairs exist, a user-editable display name, and when it was
|
|
/// last used.
|
|
struct SavedRouter: Identifiable, Codable, Equatable {
|
|
let id: UUID
|
|
var host: String
|
|
var username: String
|
|
/// Editable bookmark name. Defaults to the router's own hardware marketing name (e.g. "hEX")
|
|
/// the first time a connection to this host/username succeeds — RouterDeviceInfo.boardName,
|
|
/// the closest thing to a "werksmäßige Bezeichnung" (factory designation) this app already
|
|
/// reads — but is never overwritten afterwards, preserving whatever the user renames it to.
|
|
var name: String
|
|
/// Free-text location/purpose (e.g. "Keller, Serverschrank" or "1. OG, Gästezimmer") — added
|
|
/// per explicit request to tell multiple saved routers apart at a glance. Always
|
|
/// user-supplied, no default; empty means "not set", never shown then.
|
|
var location: String
|
|
var lastConnectedAt: Date
|
|
/// RouterOS' own hardware serial number (`/system routerboard`'s "serial-number" field) —
|
|
/// the only thing that actually identifies *this physical device*, unlike host+username,
|
|
/// which two different routers can share (e.g. both left at MikroTik's factory default
|
|
/// 192.168.88.1/admin). Nil for a device with no physical routerboard at all (e.g. a CHR
|
|
/// virtual router) or for an entry saved before this field existed — never a placeholder
|
|
/// string, so it's never mistaken for a real match. `SavedRoutersStore.
|
|
/// recordSuccessfulConnection` uses this to tell two physically different routers apart even
|
|
/// when they briefly share the same host/username, instead of silently merging them into one
|
|
/// entry (confirmed live, 2026-09-16: connecting to a second, same-model router at the same
|
|
/// factory-default address/username just renamed the *existing* entry from the first router
|
|
/// in place, with no way to tell the two apart afterwards).
|
|
var serialNumber: String?
|
|
|
|
init(
|
|
id: UUID = UUID(), host: String, username: String, name: String, location: String = "",
|
|
lastConnectedAt: Date = Date(), serialNumber: String? = nil
|
|
) {
|
|
self.id = id
|
|
self.host = host
|
|
self.username = username
|
|
self.name = name
|
|
self.location = location
|
|
self.lastConnectedAt = lastConnectedAt
|
|
self.serialNumber = serialNumber
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id, host, username, name, location, lastConnectedAt, serialNumber
|
|
}
|
|
|
|
/// Custom decoding so an already-saved list from before `location`/`serialNumber` existed
|
|
/// keeps loading (missing key -> "" / nil) instead of the whole list silently vanishing —
|
|
/// `SavedRoutersStore.load()` treats any decode failure as "no saved routers at all".
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try container.decode(UUID.self, forKey: .id)
|
|
host = try container.decode(String.self, forKey: .host)
|
|
username = try container.decode(String.self, forKey: .username)
|
|
name = try container.decode(String.self, forKey: .name)
|
|
location = try container.decodeIfPresent(String.self, forKey: .location) ?? ""
|
|
lastConnectedAt = try container.decode(Date.self, forKey: .lastConnectedAt)
|
|
serialNumber = try container.decodeIfPresent(String.self, forKey: .serialNumber)
|
|
}
|
|
}
|