Files
RouterOS/RouterOSAssistant/Core/Models/LanDevice.swift
T
KayandClaude Sonnet 5 e2b303963a M12: Geräte-Tab — LAN-Scanner mit Static-IP-Zuweisung
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
2026-09-14 13:55:46 +02:00

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