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
This commit is contained in:
Kay
2026-09-14 13:55:46 +02:00
co-authored by Claude Sonnet 5
parent 10f30a8a7f
commit e2b303963a
14 changed files with 1155 additions and 27 deletions
+152 -26
View File
@@ -6,25 +6,28 @@ teilweise gegen zwei echte physische Mikrotik-Testgeräte verifiziert
kritischer Parser-Bug gefunden und gefixt, siehe unten). Danach noch
mehrere Nutzer-Wünsche umgesetzt: Tooltips, wählbarer Backup-Ordner,
Schnell-Backup im Verbinden-Tab, "Werkseinstellungen wiederherstellen",
eigenes App-Icon.
eigenes App-Icon. Anschließend M9 (Einfach/Experte-Modusschalter im
Wizard) und M10 (Experte-Tab: kuratiertes Formular für einen großen Teil
von RouterOS plus generischer "eigener Menüpfad"-Zugriff für den Rest) —
beide live gegen den hEX-Testrouter verifiziert, sieben Bugs dabei
gefunden und gefixt (Bug 713). Alles bis hierhin committed
(`c9ecd3a`).
Neu in dieser Session: Einrichten-Wizard hat jetzt einen Einfach/Experte-
Modusschalter (M9), und ein komplett neuer "Experte"-Tab (M10) gibt
Formularzugriff auf einen großen, aber bewusst kuratierten Ausschnitt von
RouterOS (Firewall/NAT/Mangle/Raw/Adress-Listen mit vollen Tooltips,
plus Interfaces/IP/VPN/WLAN/Queues/System/Werkzeuge, plus ein "eigener
Menüpfad"-Feld für alles, was nicht kuratiert ist). Live gegen den
hEX-Testrouter (RouterOS 7.24.2) verifiziert — erst per direktem SSH,
dann zusätzlich vom Nutzer selbst in der laufenden App (VLAN anlegen,
DHCP-Server mit Adress-Pool zuweisen, löschen) — dabei **sieben** echte
Bugs gefunden und gefixt (Bug 713 unten, nach jedem Fund Release neu
gebaut und deployt). **Bug 10 ist der wichtigste Fund dieser Session:**
RouterOS' SSH-CLI gibt bei fehlgeschlagenen Befehlen trotzdem Exit-Code 0
zurück — betraf `apply()` app-weit, nicht nur den Experte-Tab (siehe
unten). Release-Build unter `/Applications/RouterOS Assistant.app` ist
auf aktuellem Arbeitsstand (alle M9/M10-Änderungen + alle Bugfixes
deployt und vom Nutzer bestätigt) — **noch nicht committed** (siehe
`git status`), letzter Commit ist weiterhin `97fc216`.
**Neu in dieser Session:** zwei weitere Tabs. **M11 "Übersicht"** zeigt
die komplette aktuelle Router-Konfiguration als Diagramm (Interfaces →
IP-Adressen → DHCP/Pools → Routen → Firewall/NAT, mit echten, aus
RouterOS' eigenen Referenzfeldern abgeleiteten Verbindungslinien,
farblich nach Verbindungsart getrennt, Hover/Klick hebt Zusammenhänge
hervor). **M12 "Geräte"** ist ein LAN-Scanner (DHCP-Leases + ARP +
Bridge-Host-Tabelle, gruppiert als eine Tabelle pro physischem Port) mit
Rechtsklick-Aktion "Feste IP zuweisen" (RouterOS' "Make Static", per
`/ip dhcp-server lease make-static`). Beide Tabs live gegen den
hEX-Testrouter verifiziert — M12 dabei besonders gründlich, weil die
Fest/Dynamisch-Erkennung und die Zuweisung selbst sich als überraschend
zäh erwiesen: **vier** echte Bugs gefunden und gefixt (Bug 1417 unten),
am Ende vom Nutzer selbst bestätigt ("das funktioniert jetzt super").
Release-Build unter `/Applications/RouterOS Assistant.app` ist auf
aktuellem Stand.
## Ziel
@@ -81,10 +84,12 @@ RouterOSAssistant/
RouterOSMenuItem.swift — generisches Item (id + [String:String] fields) für JEDEN RouterOS-Menüpfad (M10)
RouterOSSchema.swift — RouterOSFieldSchema/RouterOSMenuSchema/RouterOSMenuCategory: kuratiertes Formular-Schema fürs Experte-Tool (M10)
RouterOSSchemaCatalog.swift — die eigentliche Menü-Liste: Firewall-Familie voll kuratiert, Rest teils kuratiert/teils generisch (M10)
OverviewGraph.swift — OverviewNode/OverviewEdge/OverviewEdgeKind/DevicePortGroup: reine Datenmodelle fürs Übersicht- und Geräte-Diagramm (M11/M12)
LanDevice.swift — ein LAN-Gerät (DHCP-Lease + ARP + Bridge-Host verschmolzen), fürs Geräte-Tab (M12)
Networking/
RouterOSTransport.swift — Protocol: connect/fetchDeviceInfo/fetchInterfaces/fetchFirewallRuleCounts/fetchMenuItems/apply/disconnect
RestTransport.swift — REST-API (RouterOS ≥7.1), Zertifikats-TOFU, .set via GET+PATCH (findItemID), .remove via GET+DELETE, fetchMenuItems (Array- oder Singleton-Objekt)
SSHTransport.swift — SSH-Fallback via Citadel, CLI-Text-Parsing, resetToFactoryDefaults(), eigene Hostkey-TOFU, fetchMenuItems (generisch, inkl. Singleton-Fallback)
RouterOSTransport.swift — Protocol: connect/fetchDeviceInfo/fetchInterfaces/fetchFirewallRuleCounts/fetchMenuItems/fetchFieldValues/apply/disconnect
RestTransport.swift — REST-API (RouterOS ≥7.1), Zertifikats-TOFU, .set via GET+PATCH (findItemID), .remove via GET+DELETE, fetchMenuItems (Array- oder Singleton-Objekt), fetchFieldValues (Query-Filter, unverifiziert)
SSHTransport.swift — SSH-Fallback via Citadel, CLI-Text-Parsing, resetToFactoryDefaults(), eigene Hostkey-TOFU, fetchMenuItems (generisch, inkl. Singleton-Fallback), fetchFieldValues (`:foreach`+`find`+`get`, M12/Bug 14+17)
RouterOSCliParser.swift — parst `/system resource print`, `/interface print terse`, und generisch jeden Menüpfad (parseGenericItems/parseSingletonItem, M10)
CertificateTrustStore.swift / CertificateFingerprint.swift — TOFU für REST-Zertifikate
SSHHostKeyTrustStore.swift / SSHHostKeyFingerprint.swift — TOFU für SSH-Hostkeys (M7)
@@ -97,6 +102,8 @@ RouterOSAssistant/
Wizard/Steps/Connect/ — Verbinden-Tab (inkl. Schnell-Backup-Button nach erfolgreicher Verbindung)
Wizard/Steps/Setup/ — Einrichten-Tab: Mode → Wan → Lan → (Vlan nur Experte) → Wifi → Firewall → Review/Apply
Expert/ — Experte-Tab (M10): ExpertView (Kategorie-/Menüliste + "eigener Pfad"), ExpertViewModel, ExpertMenuDetailView (Item-Liste + Add/Edit-Sheet)
Overview/ — Übersicht-Tab (M11): OverviewView (Diagramm+Legende+Detailpanel), OverviewViewModel (buildGraph, rein), OverviewLayout (Spalten/Zeilen-Geometrie)
Devices/ — Geräte-Tab (M12): DevicesView (Port-Tabellen+Rechtsklick+Rohdaten-Sheet), DevicesViewModel (buildDevices/buildPortGroups, rein; confirmStaticAssignment)
Backup/ — Sicherungen-Tab (Ordner wählen, Gefahrenzone: Werkseinstellungen wiederherstellen)
Resources/Assets.xcassets/AppIcon.appiconset/ — App-Icon "Signal Router" (16px1024px)
RouterOSAssistantTests/ — reine Unit-Tests (Command-Builder, CLI-Parser, Fallback-Logik via Mock-Transport)
@@ -244,6 +251,59 @@ erreichen (siehe Bug 1 unten).
`ExpertItemEditView`) komplett entfernt, da überflüssig. Danach vom
Nutzer selbst in der App bestätigt: neu angelegtes VLAN erscheint
sofort in der DHCP-Server-Interface-Auswahl.
14. **`/ip dhcp-server lease` gibt das `dynamic`-Feld über `print terse`
nie aus, in keinem Zustand** — beim Bau des Geräte-Tabs (M12) zwei
falsche Annahmen nacheinander gemacht: erst "fehlt = fest" (Standard-
Annahme falsch herum, jedes echte DHCP-Gerät zeigte "Fest"), dann
nach Korrektur "fehlt = dynamisch, `dynamic=no` erscheint explizit
bei festen Leases" — beides falsch. Live an echten Rohdaten geprüft
(Nutzer schickte per "Rohdaten anzeigen" wiederholt komplette
Feldlisten, u.a. für einen zweifelsfrei dynamischen UND einen
zweifelsfrei festen Eintrag): das Feld taucht in `terse` überhaupt
nie auf, unabhängig vom Status — nur `/ip dhcp-server lease print`
(ohne terse) zeigt es als Flag-Buchstabe `D`. Fix: Status kommt nicht
mehr aus gelesenen Feldern, sondern aus einer gezielten Abfrage gegen
RouterOS' interne Eigenschaft (`RouterOSTransport.fetchFieldValues`,
siehe Bug 17 für die korrekte Syntax).
15. **`fetchMenuItems`s `.id`-Positions-Überlagerung ordnete bei
`/ip dhcp-server lease` die falsche `.id` der falschen Zeile zu** —
zwei Leases (iMac dynamisch, Laptop nach Umstellung fest), aber die
App zeigte beide als "Fest". Ursache: `fetchMenuItems` liest die
`.id`s über einen separaten `:put [<Pfad> find]`-Aufruf und ordnet
sie *nach Position* den `print terse`-Zeilen zu (Mechanismus aus
Bug 8, dort für ein anderes Menü live bestätigt) — für dieses Menü
stimmte die Reihenfolge zwischen beiden Aufrufen offenbar nicht
überein, die feste `.id` landete auf der iMac-Zeile. Fix ausschließlich
im Geräte-Tab: Erkennung *und* der `make-static`-Zielauswahl laufen
jetzt komplett über die MAC-Adresse (steht direkt in derselben
`terse`-Zeile, kein Cross-Command-Abgleich nötig) statt über `.id`.
**Nicht behoben:** derselbe `.id`-Überlagerungsmechanismus wird auch
vom Experte-Tab für `.set`/`.remove` auf beliebigen Menüs benutzt —
ob das dort ebenfalls (menü- oder situationsabhängig) falsch zuordnen
kann, ist ungeprüft. Siehe Nächste Schritte.
16. **SwiftUI-`.confirmationDialog` löschte sein eigenes Ziel, bevor die
bestätigte Aktion es lesen konnte** — "Feste IP zuweisen" tat sichtbar
nichts: kein Fehler, kein Erfolg, Dialog schloss normal. Ursache:
`isPresented` war eine berechnete `Binding`, deren Setter bei *jedem*
Schließen (auch durch den bestätigenden Knopf selbst — SwiftUI ruft
den Setter mit `false` bei jedem Tap in einem `.confirmationDialog`,
unabhängig vom gedrückten Knopf) `pendingStaticAssignment = nil`
setzte — also genau das Objekt, das `confirmStaticAssignment()`
Sekundenbruchteile später lesen wollte. Fix: Dialog-Sichtbarkeit
(`showStaticConfirmation`, ein einfaches `@State Bool`) und Ziel-
Objekt (`pendingStaticAssignment`) komplett entkoppelt — genau das
Muster, das `BackupListView`s Werksreset-Dialog schon die ganze Zeit
korrekt verwendet (eigenes `@State`-Bool statt abgeleitetem Binding).
17. **Ein erster Fix-Versuch für Bug 14 geriet zu einer weiteren, nie
verifizierten Befehls-Annahme** — `<Menü> get [find feld=wert]
rückgabefeld]` als *ein* kombinierter Befehl geraten (Annahme: `get`
akzeptiert eine `find`-Ergebnisliste direkt). Live widerlegt: eine
per Winbox bestätigte, echte feste Zuweisung wurde von dieser Abfrage
trotzdem nicht gefunden (falsches Negativ). Fix: nur noch aus zwei
einzeln diese Session mehrfach bestätigten Bausteinen zusammengesetzt
— nacktes `find` mit einer Bedingung, plus `get <id> feld` auf eine
einzelne bekannte `.id`, verbunden über eine `:foreach`-Schleife
(`RouterOSTransport.fetchFieldValues`/`SSHTransport.fetchFieldValues`).
**Lehren:** Citadel/NIOSSH-Fehler immer mit `String(describing:)` loggen,
nie `.localizedDescription`. Jede View, die ein ObservableObject aus einem
@@ -265,7 +325,24 @@ Jeder Zustand, der sich während der Nutzung ändern kann (Interface-Liste
nach Anlegen eines VLANs), gehört live nachgeladen, nicht einmalig beim
Verbinden gecacht (Bug 13) — dasselbe Prinzip wie `crossReferenceOptions`
für Pool/Profil/Skript, nur nicht von Anfang an konsequent auf alle
Auswahllisten angewendet.
Auswahllisten angewendet. **`print terse` gibt nicht jede Eigenschaft
als Feld aus** — manche (z.B. `dynamic` bei DHCP-Leases) erscheinen nur
als Flag-Buchstabe im menschenlesbaren `print`, nie als `key=value`
(Bug 14); wenn ein Feld partout nicht auftaucht, RouterOS' `find`/`get`
gegen die interne Eigenschaft fragen statt weiter nach Formulierungen im
Text zu suchen, die es nicht gibt. **`fetchMenuItems`s `.id`-Überlagerung
ist nicht für jedes Menü verlässlich** (Bug 15) — bei mehreren möglichen
Identifikatoren lieber über ein Feld matchen, das direkt in derselben
Zeile steht (MAC, Name), statt über eine per Position aus einem zweiten
Befehl zugeordnete `.id`. **Jede kombinierte RouterOS-Skript-Syntax vor
dem Einsatz live prüfen, auch wenn die Einzelteile schon bestätigt sind**
(Bug 17) — `find` allein und `get <id> feld` allein zu kennen heißt
nicht, dass `get [find ...] feld` als eine Einheit genauso funktioniert.
**Bei SwiftUI-Bestätigungsdialogen Sichtbarkeit und Nutzlast nie an
dasselbe State-Feld koppeln** (Bug 16) — `isPresented`s Setter feuert bei
jedem Knopfdruck, nicht nur bei Abbrechen; ein eigenes, unabhängiges Bool
fürs Zeigen/Verstecken hält die Nutzlast so lange am Leben, wie die
bestätigte Aktion sie braucht.
## `xcodebuild test` hängt — Gatekeeper, kein Code-Bug
@@ -296,7 +373,21 @@ wiederholen.
- **REST-Pfad ungetestet für Schreibvorgänge** — auf beiden bisherigen
Testgeräten war `www-ssl` (Port 443) aus, jeder Schreibtest lief über
SSH. Der REST-`apply()`-Pfad (`POST`/`PATCH`, `findItemID` für `.set`)
ist nur gegen Mocks getestet.
ist nur gegen Mocks getestet. Der REST-Zweig von `fetchFieldValues`
(M12/Bug 14) ist zusätzlich auf einer nirgends offiziell dokumentierten
Annahme aufgebaut (Query-String-Filter `?feld=wert` auf einem GET) —
komplett unverifiziert.
- **"Zurück auf dynamisch" fehlt im Geräte-Tab (M12)** — es gibt nur
"Feste IP zuweisen" (Winbox' "Make Static"), keine Umkehrung. Kein Bug,
einfach noch nicht gebaut; Nutzer hat danach gefragt.
- **`fetchMenuItems`s `.id`-Positions-Überlagerung: Verlässlichkeit für
andere Menüs ungeprüft** (siehe Bug 15) — bei `/ip dhcp-server lease`
live als falsch bestätigt (`.id` landete auf der falschen Zeile), für
den Geräte-Tab per MAC-Adress-Matching umgangen. Derselbe Mechanismus
läuft aber weiterhin im Experte-Tab für `.set`/`.remove` auf *jedem*
Menü — ob und wo das dort ebenfalls falsch zuordnen kann, ist nicht
untersucht. Sollte bei unerklärlichem Verhalten dort (falsches Item
geändert/gelöscht) als erste Verdachtsquelle geprüft werden.
- **WLAN-`.set`-Pfad (M5) weiterhin ungetestet gegen echte Hardware** —
nur der "kein WLAN"-Zweig ist bestätigt (zwei Testgeräte, beide ohne
WLAN-Chip). Sicherheitsprofil-Anlage + SSID/Passwort-`.set` auf einem
@@ -501,6 +592,21 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
Schreibzugriff auf `/ip address`/`/ip dhcp-client`/`/ip dns`/
`/ip service` (Risiko Selbstaussperrung/
WAN-Ausfall).
- ✅ M11: Übersicht-Tab (Diagramm: Interfaces→IP→DHCP/Pools→Routen→
Firewall/NAT, echte Referenz-Kanten, nach Verbindungsart eingefärbt,
Hover/Klick-Hervorhebung) — **live gegen Hardware verifiziert**, rein
lesend (kein `apply()`), vom Nutzer bestätigt ("das gefällt mir schon
sehr gut"). Deckt bewusst nicht ab: VPN/WLAN-Sicherheitsprofile/Queues/
System/Werkzeuge/Mangle/Raw (siehe `OverviewGraph.unmappedAreas`),
weiterhin nur über den Experte-Tab erreichbar.
- ✅ M12: Geräte-Tab (LAN-Scanner: DHCP-Leases+ARP+Bridge-Host, eine
Tabelle pro physischem Port, "Feste IP zuweisen" für dynamische Leases)
— **live gegen Hardware verifiziert und vom Nutzer bestätigt
funktionierend** ("das funktioniert jetzt super"), nach vier gefundenen
und gefixten Bugs (Bug 1417, siehe oben) — die aufwendigste
Live-Verifikation aller Milestones bisher, mehrere Diagnose-Runden mit
Rohdaten-Vergleichen und direkten Router-Gegenchecks (SSH-CLI + Winbox).
Fehlt noch: "Zurück auf dynamisch" (siehe Bekannte Einschränkungen).
## Nächste Schritte
@@ -543,12 +649,32 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
"Wiederherstellen"-Button pro Eintrag im Sicherungen-Tab
(`BackupListView`) mit Bestätigungsdialog (ähnlich der bestehenden
Werksreset-Gefahrenzone).
11. **Geräte-Tab: "Zurück auf dynamisch"-Aktion bauen** — Gegenstück zu
"Feste IP zuweisen". RouterOS-Weg dafür noch nicht recherchiert/live
geprüft (vermutlich `remove` der Lease und Neuanlage, oder ein
eigenes Menü-Kommando wie bei `make-static` — vor dem Bauen klären,
nicht raten, siehe Bug 17 als Warnung).
12. **`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
prüfen** — live als falsch bestätigt für `/ip dhcp-server lease`
(Bug 15). Betrifft potenziell jedes `.set`/`.remove` im Experte-Tab.
Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig
vorhandenen Einträgen `:put [<Pfad> find]` und `<Pfad> print terse`
unabhängig ausführen und die Reihenfolgen von Hand vergleichen.
13. Optional: den neuen Dauer-Editor auch für den Geräte-Tab nutzen, falls
dort künftig Zeitfelder (z.B. eine Lease-Zeit-Anzeige) dazukommen.
14. `Kay-Uwes-iMac` (ether2, `defconf`) und der Test-Laptop `DEDELLB2M6GK3`
(ether3, `dhcp5`/`defconf` je nach Testrunde) sind **echte Geräte des
Nutzers**, mit denen M12 live durchgetestet wurde — keine
Test-Artefakte, nicht aufräumen. Der Laptop hatte am Ende der Session
testweise eine feste IP zugewiesen bekommen; ob das der gewünschte
Dauerzustand ist oder nur zum Testen war, mit dem Nutzer klären.
Kein Gitea-Remote vorhanden — falls der Nutzer später eine Gitea-Instanz
aufsetzt (z.B. selbst gehostet auf der vorhandenen OMV-NAS, siehe
`Aperto/`-Projekt), Remote hinzufügen und pushen; bis dahin lokales Git.
`/Applications/RouterOS Assistant.app` ist der aktuell installierte
Release-Build (Stand: Commit `008ee65`, App-Icon "Signal Router"). Nach
jeder weiteren Code-Änderung muss dieser Build-und-Kopier-Schritt (siehe
oben) wiederholt werden, damit die installierte App aktuell bleibt.
Release-Build, auf dem Stand des jeweils letzten Commits auf `main`
(App-Icon "Signal Router"). Nach jeder weiteren Code-Änderung muss dieser
Build-und-Kopier-Schritt (siehe oben) wiederholt werden, damit die
installierte App aktuell bleibt.