Files
KayandClaude Sonnet 5 3034d8b874 Bug 37: SSH-Hostkey-Trust bei REST-Verbindungen proaktiv etablieren
Dedizierte SSH-Dienste (BackupService u.a.) sind der erste echte
SSH-Kontakt zu einem per REST verbundenen Router und trafen dort auf
einen unbestätigten Hostkey ohne Trust-UI (nur ReviewApplyView/
ExpertMenuDetailView-Fehlertext, kein Bestätigungsweg). Fix: nach
jedem erfolgreichen REST-Connect prüft ConnectionService den
SSH-Hostkey einmalig im Hintergrund und zeigt bei Bedarf denselben
Trust-Dialog wie der SSH-Fallback, ohne die aktive REST-Verbindung neu
aufzubauen. Live bestätigt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 16:23:33 +02:00

1848 lines
119 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RouterOS Assistant — Handoff
Stand: App-Icon fertig, alle Kern-Features (M1M6) gebaut und mindestens
teilweise gegen zwei echte physische Mikrotik-Testgeräte verifiziert
(ein generisches Gerät für M1M4, ein hEX-Router für M5/M6 — dabei ein
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. 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:** drei weitere Tabs/Features. **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`) sowie ihr
Gegenstück "Feste Zuweisung entfernen" (RouterOS kennt kein
`make-dynamic`, nur `remove` — vorher per Recherche-Agent gegen die
offizielle Doku geprüft statt geraten). **M13 Backup-Wiederherstellung**
im Sicherungen-Tab: `.rsc`-Backup per SFTP hochladen, dann RouterOS'
dokumentierter `reset-configuration`+`run-after-reset`-Restore-Weg, mit
Modell-Abgleich vor dem Bestätigungsdialog (Brick-Schutz bei falschem
Routermodell) und automatischem Login-Erhalt (siehe Bug 18). Alle drei
Tabs/Features live gegen den hEX-Testrouter verifiziert, dabei M12 (vier
Bugs, 1417) und M13 (ein besonders ernster Bug, 18: erster Testlauf
sperrte den Router komplett aus, nur per Hardware-Reset behebbar) beide
zäher als erwartet, am Ende aber jeweils vom Nutzer selbst bestätigt
("das funktioniert jetzt super" / "das hat sofort funktioniert" / "gut,
das hat funktioniert").
**Danach, gleiche Session, M14 "Update-Check":** Verbinden-Tab zeigt jetzt
volle Routerboard-Daten (Modell/Revision/Seriennummer/Firmware, aus
`/system routerboard`) und einen RouterOS-Software-Update-Check
(`/system package update`: Kanal/installierte/neueste Version/Status,
"Jetzt prüfen", "Update installieren"), plus "Firmware aktualisieren"
für die Routerboard-Bootloader-Firmware und einen "Jetzt neu starten"-
Button danach — alle vier Aktionen live bestätigt, inkl. der zuvor
unsicheren Frage, ob `/system routerboard upgrade`s normalerweise
interaktive Bestätigung den nicht-interaktiven SSH-Weg dieser App
blockiert (tut es nicht, lief sauber durch). Dabei ein weiterer Bug im
generischen Lese-Pfad gefunden (Bug 20, siehe unten). Anschließend ein
Design-Durchgang: Verbinden-Detailseite/Sicherungen/Geräte liefen auf
nackten `List`s ohne Rahmen — umgestellt auf `Form`+`.formStyle(.grouped)`,
denselben nativen macOS-"Karten"-Look, den Wizard und Experte-Tab schon
die ganze Zeit hatten. Dark Mode auf Nachfrage geprüft: funktioniert ohne
Codeänderung (keine `.preferredColorScheme`-Überschreibung, keine feste
Farbwerte, alles adaptive System-Farben) — vom Nutzer live bestätigt.
Release-Build unter `/Applications/RouterOS Assistant.app` ist auf
aktuellem Stand.
**Danach, neue Session: M8 komplett live verifiziert — erst manuell,
dann über den Wizard selbst.** Bug 21 aus der Vorsession (WAN-DHCP-
Client/PPPoE-Apply nicht idempotent) blieb bestätigt gefixt. Isolation
zuerst manuell per SSH an `ether5` nachgebaut (eigenes Subnetz, eigener
DHCP-Server, `/interface list`-basierte Forward-Drop-Regel) — dabei drei
reine Konfigurationsfehler der Reihe nach gefunden und behoben (Adresse
versehentlich als `/32` bzw. `.0`-Netzadresse statt Host-Adresse
eingetragen; `ether5` fehlte in der defconf-Interface-Liste "LAN",
wodurch DNS-Anfragen an den Router selbst blockiert wurden, obwohl
DHCP/Routing/Internet normal liefen). Isolation danach beidseitig
bestätigt (iMac ↔ Laptop erreichen sich nicht, Internet für beide
weiterhin ja).
Anschließend derselbe Test **über den App-Wizard** (Experte-Modus,
`ether4`) wiederholt, um die Abnahme-Bedingung aus der Vorsession
("kompletter Wizard-Durchlauf") tatsächlich zu erfüllen — dabei drei
echte App-Bugs gefunden und gefixt, alle auf denselben "Wizard wurde nie
gegen einen bereits konfigurierten Router bzw. mit bereits gebrückten
Ports erneut ausgeführt" blinden Fleck zurückzuführen:
- **Bug 22:** `DhcpServerCommandBuilder` fügte ein neues LAN-/VLAN-
Interface nie der defconf-Interface-Liste "LAN" hinzu — derselbe
DNS-Bug wie oben, jetzt als App-Bug bestätigt. Fix: automatisch
`/interface list add name=LAN` + `/interface list member add
list=LAN interface=<name>`, beide toleriert falls schon vorhanden.
- **Bug 23:** ein voller Wizard-Durchlauf gegen einen bereits
konfigurierten Router brach beim zweiten Befehl ab
(`/ip address add address=192.168.88.1/24 interface=bridge`
"already have such address"), weil nur WAN-DHCP-Client/PPPoE
idempotent behandelt wurden (Bug 21). Fix: `SetupViewModel.
applyIdempotently` erweitert — `/ip pool`/`/ip dhcp-server`/
`/ip dhcp-server network` werden bei Duplikat jetzt als `.set`
(gematcht auf name/name/address) erneut versucht, `/ip address` bei
exaktem Duplikat als bereits erledigt behandelt (ein Interface darf
mehrere Adressen halten, ein `.set` nach "interface" träfe sonst
potenziell die falsche).
- **Bug 24:** LAN-/VLAN-Schritt löste ein gewähltes physisches Interface
nie aus einer bestehenden Bridge — auf Werks-Routern sind ether25 ab
Werk gebridged. Als eigenes isoliertes Netz konfiguriert blieb das
Interface Bridge-"Slave", RouterOS verwarf die generierten
Isolationsregeln selbst als ungültig ("in/out-interface matcher not
possible when interface is slave - use master instead"), live an
`ether4` bestätigt. Fix: `DhcpServerCommandBuilder` stellt jetzt
`/interface bridge port remove [find interface=<name>]` voran
(übersprungen für "bridge" selbst), toleriert als bereits erledigt
falls nie gebridged.
Nach allen drei Fixes lief der komplette App-Wizard-Durchlauf für
`ether4` fehlerfrei durch (inkl. Neuanwendung der bereits vorhandenen
`bridge`/`ether1`-Konfiguration), Isolation + DNS + Internet vom Nutzer
am echten Gerät bestätigt. Alle 52 Unit-Tests grün. M8 damit als
einziger Milestone bisher **sowohl manuell als auch über die App selbst**
live verifiziert.
## Ziel
Native macOS-App (SwiftUI), die Laien per geführtem Interview-Wizard durch
die häufigsten Mikrotik-RouterOS-Konfigurationen führt: Internet-Anschluss,
Heimnetzwerk/DHCP, zusätzliche VLAN-Netzwerke, WLAN, Firewall-Grundschutz,
plus Backup/Wiederherstellung. Vollständiger ursprünglicher Plan/Kontext:
`~/.claude/plans/purrfect-puzzling-bunny.md` (seither an vielen Stellen
weiterentwickelt, siehe unten).
## Setup / Bauen
```bash
cd RouterOS
xcodegen generate # erzeugt RouterOSAssistant.xcodeproj (nicht in git)
open RouterOSAssistant.xcodeproj # oder direkt per xcodebuild
# Debug (Xcode Cmd+R empfohlen für manuelle/Live-Tests, siehe Gatekeeper-Hinweis unten)
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
-destination 'platform=macOS' build
# Nur kompilieren, nicht ausführen (umgeht den Gatekeeper-Hang, siehe unten)
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
-destination 'platform=macOS' build-for-testing
# Release + Deploy nach /Applications
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
-configuration Release build
rm -rf "/Applications/RouterOS Assistant.app"
cp -R "<DerivedData>/Build/Products/Release/RouterOSAssistant.app" \
"/Applications/RouterOS Assistant.app"
killall Dock # Icon-Cache auffrischen, falls sich nur das Icon geändert hat
```
`xcodegen` ist via Homebrew installiert. Projekt-Struktur/Targets/Package-
Abhängigkeiten stehen in `project.yml`, das ist die Quelle der Wahrheit —
nicht das generierte `.xcodeproj` von Hand editieren.
Git: Remote `origin` zeigt auf eine selbst gehostete Gitea-Instanz
(`git@192.168.178.222:kay/RouterOS.git`).
`.xcodeproj`, `DerivedData`, `.build`, Firmware-Dateien (`*.npk`, `*.cpgz`)
und Router-Backups (`*.rsc`, `/Backups/`) sind gitignored.
## Architektur
```
RouterOSAssistant/
App/RouterOSAssistantApp.swift — 3 Tabs, teilen sich EINE ConnectionService-Instanz
Core/
Models/
RouterOSCommand.swift — eine Änderung, drei Operationen (.add/.set/.remove), zwei Renderer (CLI-Zeile / REST-JSON)
WanConfig.swift, LanDhcpConfig.swift, VlanEntry.swift, WifiNetworkConfig.swift, FirewallConfig.swift — bauen je RouterOSCommand-Listen
DhcpServerCommandBuilder.swift — geteilte "Adresse+Pool+Server+Netzwerk"-Logik (LAN + VLAN)
RouterOSModels.swift — Credentials, DeviceInfo, Interface, RouterOSError
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/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), uploadScript (SFTP, M13), applyRestoreScript (reset-configuration+run-after-reset, M13)
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)
Services/
ConnectionService.swift — zentraler App-State: REST-zuerst-SSH-Fallback, hält credentials/interfaces/deviceInfo, plus generisches fetchMenuItems
BackupService.swift — Config-Export (`/export terse`) über eigene SSH-Verbindung, wählbarer Zielordner, restoreBackup (M13: SFTP-Upload + reset-configuration+run-after-reset + Login-Erhalt), backupModel (Modell-Header-Parsing)
FactoryResetService.swift — /system reset-configuration über eigene SSH-Verbindung ("Gefahrenzone")
KeychainService.swift — Passwort-Speicherung
UpdateService.swift — M14: RouterOS-Software-Update (/system package update check-for-updates/install) + Routerboard-Firmware (upgrade) + reboot, eigene SSH-Verbindung
Features/
Wizard/Steps/Connect/ — Verbinden-Tab (inkl. Schnell-Backup-Button, Routerboard-Info, Software-/Firmware-Update-Check (M14), 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/confirmStaticRemoval)
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)
```
**Architekturprinzip `RouterOSCommand` (.add/.set):** `.add` legt neue
Einträge an (CLI `... add key=value`, REST `POST`). `.set` ändert
bestehende Einträge, per `matchField`/`matchValue` gefunden — CLI löst das
inline über `set [find field=value] ...`, REST muss dafür erst per GET das
Item suchen, seine RouterOS-interne `.id` auslesen, dann
`PATCH restPath/<id>` schicken (`RestTransport.findItemID`). Eingeführt für
WLAN (M5), weil Wireless-Interfaces schon vor jeder Konfiguration
existieren. VLAN (M4) hat trotzdem weiterhin keine Port-Zuweisung
(Access/Trunk) — technisch jetzt machbar, aber bewusste Scope-Entscheidung,
nicht nachgezogen.
**System-Aktionen (Backup, Werksreset) laufen immer über eine eigene
SSH-Verbindung**, unabhängig vom aktiven Live-Transport — `BackupService`
und `FactoryResetService` folgen demselben Muster: es sind einmalige
System-Kommandos, kein Menü-Item zum Hinzufügen/Ändern, passen also nicht
ins `RouterOSCommand`-Modell und haben keine verifizierte REST-Entsprechung.
**ConnectionService ist der einzige geteilte State.** Alle drei Tabs
bekommen dieselbe Instanz von der App-Ebene injiziert (kein
`@EnvironmentObject`, explizite Übergabe im Init). SwiftUI-Falle: jede
View, die `connectionService`-Felder liest, muss ihn selbst als
`@ObservedObject` halten — nicht nur transitiv über ein anderes ViewModel
erreichen (siehe Bug 1 unten).
## Gefundene Bugs (chronologisch, alle gegen echte Geräte)
1. **ConnectView zeigte nie Verbindungsstatus**`connectionService` war
nur über `viewModel.connectionService` erreichbar (zwei Ebenen tief
verschachteltes ObservableObject), SwiftUI abonniert das nicht
automatisch. Fix: `ConnectView` hält `connectionService` zusätzlich
selbst als `@ObservedObject`.
2. **`NIOSSHError.localizedDescription` nutzlos** — bridged auf
generisches NSError (`"error 1"`). Fix: `String(describing:)`.
3. **SSH scheiterte mit `keyExchangeNegotiationFailure`** — RouterOS bietet
nur Legacy-Algorithmen an, Citadel-Default enthält sie nicht. Fix:
`algorithms: .all`.
4. **`Citadel.SSHClient.CommandFailed` zeigte nur den Exit-Code** —
`executeCommand()` verwirft die gesammelte Ausgabe bei Exit-Code ≠ 0.
Fix: eigene Sammlung über `executeCommandStream()`.
5. **Picker-Warnungen durch Platzhalter-Interfacenamen**`prepareDefaults()`
korrigiert jetzt WAN und LAN, läuft schon im `init`.
6. **Kritisch: CLI-Parser verlor 6 von 7 Interfaces auf einem hEX-Router**
`parseInterfaces`/`parseDeviceInfo` splitteten nur auf `"\n"`; die
SSH-Ausgabe dieses Geräts trennte Zeilen anders, sodass alle
Interface-Zeilen zu einer verschmolzen — beim key=value-Parsen
überschrieb jedes Feld den vorherigen Wert, übrig blieb nur das letzte
Interface im Text (`lo`, Loopback) mit vermischten Werten. Folge: **jede
WAN-Interface-Referenz in den angewendeten Firewall-Regeln zeigte auf
`lo` statt den echten WAN-Port** — auf diesem Testgerät folgenlos, weil
RouterOS bereits eine vollständige eigene Standard-Firewall (defconf)
mitbrachte, die weiterhin schützte; auf einem Gerät ohne bestehende
Firewall wäre das eine wirkungslose, aber als aktiv angezeigte Firewall
gewesen. Fix: `split(whereSeparator: \.isNewline)` (robust gegen
`\n`/`\r`/`\r\n`) + zusätzliches Sicherheitsnetz: Loopback-Interfaces
werden in `SetupView` jetzt grundsätzlich aus allen WAN/LAN/VLAN-
Auswahllisten gefiltert. Regressionstest mit echten `\r\n`-getrennten
hEX-Daten in `RouterOSCliParserTests`.
7. **`/ip service`: Feld heißt `available-from`, nicht `address`** — beim
Kuratieren des Experte-Schemas geraten statt geprüft. Live gegen den
hEX bestätigt: `/ip service print` zeigt tatsächlich `available-from=`;
ein `set ... address=...` hätte den Zugriffsfilter stillschweigend
nicht gesetzt. Schema korrigiert (`RouterOSSchemaCatalog.swift`).
8. **RouterOS 7.24.2 zeigt `.id` gar nicht in `print terse`, und
Singleton-Menüs verweigern `terse` komplett** — Annahme beim Bau des
generischen Experte-Motors (M10) war, `.id=*N` sei Teil jeder
`print terse`-Zeile (wie bei REST-JSON). Live-Test zeigte: RouterOS
7.24.2 liefert dort gar kein `.id`-Feld; Menüs mit genau einem Eintrag
statt einer Liste (`/ip dns`, `/system identity`, `/system clock`,
`/system ntp client`, `/tool e-mail`) lehnen `terse` sogar komplett ab
(`"bad parameter terse"`). Fix: `:put [<Menüpfad> find]` liefert die
echten internen IDs (z.B. `*C;*1;*2`) — live bestätigt, dass die
Reihenfolge exakt der von `print terse` entspricht, damit per Position
zuordenbar. Für Singleton-Menüs neuer `RouterOSMenuSchema.isSingleton`-
Schalter: Fetch fällt auf normales `print` (Doppelpunkt-Format,
`parseSingletonItem`) zurück, `.set` läuft ohne `[find ...]` direkt
(`matchField: ""` als Sentinel in `RouterOSCommand`/beiden Transports).
Beides live gegen `/system identity` durchgespielt (Name testweise
geändert, Fehlertext exakt geprüft, zurückgesetzt).
9. **Cross-Referenz-Felder (z.B. DHCP-Server → Adress-Pool) waren reiner
Freitext** — Nutzer meldete: "kann den vorher angelegten Adress-Pool
nicht in der Auswahl sehen". Betraf jedes Feld, das den Namen eines
Eintrags aus einem *anderen* Menü referenziert (DHCP-Server→Pool,
PPP-Benutzer→Profil, Scheduler→Skript, Firewall-Filter→Adress-Liste,
WireGuard-Peer→Interface) — alle als `.text` statt als Auswahl gebaut.
Fix: neuer Feldtyp `RouterOSFieldSchema.Kind.menuItemPick(menuPath:
restPath:valueField:)`, lädt beim Öffnen eines Menüs live die
existierenden Namen aus dem referenzierten Menü
(`ExpertViewModel.loadCrossReferenceOptions`) und zeigt sie als Picker.
Auf alle fünf betroffenen Felder angewendet.
10. **Kritisch, app-weit: RouterOS' SSH-CLI gibt bei fehlgeschlagenen
Befehlen Exit-Code 0 zurück** — Nutzer meldete: DHCP-Server-Anlage
zeigte keine Fehlermeldung, wurde aber auch nicht angelegt. Live
reproduziert: `/ip dhcp-server add ...` auf einem Interface mit
bereits bestehendem Server lieferte `"failure: server or relay with
such interface already exists"` **und Exit-Status 0**; ein ungültiger
`action=`-Wert lieferte `"syntax error (...)"`, ebenfalls Exit-Status
0 (per `expect`-Skript mit `catch wait result` geprüft). `SSHTransport.
apply()` warf bisher nur bei einem von Citadel gemeldeten
Nicht-Null-Exit-Code — also nie. **Betraf jeden `.apply()`-Aufruf über
SSH app-weit**, nicht nur den Experte-Tab: der komplette Einrichten-
Wizard (WAN/LAN/VLAN/Firewall) lief bei jedem SSH-Schreibvorgang mit
demselben blinden Fleck, seit M3. Fix: `apply()` behandelt jede
nicht-leere Ausgabe eines add/set/remove-Befehls als Fehler — laut
allen in dieser Session live getesteten Befehlsfamilien (15+) sind
erfolgreiche mutierende Befehle immer still. Nach dem Fix live end-to-
end bestätigt: komplette Kette VLAN→IP-Adresse→Pool→DHCP-Server→
DHCP-Netzwerk auf einem freien Interface angelegt, lief fehlerfrei
durch, danach vollständig wieder entfernt.
11. **Bool-Felder im Experte-Schema als `"true"/"false"` statt `"yes"/"no"`
geschrieben** — Nutzer meldete nach Bug-10-Fix: VLAN-Anlage zeigte
jetzt `"Unerwartete Antwort vom Router: syntax error (line 1 column
30)"`. Live reproduziert mit der exakt von der App erzeugten
Befehlszeile (`disabled=false`) — identischer Fehler; mit
`disabled=no` lief es durch. RouterOS' CLI akzeptiert nur `yes`/`no`
für Bool-Parameter, nicht `true`/`false`. Betraf praktisch jedes
kuratierte Schema (23× `"false"`, 4× `"true"` als `defaultValue`).
Fix zweigleisig: `ExpertViewModel.pendingCommand` normalisiert jeden
`.bool`-Feldwert vor dem Senden auf `yes`/`no` (Schutz unabhängig von
der Quelle), zusätzlich Katalog-Strings selbst bereinigt. Live erneut
end-to-end bestätigt (VLAN→IP→Pool→DHCP-Server mit `disabled=no`).
12. **Lösch-Buttons fehlten komplett im Experte-Tab** — Nutzer: "mir
fehlen komplett die Löschen-Buttons". Ursache: `.swipeActions(...)`
ist eine iOS/iPadOS-Wischgeste ohne Entsprechung auf macOS-Listen —
dort erscheint schlicht nichts, unabhängig vom Code. Fix: sichtbarer
Papierkorb-Icon-Button je Zeile statt Wisch-Geste (nur bei
Nicht-Singleton-Menüs, wo Löschen überhaupt Sinn ergibt). Lösch-Pfad
(ID-Ermittlung + `remove [find .id=...]`) live gegen das vom Nutzer
selbst angelegte VLAN nachvollzogen, funktioniert.
13. **Interface-Auswahl (`.interfacePick`) war einmalig beim Verbinden
eingefroren** — Nutzer legte ein VLAN an, danach fehlte es in der
Interface-Auswahl beim Anlegen eines DHCP-Servers. Ursache:
`.interfacePick` las aus `ConnectionService.interfaces`, nur einmal
bei `finishConnecting()` befüllt, nie danach aktualisiert — anders als
die neueren `.menuItemPick`-Felder (Pool/Profil/Skript), die bei
jedem Menü-Öffnen frisch laden. Fix: neues
`ExpertViewModel.liveInterfaceNames`, per `/interface print terse` bei
jedem `reloadItems()` aktualisiert; `availableInterfaces`-Parameter-
Durchreichung (`ExpertView``ExpertMenuDetailView`
`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`).
18. **Kritisch, live so passiert: Backup-Wiederherstellung sperrte den
Login komplett aus, nur ein Hardware-Reset half noch** — beim ersten
Live-Test der neuen Restore-Funktion (M13). Ursache doppelt: RouterOS'
`/export` kann Benutzerpasswörter grundsätzlich nie exportieren
(offiziell dokumentiert), und der für den Restore genutzte
`no-defaults=yes`-Reset löscht zusätzlich den mitgelieferten
Standard-Admin-Account komplett — nach dem Restore existierte also
weder aus der Sicherung noch als Werksvorgabe ein funktionierender
Login. Fix: `BackupService.restoreBackup` stellt dem hochgeladenen
Skript jetzt einen kleinen, selbst generierten Block voran (nicht
angehängt — RouterOS bricht den Import beim ersten Fehler ab, ein
Login-Erhalt am Ende hätte also nie gegriffen, falls irgendwo vorher
im eigentlichen Sicherungsskript ein Fehler auftritt), der exakt das
aktuell in der App verwendete Login per `:if ([/user find
name=...] = "") do={add ...} else={set ... password=...}` neu anlegt
bzw. das Passwort neu setzt — das einzige Passwort, das die App
zuverlässig kennt. Beim erneuten Live-Test danach vom Nutzer bestätigt
("das hat funktioniert").
19. **SwiftUI-Typprüfung bricht bei zu langen Modifier-Ketten ab, kein
Logikfehler** — `BackupListView` warf nach dem Hinzufügen weiterer
`.confirmationDialog`/`.alert`-Modifiere für den Restore-Dialog
`"the compiler is unable to type-check this expression in reasonable
time"`, mit einer irreführenden Zeilenangabe (zeigte auf einen ganz
gewöhnlichen, bereits woanders im selben File funktionierenden
`Binding`-Ausdruck). Fix: die Restore-spezifischen Dialoge/Alerts in
eine eigene `@ViewBuilder`-Computed-Property ausgelagert, statt alles
in einer einzigen `body`-Kette zu verketten — teilt die
Typinferenz-Last auf zwei kleinere Ausdrücke auf. Bei künftigen
Views mit vielen `.alert`/`.confirmationDialog`/`.sheet`-Modifiern in
Folge vorbeugend gleich aufteilen, statt erst bei diesem Fehler zu
reagieren.
20. **`fetchMenuItems`s Singleton-Fallback (Bug 8/M10) griff für
`/system routerboard` nicht — genau dasselbe Bug-10-Muster, nur an
einer Lesestelle statt beim Schreiben.** Die neue Routerboard-Sektion
(M14) blieb leer, kein Fehler, nichts. Ursache: der Fallback reagiert
bisher nur auf eine echte Exception ("bad parameter terse" via
Citadels `CommandFailed`), aber `/system routerboard print terse`
liefert diesen Fehlertext live bestätigt mit Exit-Code 0 zurück — der
Fehlertext landete also als normaler (unparsbarer) Output, `fetchMenuItems`
gab still `[]` zurück statt in den Singleton-Zweig zu fallen. Fix:
zusätzlich den zurückgegebenen Output-Text selbst auf "bad parameter
terse" prüfen, nicht nur die Exception — deckt beide Varianten ab, wie
RouterOS diesen Fehler je nach Menü meldet. Betrifft potenziell jedes
Singleton-Menü, nicht nur `/system routerboard` — der generische
Lese-Pfad wird von Übersicht-, Geräte- und Experte-Tab gemeinsam
genutzt.
21. **Wizard-WAN-Apply war nicht idempotent** — Wizard erneut auf einem
Interface durchlaufen, das schon einen DHCP-Client hat (hier: aus
einer zurückgespielten Sicherung), scheiterte live mit "failure:
dhcp-client on that interface already exists". `WanConfig.
buildCommands()` erzeugt für DHCP-Client/PPPoE immer `.add`, ohne
vorher zu prüfen, ob auf dem Interface schon einer existiert. Fix:
`SetupViewModel.applyIdempotently` fängt einen `.add`-Fehlschlag auf
`/ip dhcp-client`/`/interface pppoe-client` ab und wiederholt ihn als
`.set` (nach "interface" gematcht) — bewusst nur für diese zwei
Menüs mit "maximal ein Eintrag pro Interface"-Semantik, nicht generell
für jedes `.add` (z.B. `/ip address` erlaubt legitim mehrere
Adressen pro Interface). **Noch nicht erneut live getestet**, siehe
Nächste Schritte.
**Lehren:** Citadel/NIOSSH-Fehler immer mit `String(describing:)` loggen,
nie `.localizedDescription`. Jede View, die ein ObservableObject aus einem
ViewModel liest, muss es selbst separat als `@ObservedObject` halten.
Zeilen-Splitting bei SSH-Ausgaben grundsätzlich mit `\.isNewline` statt
festem `"\n"`, da sich das je nach Gerät/Version unterscheiden kann.
Bei generischem RouterOS-Tooling (M10) nichts über CLI-Ausgabeformate
annehmen, was nicht live geprüft ist — `.id`-Verhalten und `terse`-Support
unterscheiden sich zwischen Menütypen und RouterOS-Versionen. **RouterOS'
SSH-Exit-Code ist kein verlässliches Erfolgssignal** — Ausgabetext ist die
einzige verlässliche Fehlerquelle über diesen Transport (Bug 10); REST
ist davon nicht betroffen (prüft HTTP-Statuscodes korrekt). RouterOS'
CLI will `yes`/`no` für Bools, nie `true`/`false` (Bug 11) — an einer
einzigen Stelle normalisieren (`pendingCommand`) statt an jedem
Schema-Eintrag hoffen, es richtig geschrieben zu haben. SwiftUI-
Interaktionsmuster nicht plattform-blind übernehmen — `.swipeActions`
sieht in der Doku harmlos aus, ist aber faktisch iOS/iPadOS-only (Bug 12).
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. **`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. **RouterOS-Backups über `/export` enthalten
grundsätzlich nie Passwörter, und `no-defaults=yes` löscht auch den
Werks-Admin-Account** (Bug 18) — jede Restore-Funktion, die einen
kompletten Reset vor dem Re-Import macht, muss den Login selbst wieder
herstellen, sonst folgt ein kompletter Aussperr-Vorfall (nur per
Hardware-Reset behebbar, live so passiert). Login-Erhalt gehört an den
*Anfang* des Skripts, nicht ans Ende — RouterOS bricht den Import beim
ersten Fehler irgendwo im Skript komplett ab. **SwiftUI-Typprüfung kann
bei langen Modifier-Ketten mit einer irreführenden Fehlerzeile
timeouten** (Bug 19) — kein Hinweis auf einen echten Logikfehler an der
genannten Stelle; bei vielen `.alert`/`.confirmationDialog` in Folge
vorbeugend in eigene `@ViewBuilder`-Properties aufteilen. **"RouterOS'
Exit-Code ist kein verlässliches Erfolgssignal" (Bug 10) gilt auch für
Lesebefehle, nicht nur fürs Schreiben** (Bug 20) — derselbe
Singleton-Fallback, der für ein Menü per Exception ausgelöst wurde
(Bug 8), blieb für ein anderes Menü aus, weil RouterOS denselben
Fehlertext dort mit Exit-Code 0 zurückgibt; robuste Erkennung muss immer
auch den Output-Text selbst prüfen, nie nur auf eine geworfene Exception
vertrauen. **Wizard-Apply nutzte für WAN-DHCP-Client/PPPoE immer `.add`**
(Bug 21) — scheiterte live mit "failure: dhcp-client on that interface
already exists" auf einem Interface, das (z.B. aus einer zurückgespielten
Sicherung) schon einen Client hatte; `SetupViewModel.applyIdempotently`
versucht seitdem bei `.add`-Fehlschlag auf diesen beiden Menüs
automatisch `.set` (nach "interface" gematcht). **Ein neu eingerichtetes
LAN-/VLAN-Interface wurde nie der defconf-Interface-Liste "LAN"
hinzugefügt** (Bug 22) — auf Routern mit Werks-Firewall
(`chain=input action=drop in-interface-list=!LAN`) blieb dadurch jede
Anfrage an den Router selbst (DNS, Winbox) vom neuen Netz aus blockiert,
obwohl DHCP/Routing/Internet normal liefen (reine Forward-Chain-Sache,
unberührt) — live an einer manuell nachgebauten Isolation gefunden
(Lease + Default-Route vorhanden, jede DNS-Anfrage lief trotzdem in
Timeout). **Voller Wizard-Durchlauf gegen einen bereits konfigurierten
Router brach an jedem nicht-idempotenten `.add` ab** (Bug 23) — nur
WAN-DHCP-Client/PPPoE waren idempotent (Bug 21); `/ip address`
(exaktes Duplikat = bereits erledigt) sowie `/ip pool`/`/ip
dhcp-server`/`/ip dhcp-server network` (Retry als `.set`, gematcht auf
name/name/address) kamen dazu. **Ein als eigenes isoliertes Netz
konfiguriertes Interface blieb Bridge-"Slave"** (Bug 24) — auf
Werks-Routern sind ether25 ab Werk gebridged; ohne explizites Lösen aus
der Bridge verwarf RouterOS die generierten Forward-Isolationsregeln
selbst als ungültig ("in/out-interface matcher not possible when
interface is slave - use master instead"), live an `ether4` bestätigt;
`DhcpServerCommandBuilder` stellt seitdem ein `/interface bridge port
remove [find interface=<name>]` voran (übersprungen für "bridge"
selbst). **Ein geleertes Textfeld (z.B. Kommentar) blieb nach dem
Speichern unverändert bestehen** (Bug 25) — `pendingCommand` filterte
alle leeren `formValues` grundsätzlich aus den Argumenten heraus, RouterOS'
`set` ändert aber nur explizit übergebene Parameter, ein weggelassener
bleibt unangetastet statt geleert. Fix: ein Feld bleibt im Argument-Set,
wenn es vorher einen Wert hatte (jetzt als `feld=""` explizit geleert);
zusätzlich musste `RouterOSCommand`s SSH-Zeilen-Rendering einen leeren
Wert als `""` statt als nacktes `feld=` ausgeben. **Bearbeiten einer
Route (z.B. nur den Kommentar ändern) scheiterte mit "bad parameter
immediate-gw"** (Bug 26) — RouterOS liefert bei `/ip route print` u.a.
das berechnete, nur lesbare Feld `immediate-gw` mit; da dieses Feld im
Route-Schema nicht kuratiert ist, landete es unverändert in den freien
"Weitere Parameter" und wurde bei *jedem* Speichern blind mitgeschickt —
RouterOS lehnt `immediate-gw` beim `set` als ungültigen Parameter ab und
bricht dadurch die komplette Änderung ab, auch wenn nur ein unrelated
Feld wie der Kommentar geändert wurde. Fix: ein unkuratiertes Feld wird
nur noch mitgeschickt, wenn sein Wert sich gegenüber dem ursprünglich
geladenen Item tatsächlich geändert hat (oder neu hinzugefügt wurde) —
betrifft nicht nur Routen, sondern jedes Menü mit berechneten/nur
lesbaren Feldern im generischen Lese-Pfad. **Direkt beim Nachtesten
derselbe Effekt nochmal, diesmal bei einem kuratierten Feld** (Bug 27) —
eine dynamische/verbundene Route (automatisch angelegt durch eine
IP-Adresse auf dem Interface) hat `distance=0`; das *unveränderte*
Zurücksenden dieses ganz normal kuratierten Feldes beim Ändern des
Kommentars scheiterte mit "value of distance out of range (1...255)" —
RouterOS akzeptiert `distance=0` nur für System-verwaltete Routen, nicht
als Eingabewert für `set`. Zeigt: das "nur bei Änderung senden"-Prinzip
aus Bug 26 galt bisher nur für unkuratierte Felder, kuratierte Felder
wurden weiterhin immer komplett neu gesendet. Fix generalisiert:
`pendingCommand` sendet jetzt für **jedes** Feld (kuratiert wie
unkuratiert) beim Bearbeiten eines bestehenden Items nur noch den
Diff gegen das ursprünglich geladene Item — deckt implizit auch Bug 25
(Leeren eines Feldes) mit ab, der Spezialfall dafür wurde entfernt.
Nebenbei: die neue `ExpertViewModelTests.swift` lief anfangs gar nicht
mit, weil `xcodegen generate` nach dem Anlegen der Datei nicht erneut
lief — Xcodegen erzeugt die `sources`-Dateiliste einmalig beim
Generieren, eine neu angelegte Datei taucht im generierten `.xcodeproj`
nicht automatisch auf. **Merksatz: nach jeder neu angelegten Datei
`xcodegen generate` erneut laufen lassen, nicht nur nach Datei-Edits.**
**Auch nach dem Bug-27-Fix scheiterte dieselbe Route noch, jetzt mit
"no such item (4)"** (Bug 28) — der `/ip route print detail`-Gegencheck
bestätigte: die Route ist dynamisch (`D`-Flag, `distance=0`, automatisch
angelegt durch die IP-Adresse auf `bridge`). RouterOS' eigenes
"dynamic"-Flag steht nicht zuverlässig in `print terse` (dasselbe
Problem schon bei DHCP-Leases dokumentiert, siehe `LanDevice.swift`),
aber `distance=0` ist ein verlässliches Signal — keine echte statische
Route kann das haben (RouterOS erzwingt 1255 bei `set`). Fix:
Übersicht-Tab bietet für Routen mit `distance=0` gar kein "Bearbeiten"
mehr an (`editTarget=nil`) — die `.id` einer dynamischen Route ist
ohnehin nicht stabil, RouterOS kann sie jederzeit neu anlegen. 59
Unit-Tests grün (neuer Test `OverviewGraphTests.
testDynamicRouteHasNoEditTarget`). **Übersicht-Tab aktualisierte sich
nicht selbst nach Tab-Wechsel** (Bug 29) — `.task` lief nur einmal pro
View-Lebenszeit; da SwiftUI Tab-Inhalte beim Wechsel auf macOS nicht neu
erzeugt (die View bleibt am Leben), feuerte `.task` beim Zurückwechseln
nie erneut. Fix: `.onAppear` statt `.task` (feuert bei jedem
Sichtbarwerden des Tabs neu) — Änderungen aus dem Experte-Tab (z.B. eine
neu angelegte Route) erscheinen jetzt automatisch beim Reinschauen, ohne
manuellen "Aktualisieren"-Klick.
**Verbindungs-Label "Optionen" im Detailpanel unklar, Zeile nicht
anklickbar** — Nutzerfrage nach Klick auf eine IP-Adresse: was bedeutet
"Optionen" bei der verknüpften DHCP-Netzwerk-Zeile? Umbenannt zu
"DHCP-Optionen"; die "Verbindungen"-Zeilen im Detailpanel (`NodeDetailView`)
sind jetzt anklickbar (springt zum verbundenen Knoten, wie schon bei
Linien-Klicks) und zeigen beim Hovern einen Tooltip mit der
Klartext-Erklärung (`OverviewStyle.explanation(for:)`, bereits für
`EdgeDetailView` gebaut, hier wiederverwendet).
## `xcodebuild test` hängt — Gatekeeper, kein Code-Bug
`xcodebuild ... test` kann exakt beim App-Start für ~5 Minuten hängen,
dann `"The test runner hung before establishing connection."`. Ursache
(per `log show`, Filter auf `syspolicyd`/`amfid`): ad-hoc-signierte
Binaries lösen bei `syspolicyd` einen Live-Netzwerk-Check
(`GK performScan`) gegen Apples Gatekeeper-Server aus; jeder Neu-Build
ändert den Binary-Hash und kann einen neuen, manchmal langsamen Check
auslösen. Betrifft nur den CLI-Weg — Xcodes Cmd+R-Weg bekommt vom
Debugger-Start eine Gatekeeper-Ausnahme. **Deshalb:** bei Hängern erst
`xcodebuild ... build-for-testing` (kompiliert nur, kein App-Start, kein
Gatekeeper-Trigger) für den Compile-Check nutzen, echte Testläufe/manuelle
Verifikation dem Nutzer über Xcode überlassen statt CLI-Versuche zu
wiederholen.
## Bekannte Einschränkungen (bewusst, nicht vergessen)
- **Restore nur für dasselbe Routermodell, RouterOS erzwingt das selbst
nicht** — die App blockt bei erkanntem Modell-Mismatch (per Recherche
bestätigt: RouterOS importiert eine Fremd-Modell-Config anstandslos,
potenziell mit falsch zugeordneten Interfaces). Schutz greift nur, wenn
die Backup-Datei die `# model = ...`-Kopfzeile hat (RouterOS ≥ mind.
6.49.5) — bei sehr alten/handbearbeiteten Dateien ohne diese Zeile
läuft der Restore ungeprüft durch (Dialog weist explizit auf
"unbekannt" hin, blockt aber nicht).
- **Restore-Skript-Datei bleibt nach der Wiederherstellung auf dem
Router liegen** (`flash/routerosassistant-restore.rsc`) — kein
automatisches Aufräumen, weil der Router direkt im Zuge des Befehls
neu startet und die Verbindung dabei abbricht; ein erneutes Verbinden
nur zum Löschen dieser einen Datei erschien nicht aufwandsgerecht.
Harmlos (überschreibt sich beim nächsten Restore selbst), aber bewusst
in Kauf genommen, nicht vergessen.
- ~~REST-Pfad ungetestet für Schreibvorgänge~~ — am 2026-09-16 erstmals
live getestet: `www-ssl` auf dem hEX-Testrouter aktiviert (eigene CA
`local-ca` + davon signiertes `rest-cert` angelegt, da direktes
Selbstsignieren ohne separate CA mit "failure: CA not found"
scheiterte — RouterOS braucht dafür `key-usage=key-cert-sign,crl-sign`
auf einem eigenen CA-Zertifikat, das per `ca=<name>` referenziert
wird). Dabei **drei echte, vorher unentdeckte Bugs gefunden und
gefixt**, alle drei ausschließlich möglich, weil kein Testgerät vorher
je `www-ssl` an hatte:
- **Bug 30:** `RestTransport.baseURL` fehlte der abschließende `/`
(`".../rest"` statt `".../rest/"`). `URL(string:relativeTo:)`
ersetzt bei relativer Auflösung das *letzte Pfadsegment* der Basis
(RFC 3986 §5.3) — ohne trailing slash ist "rest" selbst das letzte
Segment, ein Pfad wie "system/resource" ersetzt es also statt
danach angehängt zu werden (`.../rest` + "system/resource" →
`.../system/resource`, 404). Jede REST-Verbindung scheiterte dadurch
mit stillschweigend verschlucktem "HTTP 404" (der generische
`catch`-Zweig in `ConnectionService.connect` loggte den Fehler
nirgends), fiel auf SSH zurück, ohne je den Zertifikats-Dialog zu
zeigen — sah aus wie "kein Zertifikat-Prompt trotz neuem
Zertifikat", war tatsächlich ein komplett anderer, tieferliegender
Bug. Gefunden über einen temporären Debug-`print` im Fallback-Pfad.
- **Bug 31:** `RestTransport.apply()`s `.add`-Fall nutzte `POST`,
RouterOS' REST-API erwartet dafür aber `PUT``POST` auf
`/rest/ip/firewall/address-list` lieferte `{"detail":"no such
command","error":400,"message":"Bad Request"}`, `PUT` mit
identischem Body `201` mit vollem Echo des neuen Items. Nebenbei
`send()`s Fehlerbehandlung verbessert: zeigte bisher nur den nackten
Statuscode ("HTTP 400"), jetzt wird RouterOS' eigener JSON-Fehler-
Body (`detail`-Feld) mit ausgegeben, falls vorhanden — genau dieser
Body war es, der den Bug überhaupt auffindbar machte (per direktem
`curl`-Replay derselben Anfrage).
- **Bug 32, am folgenreichsten:** RouterOS sendet auf *jede*
`/rest/...`-Antwort `Cache-Control: max-age=31536000` (ein Jahr) +
passenden `Expires`-Header — vermutlich ein Blanket-Default für
statische Web-UI-Assets, der auch auf die REST-API durchschlägt.
`URLSession(configuration: .ephemeral)` bedeutet nur "nichts auf
Platte persistieren", hält aber weiterhin einen Im-Speicher-
`URLCache` und hält sich an diese Header — die *erste* GET-Antwort
auf einen Pfad (z.B. eine leere Artikelliste, abgerufen durch
`loadCrossReferenceOptions` oder ein früheres Öffnen desselben
Menüs) wurde für den Rest der App-Laufzeit unverändert zurückgegeben,
komplett unabhängig von späteren `.add`/`.set`/`.remove`-Änderungen.
Live-Symptom: ein frisch angelegter Adress-Listen-Eintrag (per
`curl` als real bestätigt) erschien nie in der Experte-Tab-Liste,
auch nach Weg-und-zurück-Navigation nicht. Fix: `urlCache = nil` +
`.reloadIgnoringLocalCacheData` auf Session- *und* Request-Ebene
(RouterOS' Schreib-Endpunkte sind nicht idempotent genug, um sich
nur auf eine der beiden Ebenen zu verlassen).
Nach allen drei Fixes live durchgespielt: Anlegen (`PUT`), Bearbeiten
inkl. Feld-Leeren (`PATCH`, Kommentar gesetzt dann wieder gelöscht —
REST-seitig direkt per `curl` verifiziert, dass ein leerer String das
Feld tatsächlich löscht, genau wie bei SSH/Bug 25), Löschen (`DELETE`)
— alle drei vom Nutzer bestätigt ("funktioniert"). Damit ist der
REST-Transport erstmals end-to-end gegen echte Hardware verifiziert,
nicht mehr nur gegen Mocks. Der REST-Zweig von `fetchFieldValues`
(M12/Bug 14, Query-String-Filter `?feld=wert` auf einem GET) bleibt
weiterhin unverifiziert — dabei nicht auf den Weg gekreuzt.
- **Bug 33, direkt danach gefunden:** Löschen eines Eintrags im
Experte-Tab aktualisierte die sichtbare Liste nicht, bis der Nutzer
in einen anderen Tab und zurück wechselte — sah zunächst wieder wie
ein Cache-/Refresh-Problem aus, war es aber nicht: ein temporärer
Debug-`print` in `confirmRemoval` bestätigte `items.count` geht
korrekt von 1 auf 0 nach `reloadItems()`, das `@Published`-Modell
war also die ganze Zeit richtig. Reines SwiftUI-Render-Problem:
Anlegen/Bearbeiten laufen über `ExpertItemEditView` als `.sheet(item:)`,
dessen Schließen `ExpertMenuDetailView` automatisch neu mountet —
genau das erzwingt implizit den Refresh, den Löschen (über ein
direkt an `ExpertMenuDetailView` gehängtes `.confirmationDialog`,
kein Sheet, kein Remount) nie bekam. Fix: `.id(viewModel.items.count)`
auf der Einträge-`Section`, zwingt SwiftUI bei jeder Mengenänderung
zu einer frischen View-Identität statt sich auf In-Place-Diffing zu
verlassen. Live bestätigt ("funktioniert").
- **`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.
**Nachtrag 2026-09-16:** live an `/ip address` (3 Einträge), `/ip route`
(4 Einträge) und erneut `/ip dhcp-server lease` (2 Einträge) geprüft
(`:put [<Pfad> find]` gegen einzeln per Filter ermittelte echte `.id`s
je Zeile verglichen) — in allen drei Fällen stimmte die Positions-
Reihenfolge diesmal exakt. Kein erneuter Fehlgriff reproduzierbar,
**aber das entkräftet Bug 15 nicht**: RouterOS dokumentiert nirgends,
dass die Reihenfolge von `:put [<Pfad> find]` und `print terse` stabil
identisch ist, und beide werden als zwei getrennte SSH-Befehle
nacheinander abgesetzt. Arbeitshypothese, unbestätigt: bei `/ip
dhcp-server lease` handelt es sich um eine reine Timing-Race — die
Lease-Tabelle ändert sich laufend durch verbindende/trennende Geräte,
eine Umsortierung zwischen den beiden Befehlen (statt einer grundsätzlich
falschen Zuordnung) würde erklären, warum Bug 15 dort auftrat, bei den
heute getesteten, weitgehend statischen Menüs (Adressen, Routen) aber
nicht. Für statische Menüs also vermutlich unkritisch, für Menüs mit
häufig wechselnden dynamischen Einträgen (DHCP-Leases, Verbindungs-
Tracking-Tabellen, falls je generisch angebunden) weiterhin als riskant
einstufen.
- **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
echten `/interface wireless`-Interface noch nie live gelaufen.
- ~~Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut
bestätigt~~ — erledigt: beim M8-App-Test (siehe oben) lief ein voller
`FirewallConfig`-Apply mit `ether1` als WAN erneut durch,
`/ip firewall filter print` zeigte alle Basisregeln (established/
related, invalid-drop, ICMP-Accept, WAN-Drop) korrekt mit `ether1`.
- **Neuer RouterOS-WiFi-Treiber (`/interface wifi`, wifiwave2/802.11ax)
nicht unterstützt** — wird erkannt und im UI erklärt, aber nicht
konfiguriert.
- **Kein garantiertes Auto-Rollback** bei Verbindungsabbruch während
"Jetzt anwenden" — nur Backup-vorher + Bestätigungspflicht.
- **App ist ad-hoc signiert, nicht notarisiert** — beim ersten Start einer
frisch nach `/Applications` kopierten Version zeigt macOS die
"nicht verifizierter Entwickler"-Warnung (Rechtsklick → Öffnen nötig).
- ~~SSH-Hostkey-TOFU ungetestet~~ — **bestätigt gegen echte Hardware**:
erste Verbindung zeigte den "Unbekannter SSH-Schlüssel"-Dialog, nach
Bestätigen + Trennen + erneutem Verbinden lief es ohne Rückfrage durch.
- **Experte-Tool (M10): mehrere kuratierte Bereiche noch nicht gegen
Hardware bestätigt** — WLAN (Legacy- und neuer wifiwave2-Treiber:
Testgerät hat keinen WLAN-Chip), Bonding und PPPoE-Client (beide riskant
für die eigene Erreichbarkeit/brauchen echte ISP-Zugangsdaten) nur gegen
RouterOS-Dokumentation kuratiert, nicht live geprüft. `/ip address`,
`/ip dhcp-client`, `/ip dns`, `/ip service` nur lesend geprüft (Feldnamen
bestätigt), keine Schreibtests — Risiko, sich selbst auszusperren
(SSH/REST-Dienst) bzw. die WAN-Verbindung zu kappen.
- **Experte-Tool: einige RouterOS-Bereiche bewusst nur generisch, nicht
kuratiert** — Hotspot, Logging (beide mehrteilig, kein flaches Schema),
CAPsMAN, L2TP-/OpenVPN-Server, OSPF/BGP-Routing. Grund: keine verifizierte
Quelle für die genauen Feldnamen, lieber generischer Schlüssel/Wert-
Zugriff als geratene Felder (siehe Bug 7 oben, warum das eine berechtigte
Sorge ist).
## M8: Mehrere LAN-Interfaces + Netzwerk-Isolation
Auf Nutzerwunsch: mehrere physische LAN-Interfaces je mit eigenem
DHCP-Server (`SetupViewModel.lanConfigs: [LanDhcpConfig]`, analog zum
schon vorhandenen VLAN-Listen-Muster), plus ein Isolation-Toggle pro
LAN-/VLAN-Eintrag ("Von anderen Netzwerken isolieren"). Bei aktivem
Firewall-Grundschutz erzeugt `FirewallConfig` daraus paarweise
Forward-Drop-Regeln zwischen jedem isolierten Netzwerk und allen anderen
konfigurierten Netzwerken (beide Richtungen, Pair-Dedup bei gegenseitiger
Isolation). Internetzugriff bleibt weiterhin global über die bestehende
WAN-NAT-Logik, nicht pro Netzwerk abschaltbar (bewusst außerhalb Scope).
Nebenbei behoben: `VlanStepView` behauptete vorher im Hilfetext bereits
Isolation vom Hauptnetzwerk, ohne dass eine einzige Firewall-Regel das
durchsetzte — reiner Text ohne Wirkung. Jetzt ist Isolation ein echter,
optionaler Schalter mit tatsächlicher Regel-Erzeugung.
**Live gegen Hardware verifiziert (2026-09-15)** — zuerst manuell per
SSH nachgebaut (siehe "Zum Sessionende" oben), danach über den
App-Wizard selbst (`ether4`, Experte-Modus). iMac ↔ Laptop erreichen
sich über die Netzgrenze nicht mehr, Internet + DNS funktionieren auf
beiden Seiten weiter. Dabei drei App-Bugs gefunden und gefixt (Bug
2224, siehe Bug-Liste oben) — alle drei Voraussetzung dafür, dass ein
per Wizard eingerichtetes isoliertes Netz auf einem bereits
konfigurierten bzw. werksseitig gebridgten Router überhaupt funktioniert:
fehlende "LAN"-Interface-Listen-Mitgliedschaft (DNS zum Router blockiert),
fehlende Idempotenz bei erneutem Wizard-Lauf (Apply brach sofort ab),
und ein nie aus der Bridge gelöstes physisches Interface (RouterOS
verwarf die Isolationsregeln selbst als ungültig). Weiterhin unverändert
seit vorher: `FirewallConfigTests` (isoliertes Netzwerk gegen
nicht-isoliertes, gegenseitige Isolation, keine Isolation).
## M9: Einfach/Experte-Modus im Einrichten-Wizard
Nutzerwunsch: zwei Modi statt einem Wizard-Durchlauf für alle. Neuer
erster Schritt `SetupStep.mode` (`ModeStepView`, Segmented Picker).
- **Einfach**: WAN → LAN (nur ein Netzwerk, kein Hinzufügen/Entfernen,
kein Isolations-Toggle) → WLAN → Firewall (Grundschutz fest an, kein
Toggle) → Review. VLAN-Schritt wird komplett übersprungen.
- **Experte**: unveränderter voller Flow (Multi-LAN, VLAN, Isolation,
Firewall-Toggle) — das, was vorher der einzige Wizard-Pfad war.
Beim Umschalten auf Einfach werden evtl. im Expertenmodus gesetzte Werte
zurückgesetzt (`SetupViewModel.applySimpleModeConstraints`). Navigation
(`goNext`/`goBack`) von linearem `SetupStep.rawValue+1` auf expliziten
`switch` umgebaut (nötig wegen bedingtem VLAN-Skip). Nur gegen Unit-Tests
+ Compile/Test-Build verifiziert, **nicht gegen echte Hardware** (der
Live-Test dieser Session lief komplett über direktes SSH, nicht über die
App-UI).
## M10: Experte-Tab — generischer RouterOS-Konfigurationszugriff
Nutzerwunsch: "Zugriff auf alle Funktionen, was RouterOS bietet ...
alles was du findest, der komplette Umfang". RouterOS hat 300+ Menüs
(siehe `/console inspect` / CLI-Referenz) — statt für jedes einzelne ein
Swift-Modell zu bauen (unrealistisch), zweistufige Architektur:
1. **Generischer Motor** (deckt technisch jeden Menüpfad ab):
`RouterOSMenuItem` + `RouterOSCliParser.parseGenericItems`/
`parseSingletonItem`, `RouterOSCommand.remove` (neu, neben add/set),
`ConnectionService.fetchMenuItems` + Transport-Implementierungen.
Im UI: freies "eigener Menüpfad"-Eingabefeld — jeder RouterOS-Pfad ist
erreichbar, auch ohne kuratiertes Schema, dann als rohes
Schlüssel/Wert-Formular.
2. **Kuratierte Schemas** (`RouterOSSchemaCatalog`) mit echten
Formularfeldern, Tooltips, Erklärtext und Warnhinweisen: die komplette
Firewall-Familie (Filter/NAT/Mangle/Raw/Adress-Listen — der explizit
genannte Schwerpunkt), dazu Interfaces (Bridge, VLAN, WireGuard+Peers,
PPPoE-Client, Bonding), IP (Adressen, Pools, DHCP-Server+Netzwerk,
DHCP-Client, DNS, Verwaltungsdienste, Routen), VPN (PPP-Secret/Profile),
WLAN (Legacy + neuer wifiwave2-Treiber + Sicherheitsprofile), Queues
(Simple/Tree), System (Identity/Clock/NTP/Scheduler/Skripte/Nutzer),
Werkzeuge (Netwatch/E-Mail). Bewusst nicht kuratiert (siehe oben):
Hotspot, Logging, CAPsMAN, L2TP-/OVPN-Server, OSPF/BGP.
**Live gegen den hEX-Testrouter verifiziert** (Add→Print→Set→Remove-
Zyklus je Familie, Router danach nachweislich unverändert — Config-Export
vorher/nachher byte-identisch bis auf Zeitstempel): Firewall
Filter/NAT/Mangle/Raw/Adress-Listen, VLAN, WireGuard+Peers, IP-Pool,
DHCP-Server+Netzwerk, statische Route, PPP-Secret/Profile, Queue Simple,
Scheduler+Skript, Netwatch, sowie der neue Singleton-Pfad
(`/system identity`). Dabei zwei Bugs gefunden und gefixt, siehe Bug 7/8
oben. **Nicht** gegen Hardware getestet: WLAN (kein Chip im Testgerät),
Bonding, PPPoE-Client (beide riskant/brauchen echte Zugangsdaten),
Schreibzugriff auf `/ip address`/`/ip dhcp-client`/`/ip dns`/
`/ip service` (nur lesend geprüft, Risiko Selbstaussperrung/WAN-Ausfall).
**Nachtrag (Nutzer-Feedback-Runde, gleiche Session):** Nutzer meldete zwei
reale Probleme beim gedanklichen/App-Testen des DHCP-Server-Formulars —
Bug 9 (Adress-Pool-Feld ohne Auswahl) und Bug 10 (fehlgeschlagenes Anlegen
ohne jede Fehlermeldung). Beide gefunden und gefixt (siehe oben). Bug 10s
Fix wurde erneut end-to-end live bestätigt: komplette Kette
VLAN → IP-Adresse → Pool → DHCP-Server → DHCP-Netzwerk auf einem freien
Interface (statt des zuvor kollidierenden `ether5`) angelegt, kein
Fehlertext (weil tatsächlich erfolgreich), anschließend vollständig
wieder entfernt.
**Zweiter Nachtrag — jetzt tatsächlich durch die App-UI getestet (Nutzer
selbst, gleiche Session):** Nutzer legte VLAN + DHCP-Server über den
Experte-Tab live an, fand dabei drei weitere reale Bugs (11: yes/no,
12: fehlende Lösch-Buttons, 13: eingefrorene Interface-Auswahl, siehe
oben) — alle gefunden, gefixt, nach jedem Fix Release neu gebaut und
deployt, vom Nutzer direkt in der App erneut getestet. Damit ist die
SwiftUI-Oberfläche des Experte-Tabs (VLAN anlegen, einem DHCP-Server
zuweisen, Adress-Pool wählen, löschen) **erstmals tatsächlich manuell
durchgeklickt und vom Nutzer als funktionierend bestätigt** — nicht mehr
nur die zugrundeliegende RouterOS-Syntax per SSH. Bestätigt: RouterOS
erlaubt nur einen DHCP-Server pro Interface — der Versuch, ihn direkt auf
`ether5` zu setzen, scheiterte zurecht (dort hängt bereits `dhcp5` aus
einer früheren Session), auf dem VLAN-Interface funktioniert es.
**Dritter Nachtrag — Sicherheits-Angleichung ans übrige App-Verhalten:**
Nutzer fragte, ob Experte-Tab-Änderungen sofort geschrieben werden oder
eine Bestätigung braucht — Antwort war "sofort, kein eigener
Bestätigungs-Button" (nur Löschen hatte einen Dialog). Das wich vom
Einrichten-Wizard ab (Übersicht + Pflicht-Backup vor "Jetzt anwenden").
Auf Wunsch angeglichen:
- **Bestätigungsdialog vor Anlegen/Ändern**: "Anlegen"/"Speichern" öffnet
jetzt zuerst einen `.confirmationDialog` mit dem exakten Befehl, erst
ein zweiter Klick führt ihn aus (`ExpertMenuDetailView`).
- **Auto-Backup einmal pro Verbindung**: `ConnectionService.
hasExpertToolBackedUpThisSession` (zurückgesetzt bei `disconnect()`),
`ExpertViewModel.ensureSessionBackup()` zieht vor dem allerersten
Schreibvorgang im Experte-Tab automatisch ein Backup (wie der Wizard),
aber nur einmal pro Sitzung, nicht vor jeder einzelnen Änderung (ein
Experte-Tab-Durchgang sind oft viele kleine Edits hintereinander).
**Vierter Nachtrag — Dauer-Editor:** Nutzer fragte, in welcher Einheit
die DHCP-Server-Lease-Zeit angegeben wird. Live geprüft: RouterOS-
Zeitwerte sind Zahl+Einheit (`s`/`m`/`h`/`d`/`w`, kombinierbar wie
`1d12h30m`), eine bare Zahl gilt als Sekunden (bestätigt: `90` → intern
`1m30s`). Nutzerwunsch danach: "ein sinnvoll gestaltetes Auswahlfenster
... ein Tooltip wäre hilfreich" statt nur Tooltip-Text. Neuer Feldtyp
`RouterOSFieldSchema.Kind.duration` + `DurationFieldEditor` (vier
Stepper: Tage/Std/Min/Sek, parst bestehende Werte beim Öffnen, schreibt
beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
"Lease-Zeit" und Adress-Listen "Ablaufzeit". Nutzer bestätigte danach:
"funktioniert".
## M15: Übersicht-Tab — Diagramm-Fixes + Direktes Bearbeiten
Auf Nutzerwunsch zwei Themen in einer Session: erst mehrere echte Bugs im
bestehenden Übersicht-Diagramm gefunden und gefixt, danach eine neue
Fähigkeit ergänzt — ein Knoten direkt anklicken und bearbeiten, statt nur
anzusehen.
**Diagramm-Fixes** (alle live an "Verbindungslinien werden links
abgeschnitten" / "Hover springt ständig" diagnostiziert):
- Erst versucht, das Diagramm beim Öffnen automatisch ans Fenster
anzupassen (`GeometryReader` + einmalig gesetzter Scale) — hat den
ScrollView-Scrollzustand durcheinandergebracht (Inhalt verschob sich
nach dem ersten Layout, oberer Teil nicht mehr erreichbar). **Wieder
komplett zurückgezogen**, zurück zur ursprünglichen manuellen
Zoom-Steuerung (`-`/`100%`/`+`).
- **Echter Geometrie-Bug in `EdgesCanvas`:** VLAN-/Bridge-Port-Kanten
laufen innerhalb derselben Spalte (Interface → Interface), eine
Firewall/NAT-Kante läuft rückwärts (rechts nach links) — die
Kurven-Berechnung nahm aber immer "rechts aus dem Start-, links in den
Zielknoten" an, wodurch die Kontrollpunkte bei diesen Fällen weit über
den Canvas hinausschossen (auch ins Negative) und am linken Rand
abgeschnitten wurden. Fix: Kurvenaufbau jetzt richtungsabhängig
(gleiche Spalte → kleine Ausbuchtung rechts, rückwärts → gespiegelte
S-Kurve).
- **Hover-Instabilität:** Mehrere Kanten verlassen denselben Knotenpunkt
am selben Punkt — dort sind sie fast gleich nah am Cursor, kleinste
Mausbewegung ließ die Auswahl ständig springen. Fix in zwei Schritten:
(1) Trägheit — einmal gehoverte Kante bleibt aktiv, solange der Cursor
nah genug bleibt, statt bei jedem Event neu die global nächste zu
wählen; (2) Hover-Kandidaten auf die gerade hervorgehobenen
(verbundenen) Kanten beschränkt, statt gegen alle Kanten im Diagramm zu
prüfen.
- **Klick-vs-Hover-Priorität vertauscht:** `highlightNodeID` ließ Hover
über Klick gewinnen — beim Nachfahren einer Linie zu ihrem Ziel
streifte der Cursor oft andere, unbeteiligte Karten, deren Hover dann
die Hervorhebung des angeklickten Knotens verdrängte. Fix: Klick-
Auswahl gewinnt jetzt über Hover (Hover dient nur noch als Vorschau,
solange nichts ausgewählt ist).
- Verbindungslinie anklicken zeigt jetzt ausführliche, verständliche
Infos im rechten Panel (`EdgeDetailView`: Klartext-Erklärung pro
Verbindungsart, "Von"/"Nach" mit Sprung zum jeweiligen Knoten) — vorher
gab es nur den kleinen Hover-Tooltip mit den bloßen Rohdaten.
**Direktes Bearbeiten:** `OverviewNode.EditTarget` (Menüpfad, REST-Pfad,
RouterOS-eigene `.id`) an jedem Knoten, der sich sauber auf genau ein
Router-Item abbildet — IP-Adressen, Pools, DHCP-Server/-Netzwerke/
-Clients, Routen, Firewall-Filter-/NAT-Regeln, WireGuard-Peers. Klick auf
"Bearbeiten" im Detailpanel öffnet dieselbe `ExpertItemEditView`-Sheet
wie der Experte-Tab (dafür `private` entfernt, jetzt modulweit
sichtbar), über eine eigene `ExpertViewModel`-Instanz — gleiche Felder/
Tooltips, gleicher Bestätigungsdialog, gleicher Schreibpfad, keine
Duplikation der Schema-/Write-Logik. Nach dem Speichern lädt die
Übersicht automatisch neu. Bewusst nicht abgedeckt: physische/VLAN/
Bridge-Interfaces (Felder über mehrere typ-spezifische Menüs verteilt)
und Adress-Listen-Knoten (fassen mehrere Einträge zusammen, kein 1:1-
Bezug zu einem Router-Item).
**Live gegen Hardware verifiziert** — Nutzer bestätigte Kommentar-
Änderung an einer Firewall-Regel über den neuen Bearbeiten-Weg
("funktioniert"). Direkt danach beim Weitertesten vier echte Bugs
gefunden (Bug 2528 — siehe Bug-Liste oben): geleertes Textfeld blieb
bestehen, Route-Bearbeiten scheiterte erst an einem nur-lesbaren Feld
(`immediate-gw`), dann am unveränderten `distance=0` einer dynamischen
Route, schließlich an deren instabiler `.id` ("no such item"). Bug
2527 betreffen `ExpertViewModel.pendingCommand` und damit **auch den
Experte-Tab direkt**, nicht nur den neuen Übersicht-Weg — gefixt
(vereinheitlicht: nur geänderte Felder werden beim Bearbeiten gesendet,
kuratiert wie unkuratiert). Bug 28 gefixt in `OverviewViewModel`: Routen
mit `distance=0` (dynamisch) bieten gar kein "Bearbeiten" mehr an. 59
Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und
`OverviewGraphTests.testEditableNodesCarryTheirRouterOSMenuAndItemID`/
`testDynamicRouteHasNoEditTarget`).
## Stand der Milestones
- ✅ M1M4: Projektgerüst, Connect, Backup, WAN/LAN/DHCP, VLAN — gegen
echtes Testgerät verifiziert.
- ✅ M5: WLAN-Schritt — nur "kein WLAN"-Zweig verifiziert, `.set`-Pfad
weiterhin ungetestet (kein WLAN-Chip auf beiden Testgeräten bisher).
- ✅ M6: Firewall-Schritt — live angewendet, dabei Bug 6 gefunden+gefixt;
Interface-Auswahl danach erneut bestätigt, Regel-Ergebnis mit korrektem
WAN-Port aber noch nicht erneut kontrolliert.
- ✅ Zusatzfeatures nach M6: Tooltips auf allen Konfigurationsfeldern,
wählbarer Backup-Ordner (Standard: `~/Library/Application Support/...`),
Schnell-Backup-Button im Verbinden-Tab, "Werkseinstellungen
wiederherstellen" (Gefahrenzone im Sicherungen-Tab,
`/system reset-configuration no-defaults=no`), eigenes App-Icon
("Signal Router"-Motiv).
- 🔶 M7: Härtung — SSH-Hostkey-TOFU **fertig, gegen echte Hardware
bestätigt** (inkl. neuem "Trennen"-Button im Verbinden-Tab, der dafür
nötig wurde). REST-Schreibpfad gegen `www-ssl` seit 2026-09-16 verifiziert
(Bug 3032). Dabei zusätzlich Bug 37 gefixt: REST-Erstverbindungen
etablieren jetzt automatisch auch SSH-Trust im Hintergrund, statt dass
dedizierte SSH-Dienste (Backup u.a.) beim ersten Zugriff mit einem
unbestätigbaren Hostkey-Fehler dead-enden (live bestätigt, "passt").
Verbleibend: REST-Fehlerzustände bei Verbindungsabbruch mitten im Apply
noch nicht gezielt geprüft.
- ✅ M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
(eigene Firewall-Regeln pro LAN/VLAN) — **live gegen Hardware
verifiziert**, sowohl manuell (SSH) als auch über den App-Wizard
selbst (`ether4`), dabei drei App-Bugs gefunden+gefixt (Bug 2224,
siehe oben). Isolation, DNS und Internet vom Nutzer am echten Gerät
bestätigt.
- 🔶 M9: Einfach/Experte-Modus im Einrichten-Wizard — gebaut, Compile/
Unit-Test-verifiziert. UI (Modusumschalter selbst) noch nicht manuell
durchgeklickt — nur M10s Experte-Tab wurde das (siehe M10).
- ✅ M10: Experte-Tab (generischer RouterOS-Zugriff + kuratierte Firewall-
und weitere Schemas) — Kern-Logik **live gegen Hardware verifiziert**,
UI (VLAN anlegen/löschen, DHCP-Server mit Adress-Pool zuweisen) **vom
Nutzer selbst in der laufenden App bestätigt funktionierend** — sieben
reale Bugs dabei gefunden und gefixt (Bug 713), danach zusätzlich
Bestätigungsdialog vor Anlegen/Ändern + einmaliges Auto-Backup pro
Sitzung (Angleichung an den Wizard) sowie ein Dauer-Editor (Tage/Std/
Min/Sek statt Freitext) für Lease-/Ablaufzeit-Felder ergänzt, alles vom
Nutzer bestätigt. Weiterhin ungetestet: WLAN (kein Chip im Testgerät),
Bonding, PPPoE-Client (beide riskant/brauchen echte Zugangsdaten),
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**, vom
Nutzer bestätigt ("das gefällt mir schon sehr gut"). War anfangs rein
lesend, seit M15 kann ein Knoten direkt bearbeitet werden (siehe
unten) — deckt weiterhin 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,
"Feste Zuweisung entfernen" für den Rückweg) — **live gegen Hardware
verifiziert und vom Nutzer bestätigt funktionierend** ("das funktioniert
jetzt super" / "das hat sofort funktioniert"), nach vier gefundenen und
gefixten Bugs beim Hinweg (Bug 1417, siehe oben) — die aufwendigste
Live-Verifikation aller Milestones bisher, mehrere Diagnose-Runden mit
Rohdaten-Vergleichen und direkten Router-Gegenchecks (SSH-CLI + Winbox).
Rückweg ("Feste Zuweisung entfernen") lief dagegen beim ersten Versuch
fehlerfrei — RouterOS-Doku vorher per Recherche-Agent geprüft (kein
`make-dynamic`, nur `remove` + Client muss neu verbinden), nicht
geraten. Nutzer bestätigte den vollen Kreislauf (fest zuweisen →
entfernen → Kabel/WLAN trennen+verbinden → wieder dynamisch).
- ✅ M13: Backup-Wiederherstellung — `BackupService.restoreBackup`
(SFTP-Upload des `.rsc`-Skripts + RouterOS' dokumentierter
`reset-configuration no-defaults=yes run-after-reset=...`-Restore-Weg),
Modell-Abgleich vor dem Bestätigungsdialog (`/system routerboard`s
`model`-Feld gegen die `# model = ...`-Kopfzeile der Sicherung, blockt
bei Mismatch komplett), Gefahrenzone-UI mit explizitem Brick-Warnhinweis
im Sicherungen-Tab. **Live gegen Hardware verifiziert** — erster
Testlauf sperrte den Router komplett aus (Bug 18: keine Passwörter im
Export + `no-defaults=yes` löscht auch den Werks-Admin), Fix
(Login-Erhalt vorangestellt) danach vom Nutzer bestätigt ("das hat
funktioniert"). Restore-Weg selbst (Modell-Match, SFTP-Upload,
reset+run-after-reset) lief dagegen schon im ersten Versuch fehlerfrei.
- ✅ M14: Update-Check im Verbinden-Tab — Routerboard-Vollinfo
(`/system routerboard`: Modell/Revision/Seriennummer/Firmware-Typ/
Firmware-Versionen), RouterOS-Software-Update-Check
(`/system package update`: Kanal/installiert/neueste Version/Status,
"Jetzt prüfen"+"Update installieren"), Routerboard-Firmware-Update
("Firmware aktualisieren") und "Jetzt neu starten" danach. **Alle vier
Aktionen live gegen Hardware verifiziert**, inkl. der zuvor offenen
Frage, ob `/system routerboard upgrade`s normalerweise interaktive
Bestätigung ("Do you really want to upgrade firmware? [y/n]") den
nicht-interaktiven SSH-Weg dieser App blockiert — tut es nicht,
Firmware-Update lief beim ersten Versuch sauber durch ("Firmware
upgraded successfully, please reboot..."), ebenso der Neustart danach.
Dabei ein weiterer Bug im generischen Lese-Pfad gefunden (Bug 20).
Anschließend Design-Durchgang: Verbinden-Detailseite/Sicherungen/
Geräte von nackter `List` auf `Form`+`.formStyle(.grouped)`
(macOS-typischer "Karten"-Look) umgestellt, damit einheitlich mit
Wizard/Experte-Tab. Dark Mode auf Nachfrage geprüft und ohne
Codeänderung bestätigt funktionierend (keine feste Farbwerte/
Appearance-Überschreibung im Code).
- ✅ M15: Übersicht-Tab — mehrere Diagramm-Bugs gefixt (Verbindungslinien
am linken Rand abgeschnitten bei gleicher-Spalte-/Rückwärts-Kanten,
nervöses Hover, Klick-Auswahl wurde von Hover unbeteiligter Karten
verdrängt) plus neue Fähigkeit: Knoten (IP-Adresse, Pool, DHCP-Server/
-Netzwerk/-Client, Route, Firewall-Filter-/NAT-Regel, WireGuard-Peer)
direkt über dieselbe Sheet wie im Experte-Tab bearbeiten und
zurückschreiben. Dabei vier Bugs gefunden+gefixt (Bug 2528): geleertes
Feld blieb bestehen statt geleert zu werden, Route-Bearbeiten
scheiterte an einem nur-lesbaren Feld (`immediate-gw`), dann am
unveränderten `distance=0` einer dynamischen Route, schließlich an
deren instabiler `.id`. Bug 2527 betreffen auch den Experte-Tab
direkt, Bug 28 ist Übersicht-spezifisch (dynamische Routen bieten kein
"Bearbeiten" mehr an). **Live gegen Hardware verifiziert** — Nutzer
bestätigte Kommentar-Änderung an einer Firewall-Regel über den neuen
Weg
("funktioniert").
- ✅ M16: Zweisprachigkeit (DE/EN) mit manuellem Umschalt-Button —
begonnen im Experte-Tab (App-weite Fortsetzung Tab für Tab in
Folgesessions geplant, wie mit dem Nutzer vereinbart). Erster Ansatz
(Apple String Catalog `Localizable.xcstrings` + `.environment(\.locale,
...)` auf `WindowGroup`-Ebene) **live widerlegt**: der Umschalter
änderte selbst nach explizitem `LocalizedStringKey(...)`-Umschreiben
jedes betroffenen `Text`/`.help`/`Section`/Feld-Aufrufs nichts am
angezeigten Text ("bleibt auf deutsch") — `.environment(\.locale)`
steuert offenbar nur Calendar-/DateFormatter-artige APIs, nicht welche
Sprachtabelle `Text(LocalizedStringKey)` zur Laufzeit auflöst (das
scheint an Bundle-interner Locale-Verhandlung beim Start festzuhängen).
Fix: `Localizable.xcstrings` komplett entfernt, stattdessen eigener
`RouterOSAssistant/Core/Localization/L10n.swift` mit
`L10n.t(german, appLanguage) -> String` (Dictionary-Lookup, deutscher
Text als Schlüssel, Rückgabe des deutschen Texts bei fehlender
Übersetzung oder `appLanguage == "de"`). Jede betroffene View liest
`@AppStorage("appLanguage")` selbst (kein Environment-Umweg mehr) und
ruft `L10n.t(...)` direkt an jeder Text-/Tooltip-Stelle auf, sodass
SwiftUI die Abhängigkeit beim Umschalten sieht und neu rendert. Bisher
übersetzt: alle 6 Tab-Namen, alle 13 `RouterOSMenuCategory`-Werte, die
komplette Chrome von `ExpertView.swift`/`ExpertMenuDetailView.swift`
(Buttons, Dialoge, Platzhalter, Feld-Labels/-Hilfetexte). **Build +
60 Tests grün, vom Nutzer live in Xcode bestätigt** ("das sieht gut
aus"). Direkt danach den kompletten restlichen Katalog nachgezogen:
alle 9 Menü-Familien in `RouterOSSchemaCatalog.swift`
(Firewall/Interfaces/IP/Routing/VPN/WLAN/Queues/System/Werkzeuge) —
jeder `displayName`, jede `explanation`/`warning`, jedes Feld-Label
und jeder Hilfetext hat jetzt einen Eintrag in `L10n.swift`s
`translations`-Dictionary (376 Einträge). Beim Zusammenstellen auf
zwei Dictionary-Key-Kollisionen geachtet (Swift-Dictionary-Literale
crashen bei doppelten Schlüsseln zur Laufzeit statt beim Compilieren):
"Deaktiviert" und "Nur zur eigenen Wiedererkennung." kamen in zwei
Katalog-Familien mit identischem deutschen Text vor, jeweils den
zweiten Eintrag entfernt (dieselbe Übersetzung greift ohnehin für
beide Stellen). Build + 60 Tests grün, **vom Nutzer live in Xcode
bestätigt** ("funktioniert"). Damit ist der komplette Experte-Tab
zweisprachig. Direkt danach denselben Mechanismus auf den Verbinden-
Tab (`ConnectView.swift`) ausgerollt: Verbindungsformular,
Verbindungsstatus (inkl. dynamischem "Verbunden (REST-API/SSH)"),
Geräte-/Routerboard-Infos, Software-/Firmware-Update-Bereich samt
aller Bestätigungsdialoge, Zertifikats-/SSH-Hostkey-TOFU-Warnungen.
Da `ConnectView` mehrere Nachrichten mit eingebetteten Laufzeitwerten
baut (Fingerabdruck, Router-Version, Datum der letzten Sicherung),
wurden diese in übersetzte statische Textteile + unverändert
eingefügten dynamischen Wert zerlegt statt als ein Dictionary-Key mit
Platzhalter (z.B. `L10n.t("Fingerabdruck:", appLanguage) +
" \(fingerprint)"`). Die vier `View`-Extension-Hilfsfunktionen
(`withUpdateDialogs`/`withSoftwareUpdateDialogs`/
`withFirmwareUpgradeDialogs`/`withRebootDialogs`) bekamen dafür einen
zusätzlichen `appLanguage: String`-Parameter, da sie außerhalb der
View-Struct liegen und nicht direkt auf `@AppStorage` zugreifen
können. Nebenbei eine Lücke im Experte-Tab-Katalog geschlossen: das
Feld-Label "Passwort" (in fünf verschiedenen Schemas verwendet) hatte
bisher keinen eigenen Übersetzungseintrag. `L10n.swift`s
`translations`-Dictionary jetzt 444 Einträge, auf Duplikate per
Python-Skript geprüft (keine gefunden). Build + 60 Tests grün, **vom
Nutzer live in Xcode bestätigt** ("sieht gut aus"). Bewusst nicht
übersetzt: Laufzeit-/Backend-Text, der direkt von RouterOS oder dem
App-eigenen Fehler-/Ergebnis-Pfad kommt (z.B. `update.status`,
`firmwareUpgradeResultMessage`, alle `viewModel.*Error`-Alerts) — das
sind technische/englische RouterOS-Ausgaben bzw. app-generierte
Fehlertexte, keine kuratierten UI-Strings; offen bleiben die übrigen
vier Tabs (Einrichten/Übersicht/Geräte/Sicherungen), die noch auf
festen deutschen String-Literalen laufen.
- ✅ M17: "Bekannte Router" im Verbinden-Tab — Nutzerwunsch: "mehrere
Logins hinterlegen können, quasi eine Liste bekannter Router". Neue
`SavedRouter`/`SavedRoutersStore` (JSON-Array in UserDefaults, kein
eigenes Verzeichnis wie bei Backups nötig) merkt sich Host+Benutzername
nach jeder erfolgreichen Verbindung, mit einem beim ersten Mal
automatisch gesetzten Anzeigenamen — `RouterDeviceInfo.boardName`
(RouterOS' "board-name", die Marketing-Bezeichnung wie "hEX", die
bereits beim Verbinden geladen wird), als nächstliegende Auslegung von
"werksmäßige Bezeichnung". Der Name bleibt bei späteren Verbindungen
zum selben Host/Benutzernamen unverändert, auch wenn sich die erkannte
Modellbezeichnung ändern sollte — nur explizites Umbenennen überschreibt
ihn. Passwörter bleiben unverändert im macOS-Schlüsselbund (`Keychain-
Service`, Schlüssel "Benutzername@Host"), diese Liste speichert nur
Host/Benutzername/Anzeigename/letzte Verbindungszeit. Klick auf einen
Eintrag füllt Host/Benutzername/Passwort ins Formular, ohne sofort zu
verbinden; ein "…"-Menü pro Eintrag bietet Umbenennen (inline) und
Entfernen. **Live bestätigt** ("ok, funktioniert jetzt") — allerdings
erst im zweiten Anlauf: die Liste füllt sich nur bei einer *neuen*
erfolgreichen Verbindung nach dem Rebuild, eine bereits laufende
Alt-Verbindung zählt nicht rückwirkend — das war der erste
Verwirrungspunkt beim Testen ("ich sehe 'Bekannte Router' nicht").
74 Tests grün (5 neue `SavedRoutersStoreTests` + 5 neue
`ConnectionServiceTests`-Ergänzungen für die parallel gebaute
Port-Konflikt-Prüfung, siehe unten).
- **Bugfix, live gefunden:** Umbenennen tat nichts — das TextField steckte
innerhalb eines `Button(action: onSelect)`, dessen `.disabled(isRenaming)`
auch das TextField selbst deaktivierte. Fix: kein umschließender Button
mehr, `.onTapGesture` übernimmt die Zeilen-Auswahl, TextField bekommt
eigenen `@FocusState`. **Live bestätigt** ("funktioniert").
- **Erweiterung, Nutzerwunsch:** freies Standort-Feld (Etage/Raum/Zweck)
pro Eintrag, "damit macht die Zuordnung bei mehreren Geräten einfacher".
`SavedRouter.location: String` (leer=nicht gesetzt), eigene
`SavedRoutersStore.updateLocation(_:to:)`. Menüpunkt "Umbenennen" wurde
zu "Bearbeiten" (öffnet Name + Standort gemeinsam). Rückwärtskompatibles
Decoding (`init(from:)` mit `decodeIfPresent`) für bereits gespeicherte
Listen ohne `location`-Feld. 76 Tests grün (2 neue). **Live bestätigt**
("funktioniert").
- **Erweiterung, Nutzerwunsch:** Liste auf ca. 4 sichtbare Einträge
gedeckelt (eigene `ScrollView` mit `maxHeight`-Cap statt das ganze
Formular wachsen zu lassen) — "die Liste der gespeicherten Router soll
nicht zu lang werden". Noch nicht separat live bestätigt (kein
ausdrückliches Feedback, aber auch kein Einwand in den folgenden
Test-Runden).
**Gleichzeitig gebaut, noch nicht live bestätigt** (nur Build+Unit-Tests
grün, wartet auf Test durch den Nutzer in Xcode):
- **LAN-Port-Konflikt-Prüfung im Einrichten-Assistenten** —
Nutzerwunsch: vor dem Anlegen eines neuen LAN prüfen, ob der gewählte
Port frei ist (nicht in einer Bridge, keine bestehende IP-Adresse, kein
WAN-DHCP-Client/PPPoE), und bei einem Konflikt mit doppelter
Sicherheitsbestätigung nachfragen, bevor die App den Port selbständig
freimacht. Neu: `PortConflict`-Modell (Core/Models), `ConnectionService.
checkPortConflict(interfaceName:)` (liest `/interface bridge port`,
`/ip address`, `/ip dhcp-client`, `/interface pppoe-client` parallel
per `async let`), `SetupViewModel.checkPortConflict(for:)`/
`acknowledgePortConflict(for:)`/`hasUnresolvedPortConflict(for:)`,
`LanStepView`s neue `PortConflictWarningView` mit zwei aufeinander
folgenden `.confirmationDialog`s (Konsequenzen erklären, dann "Wirklich
sicher?"). Tatsächlich ausgeführt wird die Port-Freigabe erst beim
finalen "Jetzt anwenden" im Review-Schritt, nicht sofort bei der
Bestätigung. "Weiter" bleibt gesperrt, bis jeder Konflikt entweder
bestätigt oder durch Wahl eines anderen Ports vermieden wurde.
- **"Fertig"-Button nach erfolgreichem Anwenden** — vorher gab es nach
"Jetzt anwenden" nur einen (weiterhin aktiven) "Zurück"-Button und den
jetzt dauerhaft deaktivierten "Jetzt anwenden"-Button, keinen klaren
Abschluss. `SetupViewModel.finish()` setzt den Assistenten komplett
zurück (frische Default-Werte, `prepareDefaults` erneut gegen die
aktuellen Live-Interfaces) für einen sauberen nächsten Durchlauf.
- ✅ M18: Live-Traffic-Anzeige im Verbinden-Tab — Nutzerwunsch: der Punkt
vor jedem Interface soll erkennen lassen, ob es "aktuell in Verwendung
ist und Daten überträgt", nicht nur ob der Link steht. Neue
`InterfaceTraffic`/`InterfaceTrafficMonitor` (Actor, eigene dedizierte
SSH-Verbindung unabhängig von REST/SSH-Hauptverbindung — RouterOS' `/
interface monitor-traffic` ist ein reiner CLI-Befehl ohne REST-
Äquivalent, gleiche Begründung wie bei `BackupService`/`UpdateService`),
pollt alle 3s pro Interface. `ConnectViewModel.startTrafficPolling(...)`/
`stopTrafficPolling()`, angestoßen über `.onChange(of: connectionService.
state)`. Punkt: grau = kein Link, grün (fest) = Link aber keine Daten,
grün pulsierend = überträgt gerade tatsächlich Daten.
- **Zwei reale Bugs beim ersten Live-Test gefunden** ("alle buttons
sind grau, keine Animation") — auf Bitte des Nutzers zwei Diagnose-
Befehle direkt am Router ausgeführt statt zu raten:
1. `RouterOSCliParser.parseInterfaces` suchte `running=`/`disabled=`
als `key=value`-Paare — die gibt es in echtem `/interface print
terse`-Output gar nicht. RouterOS codiert das stattdessen als
Buchstaben-Flags vor dem ersten Feld (`"0 R name=ether1 ..."`,
`"2 S name=ether3 ..."`: R=running, X=disabled, S=Bridge-Slave)
— **jedes Interface las `running` bisher immer als `false`**, ein
vorbestehender, nie zuvor aufgefallener Bug (nicht durch M18
verursacht, nur durch M18 erstmals sichtbar geworden). Fix: neue
`flagsColumn(of:)`-Hilfsfunktion isoliert den Flag-Bereich vor dem
ersten "=", prüft dort auf "R"/"X" statt auf nicht existierende
Schlüssel.
2. `/interface monitor-traffic ... once` liefert Werte wie
`"50.7kbps"` (mit Einheit und Dezimalpunkt), keine reine Zahl —
`Int(...)` scheiterte daran lautlos zu 0. Fix: `SSHTransport.
parseBitsPerSecond(_:)` erkennt Gbps/Mbps/kbps/bps-Suffixe
(längere Suffixe zuerst geprüft, da "kbps" selbst auf "bps"
endet).
- 82 Tests grün (2 neue Parser-Tests mit dem exakten vom Nutzer
eingefügten Live-Output + 5 neue Bits-pro-Sekunde-Tests). **Live
bestätigt** ("sehr gut").
- ✅ M19: Übersicht-Tab — animierte Flussrichtung + verschiebbare Knoten.
- **Animierte Flussrichtung** (Nutzerwunsch: "die Linien im Übersicht-
Tab auch animiert... mit einer animierten Flußrichtung"): alle
Verbindungslinien laufen jetzt gestrichelt mit über die Zeit
wandernder `dashPhase` in Richtung "von → nach". Technisch über
`TimelineView(.animation)` um den bestehenden `Canvas` gelegt — ein
reiner `@State`-Wert mit `withAnimation(.repeatForever)` hätte den
(immediate-mode) `Canvas` nicht laufend neu gezeichnet,
`TimelineView` ruft den Draw-Closure dagegen bei jedem Frame mit
frischem Datum neu auf.
- **Verschiebbare Knoten + Zurücksetzen-Button** (Nutzerwunsch: "kann
man die einzelnen Kästchen verschiebbar machen? ... Die Linien
sollten den Kästchen automatisch folgen. ein Button für den Reset
wäre super"): neuer `nodeOffsets`-State pro Knoten-ID plus ein
`@GestureState` für die gerade laufende Drag-Bewegung, kombiniert in
`effectivePositions` — sowohl Knoten-Karten als auch `EdgesCanvas`
zeichnen auf dieser verschobenen Position, Linien folgen live mit.
`.simultaneousGesture` (nicht `.gesture`) fürs Draggen, damit der
bestehende Klick-zum-Auswählen weiter funktioniert. Toolbar-Button
"Zurücksetzen" leert `nodeOffsets`, nur aktiv wenn etwas verschoben
wurde.
- **Zwei kleine UI-Nachbesserungen, beide live gefunden:** Der
Reset-Button erschien zunächst gar nicht (`Label` mit Text+Icon
braucht mehr Platz als die reinen Icon-Buttons davor, vermutlich
von macOS in die Toolbar-Overflow verschoben) — behoben durch
Icon-only, dann von Nutzer als "verwirrend, ähnlich wie Refresh"
zurückgemeldet — endgültig auf einen reinen Text-Button
"Zurücksetzen" umgestellt (wie der bereits funktionierende
"100%"-Button).
- 82 Tests grün (unverändert — reine SwiftUI-Interaktions-/Animations-
Logik ohne neue reine Parser-Funktionen). **Live bestätigt**
("das vierschieben funktioniert super, die Linien folgen auch" /
"ok, der button ist da und funktioniert" / "funktioniert").
- ✅ M20: LAN-Scanner-Umbau — Umbenennung, Netzwerk-Tools, mehrere kleine
UX-Nachbesserungen.
- **Tab umbenannt**: "Geräte" → "LAN-Scanner" (Nutzerwunsch, "oder
ähnliches"), Toolbar-Button "Aktualisieren" → "Neu scannen"
(macht exakt dasselbe — DHCP-Leases/ARP/Bridge-Host neu abfragen —,
Umbenennung passt nur besser zum "Scanner"-Namen).
- **Netzwerk-Tools-Untermenü** beim Rechtsklick auf ein Gerät:
Ping/Traceroute/DNS-Auflösung (nslookup) — alle drei vom Router aus
über eine neue dedizierte SSH-Verbindung (`NetworkToolsService`,
gleiches Muster wie `BackupService`/`InterfaceTrafficMonitor`, da
`/ping`/`/tool traceroute`/`/resolve` reine CLI-Befehle ohne REST-
Äquivalent sind). Ergebnis als Rohtext im Sheet gezeigt statt
geparst — das genaue Ausgabeformat dieser Tools ist nicht gegen
echte Hardware verifiziert, nach den Parser-Überraschungen bei M18
bewusst vorsichtig. Sicherheitsmaßnahme: IP/Hostname eines Geräts
stammen letztlich aus dessen eigener (potenziell manipulierbarer)
DHCP-Anfrage — vor dem Einsetzen in den RouterOS-Befehlsstring auf
ein sicheres Zeichen-Set geprüft (`NetworkToolsService.sanitized`),
da RouterOS' Konsole ";" als Befehlstrenner behandelt und ein
böswillig gesetzter Hostname sonst einen zweiten Befehl einschleusen
könnte.
- **Port-Scan** (Nutzerwunsch: "Offene Ports rot, geschlossene grün,
nicht erreichbare grau") — läuft bewusst NICHT über den Router
(RouterOS hat kein eingebautes Portscan-Tool), sondern direkt von
diesem Mac per `Network.framework`/`NWConnection` gegen ~19 gängige
TCP-Ports parallel. **Live-Bug gefunden und gefixt**: eine
verweigerte Verbindung (Port zu, TCP RST) meldet sich bei
`NWConnection` über den `.waiting`-Zustand, nicht über `.failed` —
der ursprüngliche Code prüfte nur `.failed`, wartete bei einer
Ablehnung also den vollen Timeout aus und meldete fälschlich
"nicht erreichbar" statt "geschlossen". Test hat das direkt gegen
einen echten (aber garantiert nicht belegten) Loopback-Port
aufgedeckt, kein Rätselraten nötig.
- **Warnhinweis "Feste IP-Adresse zuweisen?" erweitert** (Nutzerwunsch:
"der Hinweis, wie die Adresse wieder in dynamisch gemacht werden
kann") — nennt jetzt explizit den Rückweg (Rechtsklick → "Feste
Zuweisung entfernen" im selben Tab) und dass das Gerät danach die
Verbindung kurz trennen/neu aufbauen muss.
- **"Neu scannen"-Button prominenter gemacht** (Nutzerwunsch: "wird
übersehen") — `.buttonStyle(.borderedProminent)` statt eines reinen
Icon-Buttons.
- **DE/EN-Umschalter zeigt jetzt Landesflaggen** (🇬🇧/🇩🇪) statt
"EN"/"DE"-Text — zeigt die Flagge der Sprache, zu der ein Klick
wechselt, exakt dieselbe "switch to"-Logik wie vorher.
- 92 Tests grün (6 neue `PortScannerTests`, 6 neue
`NetworkToolsServiceTests` für die Zeichen-Validierung). **Alle
Teile live bestätigt** ("funktioniert" / "sieht gut aus und
funktioniert").
- Nebenbei: großer `.alert`/`.confirmationDialog`/`.sheet`-Modifier-
Stapel in `DevicesView.swift` löste erneut Bug 19 aus ("compiler is
unable to type-check this expression in reasonable time") — nach
demselben, bereits etablierten Muster in zwei `View`-Extension-
Funktionen aufgeteilt (`withStaticAssignmentDialogs`/
`withDeviceDetailSheets`).
- ✅ M21: `L10n.t(...)`-Zweisprachigkeit (Bug-19-Muster: M16 begann im
Experte-Tab, siehe oben) auf die restlichen vier Tabs ausgerollt —
Einrichten (alle sieben Wizard-Schritte:
Mode/Wan/Lan/Vlan/Wifi/Firewall/Review, inkl. `PortConflictWarningView`
als eigener Sub-Struct mit durchgereichtem `appLanguage`), Übersicht
(inkl. `EdgeTooltipView`/`EdgeDetailView`/`NodeDetailView`/`LegendView`),
LAN-Scanner, Sicherungen. `L10n.swift` um einen entsprechenden
Übersetzungsblock erweitert; Model-Layer-Strings (z.B.
`WanConnectionMode.label`, `OverviewNode.Category.rawValue`,
`OverviewEdgeKind.rawValue`) werden wie schon bei `RouterOSMenuCategory`
am Verwendungsort gewrappt, nicht in der Model-Datei selbst geändert.
Etablierte Konvention beibehalten: nur statische View-Strings werden
übersetzt, ViewModel-generierte Laufzeitstrings (Fehlermeldungen,
`command.summary`/`cliLine`, `applyLog`-Zeilen, Router-Live-Daten)
bleiben unübersetzt — dieselbe Grenze, die schon der Experte-Tab zieht.
Bug 19 (Modifier-Ketten-Typecheck-Timeout) trat dabei ein drittes Mal
in `DevicesView.swift` auf (zwei lange Confirmation-Dialog-Texte), Fix
wie gehabt: in eigene Funktionen (`staticAssignmentMessage`/
`staticRemovalMessage`) ausgelagert statt inline verkettet. Live
bestätigt ("funktioniert").
**Nutzerfrage, keine Umsetzung** (2026-09-15): ob es Sinn macht, weitere
Sprachen "per Plugin" zu importieren. Antwort: für 1-2 weitere feste
Sprachen passt das bestehende `L10n.swift`-Dictionary-Muster gut; ein
echtes ladbares Plugin-System wäre für dieses Nischen-Tool vermutlich
Overkill ohne konkrete Nachfrage. Nutzer bleibt vorerst bei DE/EN, hat
aber erwähnt, dass Spanisch (und evtl. Hindi/indischer Markt, unklar)
später dazukommen könnte, da MikroTik-Router in ärmeren Ländern
(Brasilien, spanischsprachige Länder) stark verbreitet sind — siehe
Claude-Memory `future-language-support.md` für Details.
**"Abbrechen"-Button im Einrichten-Tab ergänzt** (2026-09-16, Nutzerwunsch:
"mir fehlt im einrichte-tab (einfach und experte) noch ein
\"Abbrechen\"-Button") — vorher gab es keinen Weg aus dem Wizard außer
sich Schritt für Schritt per "Zurück" bis zu Modus vorzuklicken. Zentral
in `SetupView.swift` als Toolbar-Button gelöst (nicht in jedem der sieben
Schritt-Views einzeln dupliziert), sichtbar auf jedem Schritt in beiden
Modi, mit `.confirmationDialog` ("Einrichten abbrechen?" /
"Weiter ausfüllen") gegen versehentlichen Datenverlust bereits
eingegebener Werte, deaktiviert während `isApplying` (ein laufendes
Apply soll nicht mitten im Ablauf abreißen). `SetupViewModel.finish()`
in eine private `resetToInitialState()` plus zwei benannte Wrapper
(`finish()`/neues `cancel()`) aufgeteilt — gleicher Reset, aber je nach
Aufrufkontext (erfolgreicher Apply vs. Nutzer bricht ab) unter dem
passenden Namen lesbar. Live bestätigt ("sieht gut aus").
**M23: Experte-Tab-Sektionsüberschriften prominenter + einklappbar**
(2026-09-16, Nutzerwunsch: "die Sektionsüberschriften ... müssen
prominenter sein und eingefärbt, bitte größer machen und passend
farblich hinterlegen (darkmode beachten)", danach "die einzelnen
Sektion einklappbar machen"). Zwei Schritte:
1. `Section("...")`s Standard-Header (macOS' winzige graue Kapitälchen-
Beschriftung) durch eigenen `Text` ersetzt: größer/fett
(`.title3.bold()`), Hintergrund `Color.accentColor.opacity(0.18)`
statt eines festen RGB-Werts, damit Light/Dark Mode automatisch
passend kontrastieren — `.textCase(nil)` nötig, sonst erzwingt macOS
trotzdem wieder seine eigene Kapitälchen-Darstellung über dem
eigenen View.
2. Einklappbar gemacht — erster Versuch mit SwiftUIs eingebautem
`Section(isExpanded:)` (native Disclosure-Dreieck), vom Nutzer
sofort korrigiert: "der Pfeil sollte immer zu sehen sein und nicht
erst beim Hovern" — das native macOS-Sidebar-Dreieck ist genau wie
in Finder standardmäßig Hover-only, keine SwiftUI-Option dafür
vorgesehen. Fix: eigenes Chevron-Icon (`chevron.right`, per
`.rotationEffect` gedreht) direkt im selbstgebauten Header, ganzer
Header als `Button` klickbar; `Section(isExpanded:)`-API komplett
fallengelassen zugunsten eines `if isExpanded { ForEach(...) }`
innerhalb eines schlichten `Section { }`, gesteuert über eigenen
`@State`/Binding (`collapsedCategories: Set<RouterOSMenuCategory>`
für die neun Kategorien, `isCustomPathExpanded: Bool` für "Eigener
Menüpfad"). Auf Nutzerwunsch ("standardmäßig sollten beim Öffnen des
Experte-Tab alle Sektion zugeklappt sein") startet alles zugeklappt.
`.listStyle(.sidebar)` an der `List` ergänzt, für den nativen
macOS-Sidebar-Look. Live bestätigt ("sehr gut, bin sehr zufrieden").
**M24: LAN-Scanner — "Aktionen"-Button, Traffic-Monitor pro Port +
Sparkline, ARP-Auflösungsbug gefixt** (2026-09-16). Drei Nutzerwünsche
in einer Runde:
1. **"Aktionen"-Button statt Rechtsklick** ("der Rechtsklick ist nicht
eindeutig erkennbar oder intuitiv, verlager bitte alle Funktionen
des Rechtsklicks in den Button") — `.contextMenu` komplett entfernt,
derselbe `deviceMenu(for:)`-Inhalt (Feste IP zuweisen/entfernen,
Netzwerk-Tools, Rohdaten anzeigen) läuft jetzt über einen sichtbaren
`Menu`-Button ("•••", Label "Aktionen") hinter jeder Geräte-Zeile.
2. **Traffic-Monitor pro Port + Sparkline** ("ein Traffic-Monitor im
LAN-Scanner", danach "ein kleines Liniendiagramm der letzten 10
Sekunden ... pro Port") — pro-*Gerät* geht nicht ohne RouterOS-
seitige Queue-Trees pro MAC (Nutzer selbst dagegen entschieden:
"Traffic pro PORT statt pro Gerät"), daher denselben bereits
gebauten `InterfaceTrafficMonitor` (Verbinden-Tab) wiederverwendet —
`DevicesViewModel` bekam eigene `startTrafficPolling`/
`stopTrafficPolling` (3s-Takt, gleiches Muster wie
`ConnectViewModel`). Live-↓/↑-Zahl direkt neben jedem Port-Titel,
plus neuer `TrafficSample`-Typ (`Core/Models/InterfaceTraffic.swift`)
für eine nach echter Zeit (nicht fester Sample-Anzahl) auf 10
Sekunden getrimmte Historie, gerendert als `Chart`/`LineMark`
(Swift Charts, `TrafficSparkline` in `DevicesView.swift`) ohne
Achsen — reiner Trend-Blick, die genaue Zahl steht ja daneben.
3. **Bug 34, dabei live gefunden:** ein Laptop an `ether4` (Traffic
sichtbar aktiv) erschien trotzdem unter "Unbekannter Port". Ursache:
RouterOS' `/ip arp`-Tabelle hielt für dieselbe MAC-Adresse zwei
Zeilen gleichzeitig — eine korrekte (`interface=ether4,
status=reachable`) und eine veraltete (`interface=bridge,
status=failed`, Überbleibsel von vor einem Netzwerkwechsel des
Geräts). `DevicesViewModel.buildDevices` baute `arpInterfaceByMAC`
bisher per blindem Überschreiben der letzten gesehenen Zeile auf —
welche Zeile "gewann" hing von der Router-internen Tabellen-
Reihenfolge ab, nicht von Korrektheit. Fix: `status=reachable`
gewinnt jetzt immer gegen jeden anderen Status, bei Gleichstand ein
konkreter Port gegen "bridge". Zwei neue Regressionstests
(`testReachableArpRowWinsOverStaleFailedRowForSameMAC`/
`...RegardlessOfOrder`, letzterer prüft explizit, dass der Fix nicht
nur für die live beobachtete Reihenfolge zufällig funktioniert).
**Nebenbefund beim Debuggen von Punkt 2, kein Code-Bug, aber ein
Betriebshinweis:** der Traffic-Monitor zeigte zunächst dauerhaft nichts
an — `InterfaceTrafficMonitor.fetchTraffic` scheiterte still (`try?`)
mit `untrustedSSHHostKey`. Ursache: der Router hatte seit dem letzten
Werksreset (2026-09-15) einen neuen SSH-Host-Key, aber da die Haupt-
verbindung dieser Session durchgehend über REST lief (M22-Fix), kam der
bereits vorhandene "Unbekannter SSH-Schlüssel"-Bestätigungsdialog nie
zum Zug — der lief bisher nur im SSH-*Fallback*-Pfad von
`ConnectionService.connect`. Dedizierte SSH-Verbindungen (Backup,
Netzwerk-Tools, Traffic-Monitor, Update, Werksreset) haben *keinen*
eigenen UI-Bestätigungspfad für einen neuen Host-Key, scheitern bei
einem Mismatch einfach still. Behoben durch `www-ssl` kurz deaktiviert
(erzwingt den SSH-Fallback-Pfad in der Haupt-Verbindung), Nutzer hat
den neuen Fingerprint einmalig über den bestehenden Dialog bestätigt
(`SSHHostKeyTrustStore` ist ein einziger, über `UserDefaults.standard`
geteilter Speicher — einmal dort vertraut gilt für jede dedizierte
SSH-Verbindung mit), danach `www-ssl` wieder aktiviert. **Bleibt ein
echter, noch offener Schwachpunkt:** falls der SSH-Host-Key sich künftig
nochmal ändert (z.B. nach einem weiteren Werksreset) UND REST zu diesem
Zeitpunkt bereits verbunden ist, würden dieselben dedizierten Dienste
wieder wortlos nichts tun, ohne Hinweis auf die Ursache — siehe
"Nächste Schritte".
**M25: Einrichten-Wizard-Politur** (2026-09-16, mehrere kleine
Nutzerwünsche in einer Runde):
- **WAN-Schritt: "Zurück"-Button ergänzt** — fehlte seit jeher (nur
"Weiter"), in beiden Modi, fiel im Experte-Modus nur eher auf, da
dort mehr Schritte durchgeklickt werden. Live bestätigt.
- **"Abbrechen" (Einrichten) + "Jetzt sichern" (Sicherungen) prominent
gemacht** — dieselbe `.buttonStyle(.borderedProminent)`-Behandlung
wie zuvor schon "Neu scannen" (Bug: "leicht zu übersehen"), jetzt
konsequent auf alle **einzelnen, gleichrangigen** Toolbar-Aktions-
buttons der App angewendet. Übersicht-Tabs Toolbar-Gruppe (Zoom/
Zurücksetzen/Aktualisieren, mehrere gleichrangige Buttons
nebeneinander) bewusst ausgenommen — dort würde durchgängiges
Prominent-Styling überladen wirken, ein anderes UI-Muster als "eine
einzelne leicht übersehene Hauptaktion".
- **LAN- und VLAN-Schritt: Adressfelder starten leer** (Nutzerwunsch:
"die auszufüllenden Felder leer lassen, nur Beispiele zeigen ...
so sieht man das Schema dahinter") — `LanDhcpConfig`/`VlanEntry`
hatten bisher konkrete Default-Werte (z.B. "192.168.88.1/24" bzw.
eine aus der VLAN-ID abgeleitete Adresse); da jedes TextField-Label
("Router-Adresse (z.B. 192.168.88.1/24)") ohnehin als Platzhalter
dient, sobald das Feld leer ist, versteckte ein vorbelegter Wert
dieses Beispiel bisher komplett. Betrifft beide Modi (LAN-Schritt
wird von Einfach und Experte geteilt). `interfaceName` (Picker) und
`leaseTimeHours` (Stepper) bleiben unverändert vorbelegt — beides
Auswahl-UI ohne "Beispiel tippen"-Konzept. Notwendige Begleitänderung:
`VlanStepView`s "Weiter" hatte bisher **keine** Validierungssperre
(unnötig, solange die Defaults immer gültig waren) — jetzt analog zum
längst vorhandenen `LanStepView`-Muster ergänzt, sonst hätte ein leer
gelassenes VLAN-Feld einen Befehl mit leeren Argumenten zum Router
geschickt. Zwei Tests entsprechend angepasst
(`testDefaultAddressesStartEmptyForExamplePlaceholder` ersetzt die
alte Ableitungs-Prüfung; `testLanDhcpCommandsCoverAddressPoolServerAndNetwork`
setzt die Adressfelder jetzt explizit statt sich auf Modell-Defaults
zu verlassen). Live bestätigt ("gut soweit").
**M26: "Bekannte Router" hinterlegt jetzt die Seriennummer**
(2026-09-16). Nutzer-Szenario, live so passiert: mit einem zweiten
Router desselben Modells verbunden (Werks-Adresse+Zugangsdaten
identisch zum bisherigen Testrouter, `192.168.88.1`/`admin`) — die App
hat daraufhin den *bestehenden* "Bekannte Router"-Eintrag des alten
Testrouters einfach für den neuen umbenannt (`SavedRoutersStore.
recordSuccessfulConnection` matchte bisher nur auf Host+Benutzername,
kein Feld unterschied zwei physisch verschiedene Geräte an derselben
Werks-Adresse). Klick auf den alten Eintrag verband danach zwangsläufig
mit dem tatsächlich erreichbaren Gerät (dem neuen Router), nicht mit
dem gemeinten alten. Fix (Nutzervorschlag: "ist es nicht besser die
Seriennummer des Gerätes mit zu hinterlegen?"): `SavedRouter` bekam ein
neues optionales `serialNumber`-Feld (`/system routerboard`s
"serial-number", bereits seit M14 als `RouterBoardInfo.serialNumber`
ausgelesen — "unbekannt" wird dabei wie `nil` behandelt, sonst würden
zwei Geräte ohne auslesbare Seriennummer fälschlich als "gleich"
gelten). `recordSuccessfulConnection` matcht jetzt gestuft: exakte
Seriennummer gewinnt zuerst; ein einzelner bestehender Eintrag ohne
gespeicherte Seriennummer (alte, vor diesem Feld gespeicherte Einträge,
oder Geräte ohne physisches Routerboard) wird beim nächsten Connect
einmalig nachträglich damit befüllt statt dupliziert; alles andere legt
einen neuen, getrennten Eintrag an. In `SavedRouterRow` als kleine
"SN: ..."-Zeile sichtbar gemacht, damit zwei gleich benannte/adressierte
Einträge auch optisch unterscheidbar sind. Drei neue Tests
(`testDifferentSerialAtSameHostUsernameCreatesSeparateEntry`,
`testLegacyEntryWithoutSerialGetsUpgradedInPlaceNotDuplicated`,
`testSameSerialReconnectUpdatesRecencyNotDuplicated`). Migrationshinweis
für den konkreten Fall: der bereits verschmolzene Bestandseintrag zeigt
aktuell die Seriennummer des neuen Routers — der nächste Connect zum
alten Testrouter legt automatisch einen neuen, eigenen Eintrag an (da
dessen Seriennummer nicht mehr passt), bei Bedarf danach umbenennen.
Live bestätigt ("passt, funktioniert") — allerdings dabei zwei weitere,
eng verwandte Bugs gefunden, siehe M27 direkt danach.
**M27: Passwort-Anzeige-Button, Schlüsselbund-Trennung, `terse`-Fallback
verallgemeinert** (2026-09-16, beim Live-Test von M26 gefunden):
1. **Passwort-Anzeige-Button** (Nutzerwunsch: "setze hinter das Passwort
eine Auge-Symbol zum aufdecken des Passwortes, so kann ich prüfen, ob
die Daten übernommen wurden") — Augen-Icon neben dem Passwortfeld im
Verbinden-Tab, schaltet zwischen `SecureField` und `TextField` um.
War das Diagnose-Werkzeug, das die beiden folgenden Bugs erst
aufdeckte.
2. **Bug 35: Schlüsselbund teilte sich einen Eintrag zwischen zwei
Routern** — beim Umschalten zwischen zwei "Bekannte Router"-Einträgen
(seit M26 korrekt als getrennte Einträge geführt) blieb das
angezeigte Passwort trotzdem identisch. Aufwendig eingegrenzt: ein
temporärer Debug-Print bestätigte zunächst, dass `ConnectViewModel`s
`@Published`-Werte bei jedem Klick korrekt gesetzt wurden (auch die
View selbst rendert nachweislich jedes Mal frisch, per zweitem
Debug-Print in `mainContent` bestätigt) — zwei SwiftUI-`.id()`-
Reparaturversuche (erst auf Section-, dann auf Feld-Ebene) liefen
deshalb ins Leere, weil das eigentliche Problem gar keins der
Anzeige war. Tatsächliche Ursache: `KeychainService` speichert
Passwörter nur nach `"username@host"` — zwei physisch verschiedene
Router mit identischem Host+Benutzername (Werks-Adresse) teilen sich
dadurch denselben Schlüsselbund-Eintrag, unabhängig davon, dass
`SavedRoutersStore` sie seit M26 bereits korrekt als getrennte
Einträge führt. Fix: `KeychainService.save`/`loadPassword`/
`deletePassword` bekommen einen optionalen `serialNumber`-Parameter,
der den Account-Key auf `"username@host#seriennummer"` erweitert;
`loadPassword` fällt auf den alten, unqualifizierten Key zurück,
wenn kein qualifizierter Eintrag existiert (Rückwärtskompatibilität
für vor diesem Fix gespeicherte Passwörter). `ConnectViewModel.
connect()`s Passwort-Speicherung musste dafür hinter den
erfolgreichen Verbindungsaufbau verschoben werden (die Seriennummer
ist vorher noch nicht bekannt). Die beiden zuvor eingebauten
`.id()`-Umwege wieder entfernt, da wirkungslos und irreführend
(falsche Fehlerursache im Kommentar).
3. **Bug 36, direkt danach beim selben Live-Test gefunden: neuer Router
zeigte gar keine Routerboard-Infos, keine Seriennummer** — derselbe
Root Cause wie der vorherige "Jetzt prüfen"-Fehler (`/system package
update`, siehe oben): `/system routerboard print ... terse` schlägt
auf diesem Router/dieser RouterOS-Version mit `"expected end of
command (line 1 column N)"` fehl statt mit dem bisher einzig
abgefangenen `"bad parameter terse"`. `fetchMenuItems`s
Singleton-Fallback (Bug 8/20) griff deshalb nicht,
`routerBoardInfo` blieb `nil` (best-effort, Fehler wurde still
verschluckt) — betraf gleichzeitig die Seriennummer (M26) und die
komplette Routerboard-Anzeige im Verbinden-Tab. Fix:
`SSHTransport.fetchMenuItems`s Erkennung in eine neue
`rejectsTerse(_:)`-Hilfsfunktion ausgelagert, die jetzt beide
bekannten Formulierungen abfängt. Sicher eingegrenzt, da die
geprüfte Fehlermeldung ausschließlich vom exakten Aufruf `"<menuPath>
print without-paging terse"` stammt — jeder Parserfehler dort betrifft
per Konstruktion das anhängende `"terse"`. Behebt nebenbei auch den
"Jetzt prüfen"-Fehler von vorhin (`/system package update`), da
beide über denselben Code-Pfad laufen. Live bestätigt
("passt, funktioniert").
## Nächste Schritte
1. ~~M16: restliche `RouterOSSchemaCatalog.swift`-Sektionen übersetzen~~,
~~Verbinden-Tab zweisprachig~~ und ~~`L10n.t(...)`-Umstellung auf die
übrigen vier Tabs (Einrichten/Übersicht/LAN-Scanner/Sicherungen)~~ —
alles erledigt, live bestätigt ("funktioniert"). Damit sind alle
fünf Tabs vollständig zweisprachig (DE/EN).
2. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
2. ~~Rest von M7: REST-Schreibtest an einem Gerät mit aktivem
`www-ssl`~~ — erledigt (2026-09-16), siehe "Bekannte Einschränkungen"
oben (Bug 3032). `www-ssl` bleibt auf dem hEX-Testrouter aktiv
(eigene CA `local-ca` + `rest-cert`), damit die App weiterhin
REST-zuerst verbindet statt SSH-Fallback. Fehlerzustände/Politur im
REST-Pfad noch nicht gezielt geprüft (z.B. Verbindungsabbruch
mitten im Apply) — optional für später.
3. M9 UI (Einfach/Experte-Modusumschalter im Einrichten-Tab selbst) noch
manuell durchklicken — M10s Experte-Tab wurde bereits vom Nutzer
bestätigt (siehe oben), der Moduswechsel im Wizard noch nicht.
4. M10: WLAN-Schemas (an Gerät mit WLAN-Chip), Bonding, PPPoE-Client
(mit echten oder Test-ISP-Zugangsdaten) noch gegen Hardware verifizieren.
5. ~~Dauer-Editor (`RouterOSFieldSchema.Kind.duration`) auch auf weitere
Zeitwert-Felder anwenden~~ — erledigt (2026-09-16): WireGuard-Peer
"Keepalive" (Label-Zusatz "(Sekunden)" entfernt, Stepper zeigt Einheit
selbst), Scheduler "Intervall", Netwatch "Prüf-Intervall" (Default
von `"00:00:10"` auf `"10s"` umgestellt — `DurationFieldEditor.parse`
versteht nur das Suffix-Format `<Zahl><d/h/m/s>` bzw. eine nackte
Sekundenzahl, kein `HH:MM:SS`; der alte Default wäre beim Öffnen
stillschweigend als "leer" geparst worden). Vor der Umstellung live
geprüft (Test-Einträge angelegt und sofort wieder entfernt): RouterOS
akzeptiert das Suffix-Format bei allen drei Feldern
(`persistent-keepalive=25s`, Scheduler-`interval=30s`,
Netwatch-`interval=30s`, jeweils im `print terse` bestätigt). Live in
der App bestätigt ("passt, live bestätigt"). Geräte-Tab hat weiterhin
keine Zeitfelder, daher nicht betroffen.
6. ~~Firewall-NAT-/Filter-`add`-Befehle sind bei wiederholtem Wizard-Lauf
weiterhin nicht idempotent~~ — behoben (2026-09-16): im Gegensatz zu den bei Bug 2224
gefixten Menüs (dort per `.set`-Retry auf einem eindeutigen Feld wie
`name`/`interface` gelöst) haben Firewall-Filter/NAT-Regeln kein
eindeutiges Identitätsfeld — "dieselbe Regel" heißt hier "identisches
Argument-Set". `SetupViewModel.apply(credentials:)` holt deshalb
einmalig (nicht pro Befehl) eine Live-Momentaufnahme der bestehenden
`/ip firewall filter`- und `/ip firewall nat`-Regeln
(`connectionService.fetchMenuItems`), und `isDuplicateFirewallRule`
vergleicht jeden geplanten `.add`-Befehl (ohne das rein schreibseitige
`"place-before"`-Argument, das nie ein gespeichertes RouterOS-Feld
ist) gegen diese Momentaufnahme — bei Treffer wird der Befehl
übersprungen und im Ablauf-Log als "bereits vorhanden, übersprungen"
vermerkt statt erneut ausgeführt. Einmalige Momentaufnahme statt
Nachfrage pro Befehl, damit in derselben Wizard-Ausführung neu
hinzugefügte Regeln (z.B. eine zweite Isolationsregel) nicht
versehentlich gegen sich selbst als Duplikat erkannt werden, aber
Regeln aus einem früheren Wizard-Lauf trotzdem erkannt werden.
Live bestätigt: kompletter Wizard mit aktivierter Firewall-Sektion
zweimal hintereinander gegen denselben Router angewendet, Filter-/
NAT-Regelanzahl blieb beim zweiten Durchlauf unverändert
("passt, anzahlen ändern sich nicht").
7. ~~`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
prüfen~~ — am 2026-09-16 live an `/ip address`/`/ip route`/erneut
`/ip dhcp-server lease` geprüft, keine erneute Fehlzuordnung
reproduzierbar (siehe "Bekannte Einschränkungen" oben für Details +
Timing-Race-Hypothese für dynamische Menüs). Damit fürs Erste
ausreichend untersucht — echte Behebung (z.B. `fetchMenuItems` auf
Einzel-Lookup statt Bulk-`find`+Positions-Zip umstellen) bleibt
offen, aber ohne aktuell reproduzierbaren Schadensfall nicht
dringend.
8. **Router wurde zwischen M12 und M13 vom Nutzer komplett auf
Werkseinstellungen zurückgesetzt** ("damit wir sauber weitermachen
können"), im Zuge des ersten M13-Testlaufs (Bug 18, Login-Sperre)
nochmal per Hardware-Reset, und am 2026-09-15 erneut vom Nutzer
manuell auf Werkseinstellungen zurückgesetzt ("ich räume den Router
mal auf - werksreset", danach bestätigt: "ist durch, funktioniert").
Jeder Config-Stand aus früheren Sessions (VLANs, Test-Pools/-DHCP-
Server, Interface-Umbenennungen wie `ether51`, angelegte Firewall-
Regeln/Routen, etc.) existiert dadurch **nicht mehr — nicht mehr
relevant, nicht danach suchen.** Vor Annahmen über den aktuellen
Stand immer neu per Geräte-/Übersicht-/Verbinden-Tab prüfen statt auf
ältere Einträge hier zu vertrauen.
9. ~~`fetchMenuItems`s Output-Text-Fallback (Bug 20) auf weitere
Singleton-Menüs prüfen~~ — am 2026-09-16 live an 12 Singleton-Menüs
geprüft (`/ip dns`, `/system identity`, `/system clock`,
`/system ntp client`, `/tool e-mail`, `/system routerboard`,
`/system resource`, `/system note`, `/system watchdog`,
`/ip settings`, `/ip cloud`, `/interface bridge settings`): alle
melden exakt denselben Text `"bad parameter terse (line 1 column
N)"` (Spaltennummer variiert, Substring immer gleich) — der
bestehende Check `output.contains("bad parameter terse")` fängt sie
alle ab, keine dritte, unbehandelte Fehlerform gefunden. Nebenbefund:
`/system health` ist **kein** Singleton, sondern liefert über
`terse` eine echte Liste (mehrere Sensor-Zeilen, z.B. Spannung/
Temperatur) — falls das je kuratiert wird, nicht als `isSingleton`
markieren. Damit fürs Erste ausreichend untersucht, kein Codefix
nötig.
10. ~~Ausführlichere Tooltips im Experte-Tab~~ — erledigt: alle
`RouterOSFieldSchema.help`-Texte in `RouterOSSchemaCatalog.swift`
überarbeitet, jedes Adress-/Netz-/Bereichs-Feld hat jetzt ein
konkretes Beispiel (z.B. "192.168.88.1/24" oder
"192.168.88.10-192.168.88.254"), vorher leere `help: ""`-Texte
gefüllt (u.a. bei Mangle/Raw-Regeln, den meisten "Deaktiviert"-
Feldern, WireGuard-Peer-Feldern, System-/Tool-Menüs). Dabei zwei
weitere Nutzerwünsche ergänzt: eine fette Überschrift (Menüname +
Kategorie) oben in `ExpertMenuDetailView`, da vorher nicht erkennbar
war, in welcher Sektion man sich befindet; und eine hellblaue
Hervorhebung des gerade geöffneten Eintrags in der linken Liste
(`ExpertView`) — dafür `.listRowBackground` auf macOS' Sidebar-Stil
verworfen (wird vom System überschrieben/nicht sichtbar), stattdessen
Hintergrund direkt am Zeileninhalt (`Text` in `RoundedRectangle`-
Fill) gesetzt. Alles vom Nutzer live in Xcode bestätigt ("passt
gut").
11. ~~M15-Bearbeiten-Funktion deckt Interfaces bewusst nicht ab~~ —
erledigt: jedes Interface hat jetzt ein `editTarget` — VLAN-
Interfaces über das kuratierte `/interface vlan`-Schema (mit der
eigenen `.id` aus `/interface vlan`, nicht der generischen
`/interface`-Liste), alle anderen Typen (Ethernet, Bridge, WLAN,
WireGuard) generisch über `/interface`. Dabei live einen echten
Stolperstein gefunden: der Nutzer wollte `ether5` zu `ether51`
umbenennen, hat aber (da für `/interface` bisher gar kein
kuratiertes Schema existierte, alles landete in "Weitere Parameter")
versehentlich `default-name` statt `name` geändert —
"bad parameter default-name". `default-name` ist RouterOS' Werksname
des Ports, nie änderbar; `name` das eigentliche, umbenennbare Feld,
beide standen unbeschriftet nebeneinander. Fix: `/interface` jetzt
mit kuratiertem Schema (`name`/`comment`/`disabled`), Tooltip auf
`name` weist explizit auf die Verwechslungsgefahr mit `default-name`
hin. Live bestätigt ("funktioniert"). Adress-Listen-Knoten bleiben
bewusst weiter nicht editierbar — fassen mehrere Einträge in einem
Knoten zusammen, kein 1:1-Bezug zu einem Router-Item, bräuchte eine
andere UI (Liste statt Formular).
12. ~~Übersicht-Tab: Interface-Klick hebt jetzt transitiv alle
zusammenhängenden Linien hervor, nicht nur die direkt anliegenden~~
— Nutzeridee: "wenn ich einen Interface-Button klicke, möchte ich
ALLE Linien sehen, die mit dem Interface irgendwie zusammenhängen".
Bisher galt für jeden Knotentyp dieselbe 1-Hop-Regel (nur Kanten, die
den geklickten Knoten selbst berühren) — bei einem Interface reißt
das z.B. die Kette "DHCP-Server → sein Pool" oder "IP-Adresse →
DHCP-Netzwerk-Optionen" ab, da diese Kanten nicht am Interface selbst
hängen. Fix: `OverviewGraph.highlightedNodeIDs(startingAt:)` — für
jeden Knotentyp außer Interface unverändert nur der eine Knoten
selbst, für ein Interface eine Breitensuche (BFS) über alle Kanten in
beide Richtungen, die den kompletten zusammenhängenden Ast einsammelt
(Interface → Adresse/DHCP-Server/Route/Firewall-Regel, und von dort
weiter → Pool bzw. DHCP-Netzwerk). Ausdrücklich nur für Interfaces so
gewollt — alle anderen Knotentypen bleiben bei der bisherigen 1-Hop-
Regel. Da ich die laufende App-GUI nicht selbst anklicken kann, die
BFS-Logik stattdessen isoliert in `OverviewGraphTests.swift` getestet
(`testHighlightedNodeIDsExpandsTransitivelyOnlyForInterfaces`):
baut einen Graphen mit Interface→Adresse→DHCP-Netzwerk und
Interface→DHCP-Server→Pool auf, bestätigt zunächst per Sanity-Check,
dass Pool und DHCP-Netzwerk tatsächlich zwei Hops entfernt sind (kein
direkter Edge zum Interface), und prüft dann, dass
`highlightedNodeIDs(startingAt:)` vom Interface aus alle fünf Knoten
liefert, aber vom Pool oder von der Adresse aus nur sich selbst. 61
Tests grün (60 + 1 neu). **Visuelle Live-Prüfung im laufenden
Diagramm** (2026-09-16): Interface-Klick (`bridge`) hebt die
komplette zusammenhängende Kette hervor (inkl. Pool und
DHCP-Netzwerk-Optionen zwei Hops entfernt), Klick auf Pool/Adresse
bleibt bei der 1-Hop-Regel — beides wie erwartet bestätigt
("passt, live bestätigt").
13. **Dedizierte SSH-Dienste scheitern still bei SSH-Host-Key-Mismatch,
solange REST verbindet** (siehe M24-Nebenbefund oben) —
`BackupService`/`NetworkToolsService`/`InterfaceTrafficMonitor`/
`UpdateService`/`FactoryResetService` haben keinen eigenen
UI-Bestätigungspfad für einen neuen SSH-Host-Key, nur
`ConnectionService.connect`s SSH-*Fallback* zeigt den Dialog — und
der wird nie erreicht, solange REST erfolgreich verbindet. Mögliche
Fixes: diese Dienste bei `untrustedSSHHostKey` einen eigenen
Bestätigungsdialog zeigen lassen, oder `ConnectionService` beim
Verbinden zusätzlich (nicht nur im Fallback-Fall) einmal den
SSH-Host-Key prüfen/bestätigen lassen, unabhängig davon, ob REST
erfolgreich war. **Tatsächlich so eingetreten (2026-09-16):** beim
ersten Experte-Tab-Schreibversuch einer neuen Sitzung (`ensureSessionBackup`,
läuft einmal pro Verbindung vor der ersten Änderung) schlug
`BackupService`s eigene, dedizierte SSH-Verbindung mit
"Unbekannter SSH-Schlüssel" fehl — sichtbar nur als roter
`applyError`-Text im Experte-Formular, ohne erkennbaren
Zusammenhang zum eigentlichen SSH-Host-Key-Problem. Der erste
Trust-Fix-Versuch von vorhin (M24-Nebenbefund) hatte sich entgegen
der Live-Bestätigung des Nutzers ("vertraut, verbunden per SSH")
**nicht** tatsächlich persistiert — `defaults read
com.focus72.RouterOSAssistant
RouterOSAssistant.TrustedSSHHostKeyFingerprints` zeigte einen
komplett fehlenden Schlüssel, obwohl der strukturell identische,
nachweislich funktionierende REST-Zertifikat-Trust
(`TrustedCertificateFingerprints`) im selben Preferences-Bereich
korrekt vorhanden war. Ursache dafür bleibt ungeklärt — der Code-Pfad
(`ConnectViewModel.trustSSHHostKeyAndRetry` →
`ConnectionService.trustCurrentSSHHostKeyAndRetry` →
`SSHHostKeyTrustStore.trust`) ist exakt symmetrisch zum
funktionierenden Zertifikat-Pfad, kein Unterschied im Code
gefunden. Fix diesmal per `www-ssl` erneut kurz deaktiviert,
Trust-Dialog erneut bestätigt, **und diesmal per `defaults read`
direkt verifiziert, dass der Fingerprint wirklich persistiert wurde**
(war er) — DHCP-Netzwerk-Anlage danach live erfolgreich. Falls
dieses Symptom nochmal auftritt: zuerst per `defaults read` prüfen,
ob der Trust wirklich gespeichert wurde, nicht nur der UI-Bestätigung
vertrauen. **Bug 37, richtig gefixt (2026-09-16):** genau dieses Symptom
erneut live reproduziert (neuer Testrouter, REST verbunden, erster
Wizard-Apply scheiterte mit "Unbekannter SSH-Schlüssel", reiner
`applyError`-Text ohne Bestätigungsmöglichkeit in `ReviewApplyView`).
Root Cause diesmal wirklich behoben statt nur umgangen: `ConnectionService.
connect` kehrt bei erfolgreichem REST-Connect sofort zurück, ohne je
einen SSH-Verbindungsversuch zu machen — der SSH-Host-Key dieses Hosts
wurde also nie geprüft/vertraut, `SSHHostKeyTrustStore` hatte schlicht
keinen Eintrag dafür. Dedizierte SSH-Dienste (`BackupService` u.a., s.o.)
sind aber der erste tatsächliche SSH-Kontakt zu diesem Host — treffen
dort auf einen komplett neuen, unbestätigten Schlüssel, aber `ReviewApplyView`/
`ExpertMenuDetailView` haben nur ein simples `.alert` mit "OK", keinen
Trust-Button. Fix: neues `ConnectionService.pendingSSHTrustFingerprint`
(getrennt von `state`) — nach jedem erfolgreichen REST-Connect prüft
`verifySSHTrust` einmalig per Wegwerf-`SSHTransport` den Host-Key im
Hintergrund; bei `untrustedSSHHostKey` wird der Fingerabdruck dort
abgelegt, `ConnectView` zeigt dafür denselben Trust-Dialog wie beim
reinen SSH-Fallback (`sshHostKeyFingerprint` liest jetzt beide Quellen),
Bestätigen ruft `trustPendingSSHHostKeyAndRetry()` (kein Reconnect nötig,
REST-Verbindung bleibt unberührt) statt `trustCurrentSSHHostKeyAndRetry`
(das reconnectet komplett neu, nur für den echten Fallback-Fall
gebraucht). Damit ist der Host-Key schon beim Verbinden bestätigt, lange
bevor Backup/Wizard-Apply/Experte-Tab ihn zum ersten Mal brauchen. Live
bestätigt ("passt") — REST-Connect zum Testrouter zeigte den Trust-Dialog
sofort, danach lief ein Wizard-Apply ohne den Dead-End-Fehler durch.
14. ~~M26 (Seriennummer bei "Bekannte Router") noch live testen~~ —
erledigt, siehe M27 oben (dabei zwei weitere Bugs gefunden und
gefixt: geteilter Schlüsselbund-Eintrag, `/system routerboard`s
`terse`-Ablehnung mit anderem Fehlertext als bisher erkannt).
Gitea-Remote `origin` ist eingerichtet und wird laufend gepusht (siehe
oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15.
`/Applications/RouterOS Assistant.app` ist der aktuell installierte
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.