# RouterOS Assistant — Handoff Stand: App-Icon fertig, alle Kern-Features (M1–M6) gebaut und mindestens teilweise gegen zwei echte physische Mikrotik-Testgeräte verifiziert (ein generisches Gerät für M1–M4, 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 7–13). 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, 14–17) 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=`, 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 ether2–5 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=]` 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 "/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" (16px–1024px) 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/` 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 [ 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 [ 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** — ` 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 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 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 ether2–5 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=]` 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 1–255 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=` 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 [ 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 [ 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 22–24, 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 25–28 — 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 25–27 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 - ✅ M1–M4: 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 30–32). 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 22–24, 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 7–13), 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 14–17, 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 25–28): 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 25–27 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` 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 `" 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 30–32). `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 `` 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 22–24 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.