Files
RouterOS/RouterOSAssistant/Core/Models/SavedRouter.swift
T
KayandClaude Sonnet 5 9de9311bee M26+M27: Seriennummer bei Bekannte Router, Schlüsselbund-Trennung, terse-Fallback verallgemeinert
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>
2026-09-16 14:02:27 +02:00

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)
}
}