Neuer Tab: eine Tabelle pro physischem Ethernet/WLAN-Port mit den dort gefundenen Geräten (Name, IP, MAC, Fest/Dynamisch/Kein-DHCP), gebaut aus DHCP-Leases + ARP + Bridge-Host-Tabelle. Rechtsklick auf ein dynamisches Gerät -> "Feste IP zuweisen" (RouterOS' "Make Static", per /ip dhcp-server lease make-static), mit Bestätigungsdialog und Session-Backup vor dem ersten Schreibvorgang (geteilter Mechanismus mit dem Experte-Tab). Vier reale Bugs live gefunden und gefixt (siehe HANDOFF.md Bug 14-17): - "print terse" gibt das "dynamic"-Feld von /ip dhcp-server lease nie aus, in keinem Zustand -> Status kommt jetzt über RouterOS' find/get gegen die interne Eigenschaft, nicht aus gelesenen Feldern. - fetchMenuItems' .id-Positionsüberlagerung ordnete für dieses Menü die falsche .id der falschen Zeile zu -> Erkennung und make-static-Ziel laufen jetzt über die MAC-Adresse statt .id. - Ein SwiftUI-.confirmationDialog löschte sein eigenes Ziel-Objekt vor der Ausführung der bestätigten Aktion (Setter feuert bei jedem Knopfdruck, nicht nur Abbrechen) -> Dialog-Sichtbarkeit und Nutzlast entkoppelt, wie in BackupListView. - Die eigene Verifikations-Abfrage (get [find ...] feld als ein kombinierter Befehl) war selbst eine nie verifizierte Annahme und lieferte falsche Negative -> ersetzt durch :foreach aus zwei einzeln bestätigten Bausteinen (find, get <id> feld). RouterOSCommand bekommt einen neuen .action-Operationstyp für RouterOS-"Menü-spezifische Befehle" jenseits von add/set/remove (aktuell nur make-static). HANDOFF.md/CHATLOG.md mit allen vier Bugs, neuen Milestones M11/M12 und offenen Punkten aktualisiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ
64 lines
3.5 KiB
Swift
64 lines
3.5 KiB
Swift
import Foundation
|
|
|
|
/// One device seen on the LAN — combines a DHCP lease (name/IP/MAC/static-or-not) with the
|
|
/// physical port it was learned on, resolved from ARP + the bridge host table. Built once per
|
|
/// "Geräte" tab refresh by `DevicesViewModel.buildDevices`, never mutated in place.
|
|
struct LanDevice: Identifiable, Equatable {
|
|
/// Unique per row (not the MAC alone) — RouterOS can report two lease entries for the same
|
|
/// MAC (observed live: a device converted to static that still shows a separate dynamic
|
|
/// entry too), and both need to render rather than collide under one SwiftUI List identity.
|
|
let id: String
|
|
let ipAddress: String
|
|
let macAddress: String
|
|
let hostName: String?
|
|
/// True once RouterOS' own "dynamic" flag on the lease is false/absent — a manually added or
|
|
/// "make-static"-converted lease. Always true for ARP-only entries (nothing to make static).
|
|
/// NOTE: RouterOS' official docs don't formally list "dynamic" as a lease property (only as a
|
|
/// print *flag* letter — see RouterOSCommand.Operation.action's doc comment) — if this turns
|
|
/// out wrong on real hardware, check `rawFields` (via "Rohdaten anzeigen") for the actual key.
|
|
let isStatic: Bool
|
|
/// False for a device seen only via ARP, with no matching DHCP lease — RouterOS has no lease
|
|
/// item to convert for these, so the Geräte tab shows them read-only.
|
|
let hasLease: Bool
|
|
/// The lease's RouterOS `.id`, needed for the "make-static" command. Nil when `hasLease` is
|
|
/// false.
|
|
let leaseID: String?
|
|
let dhcpServerName: String?
|
|
/// The physical port this device was learned on (e.g. "ether3") — set only when resolved via
|
|
/// the bridge host table or a non-bridge ARP interface, i.e. an actual port, never a guess.
|
|
let resolvedPort: String?
|
|
/// Set instead of `resolvedPort` when the device is only known to be somewhere behind this
|
|
/// bridge/network, not which physical port — kept distinct so the UI never overstates
|
|
/// precision it doesn't have.
|
|
let networkHint: String?
|
|
let comment: String?
|
|
/// Every field RouterOS returned for this item, unfiltered — shown via "Rohdaten anzeigen"
|
|
/// so a wrong assumption above (e.g. about the "dynamic" field) is one right-click away from
|
|
/// being checked against the real device instead of guessed again.
|
|
let rawFields: [(key: String, value: String)]
|
|
|
|
var isExactPort: Bool { resolvedPort != nil }
|
|
|
|
var displayPort: String {
|
|
if let resolvedPort { return "Port: \(resolvedPort)" }
|
|
if let networkHint { return "Netz: \(networkHint)" }
|
|
return "unbekannt"
|
|
}
|
|
|
|
static func == (lhs: LanDevice, rhs: LanDevice) -> Bool {
|
|
lhs.id == rhs.id && lhs.ipAddress == rhs.ipAddress && lhs.macAddress == rhs.macAddress
|
|
&& lhs.hostName == rhs.hostName && lhs.isStatic == rhs.isStatic && lhs.hasLease == rhs.hasLease
|
|
&& lhs.leaseID == rhs.leaseID && lhs.dhcpServerName == rhs.dhcpServerName
|
|
&& lhs.resolvedPort == rhs.resolvedPort && lhs.networkHint == rhs.networkHint && lhs.comment == rhs.comment
|
|
&& lhs.rawFields.map(\.key) == rhs.rawFields.map(\.key) && lhs.rawFields.map(\.value) == rhs.rawFields.map(\.value)
|
|
}
|
|
}
|
|
|
|
/// One physical Ethernet/WLAN port with the devices resolved onto it — the "Geräte" tab's
|
|
/// per-port table. Includes ports with zero devices, so unused ports are visible too.
|
|
struct DevicePortGroup: Identifiable {
|
|
let id: String
|
|
let title: String
|
|
let devices: [LanDevice]
|
|
}
|