forked from kay/RouterOS
Dedizierte SSH-Dienste (BackupService u.a.) sind der erste echte SSH-Kontakt zu einem per REST verbundenen Router und trafen dort auf einen unbestätigten Hostkey ohne Trust-UI (nur ReviewApplyView/ ExpertMenuDetailView-Fehlertext, kein Bestätigungsweg). Fix: nach jedem erfolgreichen REST-Connect prüft ConnectionService den SSH-Hostkey einmalig im Hintergrund und zeigt bei Bedarf denselben Trust-Dialog wie der SSH-Fallback, ohne die aktive REST-Verbindung neu aufzubauen. Live bestätigt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1848 lines
119 KiB
Markdown
1848 lines
119 KiB
Markdown
# 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=<name>`, beide toleriert falls schon vorhanden.
|
||
- **Bug 23:** ein voller Wizard-Durchlauf gegen einen bereits
|
||
konfigurierten Router brach beim zweiten Befehl ab
|
||
(`/ip address add address=192.168.88.1/24 interface=bridge` —
|
||
"already have such address"), weil nur WAN-DHCP-Client/PPPoE
|
||
idempotent behandelt wurden (Bug 21). Fix: `SetupViewModel.
|
||
applyIdempotently` erweitert — `/ip pool`/`/ip dhcp-server`/
|
||
`/ip dhcp-server network` werden bei Duplikat jetzt als `.set`
|
||
(gematcht auf name/name/address) erneut versucht, `/ip address` bei
|
||
exaktem Duplikat als bereits erledigt behandelt (ein Interface darf
|
||
mehrere Adressen halten, ein `.set` nach "interface" träfe sonst
|
||
potenziell die falsche).
|
||
- **Bug 24:** LAN-/VLAN-Schritt löste ein gewähltes physisches Interface
|
||
nie aus einer bestehenden Bridge — auf Werks-Routern sind 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=<name>]` voran
|
||
(übersprungen für "bridge" selbst), toleriert als bereits erledigt
|
||
falls nie gebridged.
|
||
|
||
Nach allen drei Fixes lief der komplette App-Wizard-Durchlauf für
|
||
`ether4` fehlerfrei durch (inkl. Neuanwendung der bereits vorhandenen
|
||
`bridge`/`ether1`-Konfiguration), Isolation + DNS + Internet vom Nutzer
|
||
am echten Gerät bestätigt. Alle 52 Unit-Tests grün. M8 damit als
|
||
einziger Milestone bisher **sowohl manuell als auch über die App selbst**
|
||
live verifiziert.
|
||
|
||
## Ziel
|
||
|
||
Native macOS-App (SwiftUI), die Laien per geführtem Interview-Wizard durch
|
||
die häufigsten Mikrotik-RouterOS-Konfigurationen führt: Internet-Anschluss,
|
||
Heimnetzwerk/DHCP, zusätzliche VLAN-Netzwerke, WLAN, Firewall-Grundschutz,
|
||
plus Backup/Wiederherstellung. Vollständiger ursprünglicher Plan/Kontext:
|
||
`~/.claude/plans/purrfect-puzzling-bunny.md` (seither an vielen Stellen
|
||
weiterentwickelt, siehe unten).
|
||
|
||
## Setup / Bauen
|
||
|
||
```bash
|
||
cd RouterOS
|
||
xcodegen generate # erzeugt RouterOSAssistant.xcodeproj (nicht in git)
|
||
open RouterOSAssistant.xcodeproj # oder direkt per xcodebuild
|
||
|
||
# Debug (Xcode Cmd+R empfohlen für manuelle/Live-Tests, siehe Gatekeeper-Hinweis unten)
|
||
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
|
||
-destination 'platform=macOS' build
|
||
|
||
# Nur kompilieren, nicht ausführen (umgeht den Gatekeeper-Hang, siehe unten)
|
||
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
|
||
-destination 'platform=macOS' build-for-testing
|
||
|
||
# Release + Deploy nach /Applications
|
||
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
|
||
-configuration Release build
|
||
rm -rf "/Applications/RouterOS Assistant.app"
|
||
cp -R "<DerivedData>/Build/Products/Release/RouterOSAssistant.app" \
|
||
"/Applications/RouterOS Assistant.app"
|
||
killall Dock # Icon-Cache auffrischen, falls sich nur das Icon geändert hat
|
||
```
|
||
|
||
`xcodegen` ist via Homebrew installiert. Projekt-Struktur/Targets/Package-
|
||
Abhängigkeiten stehen in `project.yml`, das ist die Quelle der Wahrheit —
|
||
nicht das generierte `.xcodeproj` von Hand editieren.
|
||
|
||
Git: Remote `origin` zeigt auf eine selbst gehostete Gitea-Instanz
|
||
(`git@192.168.178.222:kay/RouterOS.git`).
|
||
`.xcodeproj`, `DerivedData`, `.build`, Firmware-Dateien (`*.npk`, `*.cpgz`)
|
||
und Router-Backups (`*.rsc`, `/Backups/`) sind gitignored.
|
||
|
||
## Architektur
|
||
|
||
```
|
||
RouterOSAssistant/
|
||
App/RouterOSAssistantApp.swift — 3 Tabs, teilen sich EINE ConnectionService-Instanz
|
||
Core/
|
||
Models/
|
||
RouterOSCommand.swift — eine Änderung, drei Operationen (.add/.set/.remove), zwei Renderer (CLI-Zeile / REST-JSON)
|
||
WanConfig.swift, LanDhcpConfig.swift, VlanEntry.swift, WifiNetworkConfig.swift, FirewallConfig.swift — bauen je RouterOSCommand-Listen
|
||
DhcpServerCommandBuilder.swift — geteilte "Adresse+Pool+Server+Netzwerk"-Logik (LAN + VLAN)
|
||
RouterOSModels.swift — Credentials, DeviceInfo, Interface, RouterOSError
|
||
RouterOSMenuItem.swift — generisches Item (id + [String:String] fields) für JEDEN RouterOS-Menüpfad (M10)
|
||
RouterOSSchema.swift — RouterOSFieldSchema/RouterOSMenuSchema/RouterOSMenuCategory: kuratiertes Formular-Schema fürs Experte-Tool (M10)
|
||
RouterOSSchemaCatalog.swift — die eigentliche Menü-Liste: Firewall-Familie voll kuratiert, Rest teils kuratiert/teils generisch (M10)
|
||
OverviewGraph.swift — OverviewNode/OverviewEdge/OverviewEdgeKind/DevicePortGroup: reine Datenmodelle fürs Übersicht- und Geräte-Diagramm (M11/M12)
|
||
LanDevice.swift — ein LAN-Gerät (DHCP-Lease + ARP + Bridge-Host verschmolzen), fürs Geräte-Tab (M12)
|
||
Networking/
|
||
RouterOSTransport.swift — Protocol: connect/fetchDeviceInfo/fetchInterfaces/fetchFirewallRuleCounts/fetchMenuItems/fetchFieldValues/apply/disconnect
|
||
RestTransport.swift — REST-API (RouterOS ≥7.1), Zertifikats-TOFU, .set via GET+PATCH (findItemID), .remove via GET+DELETE, fetchMenuItems (Array- oder Singleton-Objekt), fetchFieldValues (Query-Filter, unverifiziert)
|
||
SSHTransport.swift — SSH-Fallback via Citadel, CLI-Text-Parsing, resetToFactoryDefaults(), eigene Hostkey-TOFU, fetchMenuItems (generisch, inkl. Singleton-Fallback), fetchFieldValues (`:foreach`+`find`+`get`, M12/Bug 14+17), uploadScript (SFTP, M13), applyRestoreScript (reset-configuration+run-after-reset, M13)
|
||
RouterOSCliParser.swift — parst `/system resource print`, `/interface print terse`, und generisch jeden Menüpfad (parseGenericItems/parseSingletonItem, M10)
|
||
CertificateTrustStore.swift / CertificateFingerprint.swift — TOFU für REST-Zertifikate
|
||
SSHHostKeyTrustStore.swift / SSHHostKeyFingerprint.swift — TOFU für SSH-Hostkeys (M7)
|
||
Services/
|
||
ConnectionService.swift — zentraler App-State: REST-zuerst-SSH-Fallback, hält credentials/interfaces/deviceInfo, plus generisches fetchMenuItems
|
||
BackupService.swift — Config-Export (`/export terse`) über eigene SSH-Verbindung, wählbarer Zielordner, restoreBackup (M13: SFTP-Upload + reset-configuration+run-after-reset + Login-Erhalt), backupModel (Modell-Header-Parsing)
|
||
FactoryResetService.swift — /system reset-configuration über eigene SSH-Verbindung ("Gefahrenzone")
|
||
KeychainService.swift — Passwort-Speicherung
|
||
UpdateService.swift — M14: RouterOS-Software-Update (/system package update check-for-updates/install) + Routerboard-Firmware (upgrade) + reboot, eigene SSH-Verbindung
|
||
Features/
|
||
Wizard/Steps/Connect/ — Verbinden-Tab (inkl. Schnell-Backup-Button, Routerboard-Info, Software-/Firmware-Update-Check (M14), nach erfolgreicher Verbindung)
|
||
Wizard/Steps/Setup/ — Einrichten-Tab: Mode → Wan → Lan → (Vlan nur Experte) → Wifi → Firewall → Review/Apply
|
||
Expert/ — Experte-Tab (M10): ExpertView (Kategorie-/Menüliste + "eigener Pfad"), ExpertViewModel, ExpertMenuDetailView (Item-Liste + Add/Edit-Sheet)
|
||
Overview/ — Übersicht-Tab (M11): OverviewView (Diagramm+Legende+Detailpanel), OverviewViewModel (buildGraph, rein), OverviewLayout (Spalten/Zeilen-Geometrie)
|
||
Devices/ — Geräte-Tab (M12): DevicesView (Port-Tabellen+Rechtsklick+Rohdaten-Sheet), DevicesViewModel (buildDevices/buildPortGroups, rein; confirmStaticAssignment/confirmStaticRemoval)
|
||
Backup/ — Sicherungen-Tab (Ordner wählen, Gefahrenzone: Werkseinstellungen wiederherstellen)
|
||
Resources/Assets.xcassets/AppIcon.appiconset/ — App-Icon "Signal Router" (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/<id>` schicken (`RestTransport.findItemID`). Eingeführt für
|
||
WLAN (M5), weil Wireless-Interfaces schon vor jeder Konfiguration
|
||
existieren. VLAN (M4) hat trotzdem weiterhin keine Port-Zuweisung
|
||
(Access/Trunk) — technisch jetzt machbar, aber bewusste Scope-Entscheidung,
|
||
nicht nachgezogen.
|
||
|
||
**System-Aktionen (Backup, Werksreset) laufen immer über eine eigene
|
||
SSH-Verbindung**, unabhängig vom aktiven Live-Transport — `BackupService`
|
||
und `FactoryResetService` folgen demselben Muster: es sind einmalige
|
||
System-Kommandos, kein Menü-Item zum Hinzufügen/Ändern, passen also nicht
|
||
ins `RouterOSCommand`-Modell und haben keine verifizierte REST-Entsprechung.
|
||
|
||
**ConnectionService ist der einzige geteilte State.** Alle drei Tabs
|
||
bekommen dieselbe Instanz von der App-Ebene injiziert (kein
|
||
`@EnvironmentObject`, explizite Übergabe im Init). SwiftUI-Falle: jede
|
||
View, die `connectionService`-Felder liest, muss ihn selbst als
|
||
`@ObservedObject` halten — nicht nur transitiv über ein anderes ViewModel
|
||
erreichen (siehe Bug 1 unten).
|
||
|
||
## Gefundene Bugs (chronologisch, alle gegen echte Geräte)
|
||
|
||
1. **ConnectView zeigte nie Verbindungsstatus** — `connectionService` war
|
||
nur über `viewModel.connectionService` erreichbar (zwei Ebenen tief
|
||
verschachteltes ObservableObject), SwiftUI abonniert das nicht
|
||
automatisch. Fix: `ConnectView` hält `connectionService` zusätzlich
|
||
selbst als `@ObservedObject`.
|
||
2. **`NIOSSHError.localizedDescription` nutzlos** — bridged auf
|
||
generisches NSError (`"error 1"`). Fix: `String(describing:)`.
|
||
3. **SSH scheiterte mit `keyExchangeNegotiationFailure`** — RouterOS bietet
|
||
nur Legacy-Algorithmen an, Citadel-Default enthält sie nicht. Fix:
|
||
`algorithms: .all`.
|
||
4. **`Citadel.SSHClient.CommandFailed` zeigte nur den Exit-Code** —
|
||
`executeCommand()` verwirft die gesammelte Ausgabe bei Exit-Code ≠ 0.
|
||
Fix: eigene Sammlung über `executeCommandStream()`.
|
||
5. **Picker-Warnungen durch Platzhalter-Interfacenamen** — `prepareDefaults()`
|
||
korrigiert jetzt WAN und LAN, läuft schon im `init`.
|
||
6. **Kritisch: CLI-Parser verlor 6 von 7 Interfaces auf einem hEX-Router** —
|
||
`parseInterfaces`/`parseDeviceInfo` splitteten nur auf `"\n"`; die
|
||
SSH-Ausgabe dieses Geräts trennte Zeilen anders, sodass alle
|
||
Interface-Zeilen zu einer verschmolzen — beim key=value-Parsen
|
||
überschrieb jedes Feld den vorherigen Wert, übrig blieb nur das letzte
|
||
Interface im Text (`lo`, Loopback) mit vermischten Werten. Folge: **jede
|
||
WAN-Interface-Referenz in den angewendeten Firewall-Regeln zeigte auf
|
||
`lo` statt den echten WAN-Port** — auf diesem Testgerät folgenlos, weil
|
||
RouterOS bereits eine vollständige eigene Standard-Firewall (defconf)
|
||
mitbrachte, die weiterhin schützte; auf einem Gerät ohne bestehende
|
||
Firewall wäre das eine wirkungslose, aber als aktiv angezeigte Firewall
|
||
gewesen. Fix: `split(whereSeparator: \.isNewline)` (robust gegen
|
||
`\n`/`\r`/`\r\n`) + zusätzliches Sicherheitsnetz: Loopback-Interfaces
|
||
werden in `SetupView` jetzt grundsätzlich aus allen WAN/LAN/VLAN-
|
||
Auswahllisten gefiltert. Regressionstest mit echten `\r\n`-getrennten
|
||
hEX-Daten in `RouterOSCliParserTests`.
|
||
|
||
7. **`/ip service`: Feld heißt `available-from`, nicht `address`** — beim
|
||
Kuratieren des Experte-Schemas geraten statt geprüft. Live gegen den
|
||
hEX bestätigt: `/ip service print` zeigt tatsächlich `available-from=`;
|
||
ein `set ... address=...` hätte den Zugriffsfilter stillschweigend
|
||
nicht gesetzt. Schema korrigiert (`RouterOSSchemaCatalog.swift`).
|
||
8. **RouterOS 7.24.2 zeigt `.id` gar nicht in `print terse`, und
|
||
Singleton-Menüs verweigern `terse` komplett** — Annahme beim Bau des
|
||
generischen Experte-Motors (M10) war, `.id=*N` sei Teil jeder
|
||
`print terse`-Zeile (wie bei REST-JSON). Live-Test zeigte: RouterOS
|
||
7.24.2 liefert dort gar kein `.id`-Feld; Menüs mit genau einem Eintrag
|
||
statt einer Liste (`/ip dns`, `/system identity`, `/system clock`,
|
||
`/system ntp client`, `/tool e-mail`) lehnen `terse` sogar komplett ab
|
||
(`"bad parameter terse"`). Fix: `:put [<Menüpfad> find]` liefert die
|
||
echten internen IDs (z.B. `*C;*1;*2`) — live bestätigt, dass die
|
||
Reihenfolge exakt der von `print terse` entspricht, damit per Position
|
||
zuordenbar. Für Singleton-Menüs neuer `RouterOSMenuSchema.isSingleton`-
|
||
Schalter: Fetch fällt auf normales `print` (Doppelpunkt-Format,
|
||
`parseSingletonItem`) zurück, `.set` läuft ohne `[find ...]` direkt
|
||
(`matchField: ""` als Sentinel in `RouterOSCommand`/beiden Transports).
|
||
Beides live gegen `/system identity` durchgespielt (Name testweise
|
||
geändert, Fehlertext exakt geprüft, zurückgesetzt).
|
||
9. **Cross-Referenz-Felder (z.B. DHCP-Server → Adress-Pool) waren reiner
|
||
Freitext** — Nutzer meldete: "kann den vorher angelegten Adress-Pool
|
||
nicht in der Auswahl sehen". Betraf jedes Feld, das den Namen eines
|
||
Eintrags aus einem *anderen* Menü referenziert (DHCP-Server→Pool,
|
||
PPP-Benutzer→Profil, Scheduler→Skript, Firewall-Filter→Adress-Liste,
|
||
WireGuard-Peer→Interface) — alle als `.text` statt als Auswahl gebaut.
|
||
Fix: neuer Feldtyp `RouterOSFieldSchema.Kind.menuItemPick(menuPath:
|
||
restPath:valueField:)`, lädt beim Öffnen eines Menüs live die
|
||
existierenden Namen aus dem referenzierten Menü
|
||
(`ExpertViewModel.loadCrossReferenceOptions`) und zeigt sie als Picker.
|
||
Auf alle fünf betroffenen Felder angewendet.
|
||
10. **Kritisch, app-weit: RouterOS' SSH-CLI gibt bei fehlgeschlagenen
|
||
Befehlen Exit-Code 0 zurück** — Nutzer meldete: DHCP-Server-Anlage
|
||
zeigte keine Fehlermeldung, wurde aber auch nicht angelegt. Live
|
||
reproduziert: `/ip dhcp-server add ...` auf einem Interface mit
|
||
bereits bestehendem Server lieferte `"failure: server or relay with
|
||
such interface already exists"` **und Exit-Status 0**; ein ungültiger
|
||
`action=`-Wert lieferte `"syntax error (...)"`, ebenfalls Exit-Status
|
||
0 (per `expect`-Skript mit `catch wait result` geprüft). `SSHTransport.
|
||
apply()` warf bisher nur bei einem von Citadel gemeldeten
|
||
Nicht-Null-Exit-Code — also nie. **Betraf jeden `.apply()`-Aufruf über
|
||
SSH app-weit**, nicht nur den Experte-Tab: der komplette Einrichten-
|
||
Wizard (WAN/LAN/VLAN/Firewall) lief bei jedem SSH-Schreibvorgang mit
|
||
demselben blinden Fleck, seit M3. Fix: `apply()` behandelt jede
|
||
nicht-leere Ausgabe eines add/set/remove-Befehls als Fehler — laut
|
||
allen in dieser Session live getesteten Befehlsfamilien (15+) sind
|
||
erfolgreiche mutierende Befehle immer still. Nach dem Fix live end-to-
|
||
end bestätigt: komplette Kette VLAN→IP-Adresse→Pool→DHCP-Server→
|
||
DHCP-Netzwerk auf einem freien Interface angelegt, lief fehlerfrei
|
||
durch, danach vollständig wieder entfernt.
|
||
11. **Bool-Felder im Experte-Schema als `"true"/"false"` statt `"yes"/"no"`
|
||
geschrieben** — Nutzer meldete nach Bug-10-Fix: VLAN-Anlage zeigte
|
||
jetzt `"Unerwartete Antwort vom Router: syntax error (line 1 column
|
||
30)"`. Live reproduziert mit der exakt von der App erzeugten
|
||
Befehlszeile (`disabled=false`) — identischer Fehler; mit
|
||
`disabled=no` lief es durch. RouterOS' CLI akzeptiert nur `yes`/`no`
|
||
für Bool-Parameter, nicht `true`/`false`. Betraf praktisch jedes
|
||
kuratierte Schema (23× `"false"`, 4× `"true"` als `defaultValue`).
|
||
Fix zweigleisig: `ExpertViewModel.pendingCommand` normalisiert jeden
|
||
`.bool`-Feldwert vor dem Senden auf `yes`/`no` (Schutz unabhängig von
|
||
der Quelle), zusätzlich Katalog-Strings selbst bereinigt. Live erneut
|
||
end-to-end bestätigt (VLAN→IP→Pool→DHCP-Server mit `disabled=no`).
|
||
12. **Lösch-Buttons fehlten komplett im Experte-Tab** — Nutzer: "mir
|
||
fehlen komplett die Löschen-Buttons". Ursache: `.swipeActions(...)`
|
||
ist eine iOS/iPadOS-Wischgeste ohne Entsprechung auf macOS-Listen —
|
||
dort erscheint schlicht nichts, unabhängig vom Code. Fix: sichtbarer
|
||
Papierkorb-Icon-Button je Zeile statt Wisch-Geste (nur bei
|
||
Nicht-Singleton-Menüs, wo Löschen überhaupt Sinn ergibt). Lösch-Pfad
|
||
(ID-Ermittlung + `remove [find .id=...]`) live gegen das vom Nutzer
|
||
selbst angelegte VLAN nachvollzogen, funktioniert.
|
||
13. **Interface-Auswahl (`.interfacePick`) war einmalig beim Verbinden
|
||
eingefroren** — Nutzer legte ein VLAN an, danach fehlte es in der
|
||
Interface-Auswahl beim Anlegen eines DHCP-Servers. Ursache:
|
||
`.interfacePick` las aus `ConnectionService.interfaces`, nur einmal
|
||
bei `finishConnecting()` befüllt, nie danach aktualisiert — anders als
|
||
die neueren `.menuItemPick`-Felder (Pool/Profil/Skript), die bei
|
||
jedem Menü-Öffnen frisch laden. Fix: neues
|
||
`ExpertViewModel.liveInterfaceNames`, per `/interface print terse` bei
|
||
jedem `reloadItems()` aktualisiert; `availableInterfaces`-Parameter-
|
||
Durchreichung (`ExpertView`→`ExpertMenuDetailView`→
|
||
`ExpertItemEditView`) komplett entfernt, da überflüssig. Danach vom
|
||
Nutzer selbst in der App bestätigt: neu angelegtes VLAN erscheint
|
||
sofort in der DHCP-Server-Interface-Auswahl.
|
||
14. **`/ip dhcp-server lease` gibt das `dynamic`-Feld über `print terse`
|
||
nie aus, in keinem Zustand** — beim Bau des Geräte-Tabs (M12) zwei
|
||
falsche Annahmen nacheinander gemacht: erst "fehlt = fest" (Standard-
|
||
Annahme falsch herum, jedes echte DHCP-Gerät zeigte "Fest"), dann
|
||
nach Korrektur "fehlt = dynamisch, `dynamic=no` erscheint explizit
|
||
bei festen Leases" — beides falsch. Live an echten Rohdaten geprüft
|
||
(Nutzer schickte per "Rohdaten anzeigen" wiederholt komplette
|
||
Feldlisten, u.a. für einen zweifelsfrei dynamischen UND einen
|
||
zweifelsfrei festen Eintrag): das Feld taucht in `terse` überhaupt
|
||
nie auf, unabhängig vom Status — nur `/ip dhcp-server lease print`
|
||
(ohne terse) zeigt es als Flag-Buchstabe `D`. Fix: Status kommt nicht
|
||
mehr aus gelesenen Feldern, sondern aus einer gezielten Abfrage gegen
|
||
RouterOS' interne Eigenschaft (`RouterOSTransport.fetchFieldValues`,
|
||
siehe Bug 17 für die korrekte Syntax).
|
||
15. **`fetchMenuItems`s `.id`-Positions-Überlagerung ordnete bei
|
||
`/ip dhcp-server lease` die falsche `.id` der falschen Zeile zu** —
|
||
zwei Leases (iMac dynamisch, Laptop nach Umstellung fest), aber die
|
||
App zeigte beide als "Fest". Ursache: `fetchMenuItems` liest die
|
||
`.id`s über einen separaten `:put [<Pfad> find]`-Aufruf und ordnet
|
||
sie *nach Position* den `print terse`-Zeilen zu (Mechanismus aus
|
||
Bug 8, dort für ein anderes Menü live bestätigt) — für dieses Menü
|
||
stimmte die Reihenfolge zwischen beiden Aufrufen offenbar nicht
|
||
überein, die feste `.id` landete auf der iMac-Zeile. Fix ausschließlich
|
||
im Geräte-Tab: Erkennung *und* der `make-static`-Zielauswahl laufen
|
||
jetzt komplett über die MAC-Adresse (steht direkt in derselben
|
||
`terse`-Zeile, kein Cross-Command-Abgleich nötig) statt über `.id`.
|
||
**Nicht behoben:** derselbe `.id`-Überlagerungsmechanismus wird auch
|
||
vom Experte-Tab für `.set`/`.remove` auf beliebigen Menüs benutzt —
|
||
ob das dort ebenfalls (menü- oder situationsabhängig) falsch zuordnen
|
||
kann, ist ungeprüft. Siehe Nächste Schritte.
|
||
16. **SwiftUI-`.confirmationDialog` löschte sein eigenes Ziel, bevor die
|
||
bestätigte Aktion es lesen konnte** — "Feste IP zuweisen" tat sichtbar
|
||
nichts: kein Fehler, kein Erfolg, Dialog schloss normal. Ursache:
|
||
`isPresented` war eine berechnete `Binding`, deren Setter bei *jedem*
|
||
Schließen (auch durch den bestätigenden Knopf selbst — SwiftUI ruft
|
||
den Setter mit `false` bei jedem Tap in einem `.confirmationDialog`,
|
||
unabhängig vom gedrückten Knopf) `pendingStaticAssignment = nil`
|
||
setzte — also genau das Objekt, das `confirmStaticAssignment()`
|
||
Sekundenbruchteile später lesen wollte. Fix: Dialog-Sichtbarkeit
|
||
(`showStaticConfirmation`, ein einfaches `@State Bool`) und Ziel-
|
||
Objekt (`pendingStaticAssignment`) komplett entkoppelt — genau das
|
||
Muster, das `BackupListView`s Werksreset-Dialog schon die ganze Zeit
|
||
korrekt verwendet (eigenes `@State`-Bool statt abgeleitetem Binding).
|
||
17. **Ein erster Fix-Versuch für Bug 14 geriet zu einer weiteren, nie
|
||
verifizierten Befehls-Annahme** — `<Menü> get [find feld=wert]
|
||
rückgabefeld]` als *ein* kombinierter Befehl geraten (Annahme: `get`
|
||
akzeptiert eine `find`-Ergebnisliste direkt). Live widerlegt: eine
|
||
per Winbox bestätigte, echte feste Zuweisung wurde von dieser Abfrage
|
||
trotzdem nicht gefunden (falsches Negativ). Fix: nur noch aus zwei
|
||
einzeln diese Session mehrfach bestätigten Bausteinen zusammengesetzt
|
||
— nacktes `find` mit einer Bedingung, plus `get <id> feld` auf eine
|
||
einzelne bekannte `.id`, verbunden über eine `:foreach`-Schleife
|
||
(`RouterOSTransport.fetchFieldValues`/`SSHTransport.fetchFieldValues`).
|
||
18. **Kritisch, live so passiert: Backup-Wiederherstellung sperrte den
|
||
Login komplett aus, nur ein Hardware-Reset half noch** — beim ersten
|
||
Live-Test der neuen Restore-Funktion (M13). Ursache doppelt: RouterOS'
|
||
`/export` kann Benutzerpasswörter grundsätzlich nie exportieren
|
||
(offiziell dokumentiert), und der für den Restore genutzte
|
||
`no-defaults=yes`-Reset löscht zusätzlich den mitgelieferten
|
||
Standard-Admin-Account komplett — nach dem Restore existierte also
|
||
weder aus der Sicherung noch als Werksvorgabe ein funktionierender
|
||
Login. Fix: `BackupService.restoreBackup` stellt dem hochgeladenen
|
||
Skript jetzt einen kleinen, selbst generierten Block voran (nicht
|
||
angehängt — RouterOS bricht den Import beim ersten Fehler ab, ein
|
||
Login-Erhalt am Ende hätte also nie gegriffen, falls irgendwo vorher
|
||
im eigentlichen Sicherungsskript ein Fehler auftritt), der exakt das
|
||
aktuell in der App verwendete Login per `:if ([/user find
|
||
name=...] = "") do={add ...} else={set ... password=...}` neu anlegt
|
||
bzw. das Passwort neu setzt — das einzige Passwort, das die App
|
||
zuverlässig kennt. Beim erneuten Live-Test danach vom Nutzer bestätigt
|
||
("das hat funktioniert").
|
||
19. **SwiftUI-Typprüfung bricht bei zu langen Modifier-Ketten ab, kein
|
||
Logikfehler** — `BackupListView` warf nach dem Hinzufügen weiterer
|
||
`.confirmationDialog`/`.alert`-Modifiere für den Restore-Dialog
|
||
`"the compiler is unable to type-check this expression in reasonable
|
||
time"`, mit einer irreführenden Zeilenangabe (zeigte auf einen ganz
|
||
gewöhnlichen, bereits woanders im selben File funktionierenden
|
||
`Binding`-Ausdruck). Fix: die Restore-spezifischen Dialoge/Alerts in
|
||
eine eigene `@ViewBuilder`-Computed-Property ausgelagert, statt alles
|
||
in einer einzigen `body`-Kette zu verketten — teilt die
|
||
Typinferenz-Last auf zwei kleinere Ausdrücke auf. Bei künftigen
|
||
Views mit vielen `.alert`/`.confirmationDialog`/`.sheet`-Modifiern in
|
||
Folge vorbeugend gleich aufteilen, statt erst bei diesem Fehler zu
|
||
reagieren.
|
||
20. **`fetchMenuItems`s Singleton-Fallback (Bug 8/M10) griff für
|
||
`/system routerboard` nicht — genau dasselbe Bug-10-Muster, nur an
|
||
einer Lesestelle statt beim Schreiben.** Die neue Routerboard-Sektion
|
||
(M14) blieb leer, kein Fehler, nichts. Ursache: der Fallback reagiert
|
||
bisher nur auf eine echte Exception ("bad parameter terse" via
|
||
Citadels `CommandFailed`), aber `/system routerboard print terse`
|
||
liefert diesen Fehlertext live bestätigt mit Exit-Code 0 zurück — der
|
||
Fehlertext landete also als normaler (unparsbarer) Output, `fetchMenuItems`
|
||
gab still `[]` zurück statt in den Singleton-Zweig zu fallen. Fix:
|
||
zusätzlich den zurückgegebenen Output-Text selbst auf "bad parameter
|
||
terse" prüfen, nicht nur die Exception — deckt beide Varianten ab, wie
|
||
RouterOS diesen Fehler je nach Menü meldet. Betrifft potenziell jedes
|
||
Singleton-Menü, nicht nur `/system routerboard` — der generische
|
||
Lese-Pfad wird von Übersicht-, Geräte- und Experte-Tab gemeinsam
|
||
genutzt.
|
||
21. **Wizard-WAN-Apply war nicht idempotent** — Wizard erneut auf einem
|
||
Interface durchlaufen, das schon einen DHCP-Client hat (hier: aus
|
||
einer zurückgespielten Sicherung), scheiterte live mit "failure:
|
||
dhcp-client on that interface already exists". `WanConfig.
|
||
buildCommands()` erzeugt für DHCP-Client/PPPoE immer `.add`, ohne
|
||
vorher zu prüfen, ob auf dem Interface schon einer existiert. Fix:
|
||
`SetupViewModel.applyIdempotently` fängt einen `.add`-Fehlschlag auf
|
||
`/ip dhcp-client`/`/interface pppoe-client` ab und wiederholt ihn als
|
||
`.set` (nach "interface" gematcht) — bewusst nur für diese zwei
|
||
Menüs mit "maximal ein Eintrag pro Interface"-Semantik, nicht generell
|
||
für jedes `.add` (z.B. `/ip address` erlaubt legitim mehrere
|
||
Adressen pro Interface). **Noch nicht erneut live getestet**, siehe
|
||
Nächste Schritte.
|
||
|
||
**Lehren:** Citadel/NIOSSH-Fehler immer mit `String(describing:)` loggen,
|
||
nie `.localizedDescription`. Jede View, die ein ObservableObject aus einem
|
||
ViewModel liest, muss es selbst separat als `@ObservedObject` halten.
|
||
Zeilen-Splitting bei SSH-Ausgaben grundsätzlich mit `\.isNewline` statt
|
||
festem `"\n"`, da sich das je nach Gerät/Version unterscheiden kann.
|
||
Bei generischem RouterOS-Tooling (M10) nichts über CLI-Ausgabeformate
|
||
annehmen, was nicht live geprüft ist — `.id`-Verhalten und `terse`-Support
|
||
unterscheiden sich zwischen Menütypen und RouterOS-Versionen. **RouterOS'
|
||
SSH-Exit-Code ist kein verlässliches Erfolgssignal** — Ausgabetext ist die
|
||
einzige verlässliche Fehlerquelle über diesen Transport (Bug 10); REST
|
||
ist davon nicht betroffen (prüft HTTP-Statuscodes korrekt). RouterOS'
|
||
CLI will `yes`/`no` für Bools, nie `true`/`false` (Bug 11) — an einer
|
||
einzigen Stelle normalisieren (`pendingCommand`) statt an jedem
|
||
Schema-Eintrag hoffen, es richtig geschrieben zu haben. SwiftUI-
|
||
Interaktionsmuster nicht plattform-blind übernehmen — `.swipeActions`
|
||
sieht in der Doku harmlos aus, ist aber faktisch iOS/iPadOS-only (Bug 12).
|
||
Jeder Zustand, der sich während der Nutzung ändern kann (Interface-Liste
|
||
nach Anlegen eines VLANs), gehört live nachgeladen, nicht einmalig beim
|
||
Verbinden gecacht (Bug 13) — dasselbe Prinzip wie `crossReferenceOptions`
|
||
für Pool/Profil/Skript, nur nicht von Anfang an konsequent auf alle
|
||
Auswahllisten angewendet. **`print terse` gibt nicht jede Eigenschaft
|
||
als Feld aus** — manche (z.B. `dynamic` bei DHCP-Leases) erscheinen nur
|
||
als Flag-Buchstabe im menschenlesbaren `print`, nie als `key=value`
|
||
(Bug 14); wenn ein Feld partout nicht auftaucht, RouterOS' `find`/`get`
|
||
gegen die interne Eigenschaft fragen statt weiter nach Formulierungen im
|
||
Text zu suchen, die es nicht gibt. **`fetchMenuItems`s `.id`-Überlagerung
|
||
ist nicht für jedes Menü verlässlich** (Bug 15) — bei mehreren möglichen
|
||
Identifikatoren lieber über ein Feld matchen, das direkt in derselben
|
||
Zeile steht (MAC, Name), statt über eine per Position aus einem zweiten
|
||
Befehl zugeordnete `.id`. **Jede kombinierte RouterOS-Skript-Syntax vor
|
||
dem Einsatz live prüfen, auch wenn die Einzelteile schon bestätigt sind**
|
||
(Bug 17) — `find` allein und `get <id> feld` allein zu kennen heißt
|
||
nicht, dass `get [find ...] feld` als eine Einheit genauso funktioniert.
|
||
**Bei SwiftUI-Bestätigungsdialogen Sichtbarkeit und Nutzlast nie an
|
||
dasselbe State-Feld koppeln** (Bug 16) — `isPresented`s Setter feuert bei
|
||
jedem Knopfdruck, nicht nur bei Abbrechen; ein eigenes, unabhängiges Bool
|
||
fürs Zeigen/Verstecken hält die Nutzlast so lange am Leben, wie die
|
||
bestätigte Aktion sie braucht. **RouterOS-Backups über `/export` enthalten
|
||
grundsätzlich nie Passwörter, und `no-defaults=yes` löscht auch den
|
||
Werks-Admin-Account** (Bug 18) — jede Restore-Funktion, die einen
|
||
kompletten Reset vor dem Re-Import macht, muss den Login selbst wieder
|
||
herstellen, sonst folgt ein kompletter Aussperr-Vorfall (nur per
|
||
Hardware-Reset behebbar, live so passiert). Login-Erhalt gehört an den
|
||
*Anfang* des Skripts, nicht ans Ende — RouterOS bricht den Import beim
|
||
ersten Fehler irgendwo im Skript komplett ab. **SwiftUI-Typprüfung kann
|
||
bei langen Modifier-Ketten mit einer irreführenden Fehlerzeile
|
||
timeouten** (Bug 19) — kein Hinweis auf einen echten Logikfehler an der
|
||
genannten Stelle; bei vielen `.alert`/`.confirmationDialog` in Folge
|
||
vorbeugend in eigene `@ViewBuilder`-Properties aufteilen. **"RouterOS'
|
||
Exit-Code ist kein verlässliches Erfolgssignal" (Bug 10) gilt auch für
|
||
Lesebefehle, nicht nur fürs Schreiben** (Bug 20) — derselbe
|
||
Singleton-Fallback, der für ein Menü per Exception ausgelöst wurde
|
||
(Bug 8), blieb für ein anderes Menü aus, weil RouterOS denselben
|
||
Fehlertext dort mit Exit-Code 0 zurückgibt; robuste Erkennung muss immer
|
||
auch den Output-Text selbst prüfen, nie nur auf eine geworfene Exception
|
||
vertrauen. **Wizard-Apply nutzte für WAN-DHCP-Client/PPPoE immer `.add`**
|
||
(Bug 21) — scheiterte live mit "failure: dhcp-client on that interface
|
||
already exists" auf einem Interface, das (z.B. aus einer zurückgespielten
|
||
Sicherung) schon einen Client hatte; `SetupViewModel.applyIdempotently`
|
||
versucht seitdem bei `.add`-Fehlschlag auf diesen beiden Menüs
|
||
automatisch `.set` (nach "interface" gematcht). **Ein neu eingerichtetes
|
||
LAN-/VLAN-Interface wurde nie der defconf-Interface-Liste "LAN"
|
||
hinzugefügt** (Bug 22) — auf Routern mit Werks-Firewall
|
||
(`chain=input action=drop in-interface-list=!LAN`) blieb dadurch jede
|
||
Anfrage an den Router selbst (DNS, Winbox) vom neuen Netz aus blockiert,
|
||
obwohl DHCP/Routing/Internet normal liefen (reine Forward-Chain-Sache,
|
||
unberührt) — live an einer manuell nachgebauten Isolation gefunden
|
||
(Lease + Default-Route vorhanden, jede DNS-Anfrage lief trotzdem in
|
||
Timeout). **Voller Wizard-Durchlauf gegen einen bereits konfigurierten
|
||
Router brach an jedem nicht-idempotenten `.add` ab** (Bug 23) — nur
|
||
WAN-DHCP-Client/PPPoE waren idempotent (Bug 21); `/ip address`
|
||
(exaktes Duplikat = bereits erledigt) sowie `/ip pool`/`/ip
|
||
dhcp-server`/`/ip dhcp-server network` (Retry als `.set`, gematcht auf
|
||
name/name/address) kamen dazu. **Ein als eigenes isoliertes Netz
|
||
konfiguriertes Interface blieb Bridge-"Slave"** (Bug 24) — auf
|
||
Werks-Routern sind 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=<name>]` voran (übersprungen für "bridge"
|
||
selbst). **Ein geleertes Textfeld (z.B. Kommentar) blieb nach dem
|
||
Speichern unverändert bestehen** (Bug 25) — `pendingCommand` filterte
|
||
alle leeren `formValues` grundsätzlich aus den Argumenten heraus, RouterOS'
|
||
`set` ändert aber nur explizit übergebene Parameter, ein weggelassener
|
||
bleibt unangetastet statt geleert. Fix: ein Feld bleibt im Argument-Set,
|
||
wenn es vorher einen Wert hatte (jetzt als `feld=""` explizit geleert);
|
||
zusätzlich musste `RouterOSCommand`s SSH-Zeilen-Rendering einen leeren
|
||
Wert als `""` statt als nacktes `feld=` ausgeben. **Bearbeiten einer
|
||
Route (z.B. nur den Kommentar ändern) scheiterte mit "bad parameter
|
||
immediate-gw"** (Bug 26) — RouterOS liefert bei `/ip route print` u.a.
|
||
das berechnete, nur lesbare Feld `immediate-gw` mit; da dieses Feld im
|
||
Route-Schema nicht kuratiert ist, landete es unverändert in den freien
|
||
"Weitere Parameter" und wurde bei *jedem* Speichern blind mitgeschickt —
|
||
RouterOS lehnt `immediate-gw` beim `set` als ungültigen Parameter ab und
|
||
bricht dadurch die komplette Änderung ab, auch wenn nur ein unrelated
|
||
Feld wie der Kommentar geändert wurde. Fix: ein unkuratiertes Feld wird
|
||
nur noch mitgeschickt, wenn sein Wert sich gegenüber dem ursprünglich
|
||
geladenen Item tatsächlich geändert hat (oder neu hinzugefügt wurde) —
|
||
betrifft nicht nur Routen, sondern jedes Menü mit berechneten/nur
|
||
lesbaren Feldern im generischen Lese-Pfad. **Direkt beim Nachtesten
|
||
derselbe Effekt nochmal, diesmal bei einem kuratierten Feld** (Bug 27) —
|
||
eine dynamische/verbundene Route (automatisch angelegt durch eine
|
||
IP-Adresse auf dem Interface) hat `distance=0`; das *unveränderte*
|
||
Zurücksenden dieses ganz normal kuratierten Feldes beim Ändern des
|
||
Kommentars scheiterte mit "value of distance out of range (1...255)" —
|
||
RouterOS akzeptiert `distance=0` nur für System-verwaltete Routen, nicht
|
||
als Eingabewert für `set`. Zeigt: das "nur bei Änderung senden"-Prinzip
|
||
aus Bug 26 galt bisher nur für unkuratierte Felder, kuratierte Felder
|
||
wurden weiterhin immer komplett neu gesendet. Fix generalisiert:
|
||
`pendingCommand` sendet jetzt für **jedes** Feld (kuratiert wie
|
||
unkuratiert) beim Bearbeiten eines bestehenden Items nur noch den
|
||
Diff gegen das ursprünglich geladene Item — deckt implizit auch Bug 25
|
||
(Leeren eines Feldes) mit ab, der Spezialfall dafür wurde entfernt.
|
||
Nebenbei: die neue `ExpertViewModelTests.swift` lief anfangs gar nicht
|
||
mit, weil `xcodegen generate` nach dem Anlegen der Datei nicht erneut
|
||
lief — Xcodegen erzeugt die `sources`-Dateiliste einmalig beim
|
||
Generieren, eine neu angelegte Datei taucht im generierten `.xcodeproj`
|
||
nicht automatisch auf. **Merksatz: nach jeder neu angelegten Datei
|
||
`xcodegen generate` erneut laufen lassen, nicht nur nach Datei-Edits.**
|
||
**Auch nach dem Bug-27-Fix scheiterte dieselbe Route noch, jetzt mit
|
||
"no such item (4)"** (Bug 28) — der `/ip route print detail`-Gegencheck
|
||
bestätigte: die Route ist dynamisch (`D`-Flag, `distance=0`, automatisch
|
||
angelegt durch die IP-Adresse auf `bridge`). RouterOS' eigenes
|
||
"dynamic"-Flag steht nicht zuverlässig in `print terse` (dasselbe
|
||
Problem schon bei DHCP-Leases dokumentiert, siehe `LanDevice.swift`),
|
||
aber `distance=0` ist ein verlässliches Signal — keine echte statische
|
||
Route kann das haben (RouterOS erzwingt 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=<name>` referenziert
|
||
wird). Dabei **drei echte, vorher unentdeckte Bugs gefunden und
|
||
gefixt**, alle drei ausschließlich möglich, weil kein Testgerät vorher
|
||
je `www-ssl` an hatte:
|
||
- **Bug 30:** `RestTransport.baseURL` fehlte der abschließende `/`
|
||
(`".../rest"` statt `".../rest/"`). `URL(string:relativeTo:)`
|
||
ersetzt bei relativer Auflösung das *letzte Pfadsegment* der Basis
|
||
(RFC 3986 §5.3) — ohne trailing slash ist "rest" selbst das letzte
|
||
Segment, ein Pfad wie "system/resource" ersetzt es also statt
|
||
danach angehängt zu werden (`.../rest` + "system/resource" →
|
||
`.../system/resource`, 404). Jede REST-Verbindung scheiterte dadurch
|
||
mit stillschweigend verschlucktem "HTTP 404" (der generische
|
||
`catch`-Zweig in `ConnectionService.connect` loggte den Fehler
|
||
nirgends), fiel auf SSH zurück, ohne je den Zertifikats-Dialog zu
|
||
zeigen — sah aus wie "kein Zertifikat-Prompt trotz neuem
|
||
Zertifikat", war tatsächlich ein komplett anderer, tieferliegender
|
||
Bug. Gefunden über einen temporären Debug-`print` im Fallback-Pfad.
|
||
- **Bug 31:** `RestTransport.apply()`s `.add`-Fall nutzte `POST`,
|
||
RouterOS' REST-API erwartet dafür aber `PUT` — `POST` auf
|
||
`/rest/ip/firewall/address-list` lieferte `{"detail":"no such
|
||
command","error":400,"message":"Bad Request"}`, `PUT` mit
|
||
identischem Body `201` mit vollem Echo des neuen Items. Nebenbei
|
||
`send()`s Fehlerbehandlung verbessert: zeigte bisher nur den nackten
|
||
Statuscode ("HTTP 400"), jetzt wird RouterOS' eigener JSON-Fehler-
|
||
Body (`detail`-Feld) mit ausgegeben, falls vorhanden — genau dieser
|
||
Body war es, der den Bug überhaupt auffindbar machte (per direktem
|
||
`curl`-Replay derselben Anfrage).
|
||
- **Bug 32, am folgenreichsten:** RouterOS sendet auf *jede*
|
||
`/rest/...`-Antwort `Cache-Control: max-age=31536000` (ein Jahr) +
|
||
passenden `Expires`-Header — vermutlich ein Blanket-Default für
|
||
statische Web-UI-Assets, der auch auf die REST-API durchschlägt.
|
||
`URLSession(configuration: .ephemeral)` bedeutet nur "nichts auf
|
||
Platte persistieren", hält aber weiterhin einen Im-Speicher-
|
||
`URLCache` und hält sich an diese Header — die *erste* GET-Antwort
|
||
auf einen Pfad (z.B. eine leere Artikelliste, abgerufen durch
|
||
`loadCrossReferenceOptions` oder ein früheres Öffnen desselben
|
||
Menüs) wurde für den Rest der App-Laufzeit unverändert zurückgegeben,
|
||
komplett unabhängig von späteren `.add`/`.set`/`.remove`-Änderungen.
|
||
Live-Symptom: ein frisch angelegter Adress-Listen-Eintrag (per
|
||
`curl` als real bestätigt) erschien nie in der Experte-Tab-Liste,
|
||
auch nach Weg-und-zurück-Navigation nicht. Fix: `urlCache = nil` +
|
||
`.reloadIgnoringLocalCacheData` auf Session- *und* Request-Ebene
|
||
(RouterOS' Schreib-Endpunkte sind nicht idempotent genug, um sich
|
||
nur auf eine der beiden Ebenen zu verlassen).
|
||
Nach allen drei Fixes live durchgespielt: Anlegen (`PUT`), Bearbeiten
|
||
inkl. Feld-Leeren (`PATCH`, Kommentar gesetzt dann wieder gelöscht —
|
||
REST-seitig direkt per `curl` verifiziert, dass ein leerer String das
|
||
Feld tatsächlich löscht, genau wie bei SSH/Bug 25), Löschen (`DELETE`)
|
||
— alle drei vom Nutzer bestätigt ("funktioniert"). Damit ist der
|
||
REST-Transport erstmals end-to-end gegen echte Hardware verifiziert,
|
||
nicht mehr nur gegen Mocks. Der REST-Zweig von `fetchFieldValues`
|
||
(M12/Bug 14, Query-String-Filter `?feld=wert` auf einem GET) bleibt
|
||
weiterhin unverifiziert — dabei nicht auf den Weg gekreuzt.
|
||
- **Bug 33, direkt danach gefunden:** Löschen eines Eintrags im
|
||
Experte-Tab aktualisierte die sichtbare Liste nicht, bis der Nutzer
|
||
in einen anderen Tab und zurück wechselte — sah zunächst wieder wie
|
||
ein Cache-/Refresh-Problem aus, war es aber nicht: ein temporärer
|
||
Debug-`print` in `confirmRemoval` bestätigte `items.count` geht
|
||
korrekt von 1 auf 0 nach `reloadItems()`, das `@Published`-Modell
|
||
war also die ganze Zeit richtig. Reines SwiftUI-Render-Problem:
|
||
Anlegen/Bearbeiten laufen über `ExpertItemEditView` als `.sheet(item:)`,
|
||
dessen Schließen `ExpertMenuDetailView` automatisch neu mountet —
|
||
genau das erzwingt implizit den Refresh, den Löschen (über ein
|
||
direkt an `ExpertMenuDetailView` gehängtes `.confirmationDialog`,
|
||
kein Sheet, kein Remount) nie bekam. Fix: `.id(viewModel.items.count)`
|
||
auf der Einträge-`Section`, zwingt SwiftUI bei jeder Mengenänderung
|
||
zu einer frischen View-Identität statt sich auf In-Place-Diffing zu
|
||
verlassen. Live bestätigt ("funktioniert").
|
||
- **`fetchMenuItems`s `.id`-Positions-Überlagerung: Verlässlichkeit für
|
||
andere Menüs ungeprüft** (siehe Bug 15) — bei `/ip dhcp-server lease`
|
||
live als falsch bestätigt (`.id` landete auf der falschen Zeile), für
|
||
den Geräte-Tab per MAC-Adress-Matching umgangen. Derselbe Mechanismus
|
||
läuft aber weiterhin im Experte-Tab für `.set`/`.remove` auf *jedem*
|
||
Menü — ob und wo das dort ebenfalls falsch zuordnen kann, ist nicht
|
||
untersucht. Sollte bei unerklärlichem Verhalten dort (falsches Item
|
||
geändert/gelöscht) als erste Verdachtsquelle geprüft werden.
|
||
**Nachtrag 2026-09-16:** live an `/ip address` (3 Einträge), `/ip route`
|
||
(4 Einträge) und erneut `/ip dhcp-server lease` (2 Einträge) geprüft
|
||
(`:put [<Pfad> find]` gegen einzeln per Filter ermittelte echte `.id`s
|
||
je Zeile verglichen) — in allen drei Fällen stimmte die Positions-
|
||
Reihenfolge diesmal exakt. Kein erneuter Fehlgriff reproduzierbar,
|
||
**aber das entkräftet Bug 15 nicht**: RouterOS dokumentiert nirgends,
|
||
dass die Reihenfolge von `:put [<Pfad> find]` und `print terse` stabil
|
||
identisch ist, und beide werden als zwei getrennte SSH-Befehle
|
||
nacheinander abgesetzt. Arbeitshypothese, unbestätigt: bei `/ip
|
||
dhcp-server lease` handelt es sich um eine reine Timing-Race — die
|
||
Lease-Tabelle ändert sich laufend durch verbindende/trennende Geräte,
|
||
eine Umsortierung zwischen den beiden Befehlen (statt einer grundsätzlich
|
||
falschen Zuordnung) würde erklären, warum Bug 15 dort auftrat, bei den
|
||
heute getesteten, weitgehend statischen Menüs (Adressen, Routen) aber
|
||
nicht. Für statische Menüs also vermutlich unkritisch, für Menüs mit
|
||
häufig wechselnden dynamischen Einträgen (DHCP-Leases, Verbindungs-
|
||
Tracking-Tabellen, falls je generisch angebunden) weiterhin als riskant
|
||
einstufen.
|
||
- **WLAN-`.set`-Pfad (M5) weiterhin ungetestet gegen echte Hardware** —
|
||
nur der "kein WLAN"-Zweig ist bestätigt (zwei Testgeräte, beide ohne
|
||
WLAN-Chip). Sicherheitsprofil-Anlage + SSID/Passwort-`.set` auf einem
|
||
echten `/interface wireless`-Interface noch nie live gelaufen.
|
||
- ~~Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut
|
||
bestätigt~~ — erledigt: beim M8-App-Test (siehe oben) lief ein voller
|
||
`FirewallConfig`-Apply mit `ether1` als WAN erneut durch,
|
||
`/ip firewall filter print` zeigte alle Basisregeln (established/
|
||
related, invalid-drop, ICMP-Accept, WAN-Drop) korrekt mit `ether1`.
|
||
- **Neuer RouterOS-WiFi-Treiber (`/interface wifi`, wifiwave2/802.11ax)
|
||
nicht unterstützt** — wird erkannt und im UI erklärt, aber nicht
|
||
konfiguriert.
|
||
- **Kein garantiertes Auto-Rollback** bei Verbindungsabbruch während
|
||
"Jetzt anwenden" — nur Backup-vorher + Bestätigungspflicht.
|
||
- **App ist ad-hoc signiert, nicht notarisiert** — beim ersten Start einer
|
||
frisch nach `/Applications` kopierten Version zeigt macOS die
|
||
"nicht verifizierter Entwickler"-Warnung (Rechtsklick → Öffnen nötig).
|
||
- ~~SSH-Hostkey-TOFU ungetestet~~ — **bestätigt gegen echte Hardware**:
|
||
erste Verbindung zeigte den "Unbekannter SSH-Schlüssel"-Dialog, nach
|
||
Bestätigen + Trennen + erneutem Verbinden lief es ohne Rückfrage durch.
|
||
- **Experte-Tool (M10): mehrere kuratierte Bereiche noch nicht gegen
|
||
Hardware bestätigt** — WLAN (Legacy- und neuer wifiwave2-Treiber:
|
||
Testgerät hat keinen WLAN-Chip), Bonding und PPPoE-Client (beide riskant
|
||
für die eigene Erreichbarkeit/brauchen echte ISP-Zugangsdaten) nur gegen
|
||
RouterOS-Dokumentation kuratiert, nicht live geprüft. `/ip address`,
|
||
`/ip dhcp-client`, `/ip dns`, `/ip service` nur lesend geprüft (Feldnamen
|
||
bestätigt), keine Schreibtests — Risiko, sich selbst auszusperren
|
||
(SSH/REST-Dienst) bzw. die WAN-Verbindung zu kappen.
|
||
- **Experte-Tool: einige RouterOS-Bereiche bewusst nur generisch, nicht
|
||
kuratiert** — Hotspot, Logging (beide mehrteilig, kein flaches Schema),
|
||
CAPsMAN, L2TP-/OpenVPN-Server, OSPF/BGP-Routing. Grund: keine verifizierte
|
||
Quelle für die genauen Feldnamen, lieber generischer Schlüssel/Wert-
|
||
Zugriff als geratene Felder (siehe Bug 7 oben, warum das eine berechtigte
|
||
Sorge ist).
|
||
|
||
## M8: Mehrere LAN-Interfaces + Netzwerk-Isolation
|
||
|
||
Auf Nutzerwunsch: mehrere physische LAN-Interfaces je mit eigenem
|
||
DHCP-Server (`SetupViewModel.lanConfigs: [LanDhcpConfig]`, analog zum
|
||
schon vorhandenen VLAN-Listen-Muster), plus ein Isolation-Toggle pro
|
||
LAN-/VLAN-Eintrag ("Von anderen Netzwerken isolieren"). Bei aktivem
|
||
Firewall-Grundschutz erzeugt `FirewallConfig` daraus paarweise
|
||
Forward-Drop-Regeln zwischen jedem isolierten Netzwerk und allen anderen
|
||
konfigurierten Netzwerken (beide Richtungen, Pair-Dedup bei gegenseitiger
|
||
Isolation). Internetzugriff bleibt weiterhin global über die bestehende
|
||
WAN-NAT-Logik, nicht pro Netzwerk abschaltbar (bewusst außerhalb Scope).
|
||
|
||
Nebenbei behoben: `VlanStepView` behauptete vorher im Hilfetext bereits
|
||
Isolation vom Hauptnetzwerk, ohne dass eine einzige Firewall-Regel das
|
||
durchsetzte — reiner Text ohne Wirkung. Jetzt ist Isolation ein echter,
|
||
optionaler Schalter mit tatsächlicher Regel-Erzeugung.
|
||
|
||
**Live gegen Hardware verifiziert (2026-09-15)** — zuerst manuell per
|
||
SSH nachgebaut (siehe "Zum Sessionende" oben), danach über den
|
||
App-Wizard selbst (`ether4`, Experte-Modus). iMac ↔ Laptop erreichen
|
||
sich über die Netzgrenze nicht mehr, Internet + DNS funktionieren auf
|
||
beiden Seiten weiter. Dabei drei App-Bugs gefunden und gefixt (Bug
|
||
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<RouterOSMenuCategory>`
|
||
für die neun Kategorien, `isCustomPathExpanded: Bool` für "Eigener
|
||
Menüpfad"). Auf Nutzerwunsch ("standardmäßig sollten beim Öffnen des
|
||
Experte-Tab alle Sektion zugeklappt sein") startet alles zugeklappt.
|
||
`.listStyle(.sidebar)` an der `List` ergänzt, für den nativen
|
||
macOS-Sidebar-Look. Live bestätigt ("sehr gut, bin sehr zufrieden").
|
||
|
||
**M24: LAN-Scanner — "Aktionen"-Button, Traffic-Monitor pro Port +
|
||
Sparkline, ARP-Auflösungsbug gefixt** (2026-09-16). Drei Nutzerwünsche
|
||
in einer Runde:
|
||
|
||
1. **"Aktionen"-Button statt Rechtsklick** ("der Rechtsklick ist nicht
|
||
eindeutig erkennbar oder intuitiv, verlager bitte alle Funktionen
|
||
des Rechtsklicks in den Button") — `.contextMenu` komplett entfernt,
|
||
derselbe `deviceMenu(for:)`-Inhalt (Feste IP zuweisen/entfernen,
|
||
Netzwerk-Tools, Rohdaten anzeigen) läuft jetzt über einen sichtbaren
|
||
`Menu`-Button ("•••", Label "Aktionen") hinter jeder Geräte-Zeile.
|
||
2. **Traffic-Monitor pro Port + Sparkline** ("ein Traffic-Monitor im
|
||
LAN-Scanner", danach "ein kleines Liniendiagramm der letzten 10
|
||
Sekunden ... pro Port") — pro-*Gerät* geht nicht ohne RouterOS-
|
||
seitige Queue-Trees pro MAC (Nutzer selbst dagegen entschieden:
|
||
"Traffic pro PORT statt pro Gerät"), daher denselben bereits
|
||
gebauten `InterfaceTrafficMonitor` (Verbinden-Tab) wiederverwendet —
|
||
`DevicesViewModel` bekam eigene `startTrafficPolling`/
|
||
`stopTrafficPolling` (3s-Takt, gleiches Muster wie
|
||
`ConnectViewModel`). Live-↓/↑-Zahl direkt neben jedem Port-Titel,
|
||
plus neuer `TrafficSample`-Typ (`Core/Models/InterfaceTraffic.swift`)
|
||
für eine nach echter Zeit (nicht fester Sample-Anzahl) auf 10
|
||
Sekunden getrimmte Historie, gerendert als `Chart`/`LineMark`
|
||
(Swift Charts, `TrafficSparkline` in `DevicesView.swift`) ohne
|
||
Achsen — reiner Trend-Blick, die genaue Zahl steht ja daneben.
|
||
3. **Bug 34, dabei live gefunden:** ein Laptop an `ether4` (Traffic
|
||
sichtbar aktiv) erschien trotzdem unter "Unbekannter Port". Ursache:
|
||
RouterOS' `/ip arp`-Tabelle hielt für dieselbe MAC-Adresse zwei
|
||
Zeilen gleichzeitig — eine korrekte (`interface=ether4,
|
||
status=reachable`) und eine veraltete (`interface=bridge,
|
||
status=failed`, Überbleibsel von vor einem Netzwerkwechsel des
|
||
Geräts). `DevicesViewModel.buildDevices` baute `arpInterfaceByMAC`
|
||
bisher per blindem Überschreiben der letzten gesehenen Zeile auf —
|
||
welche Zeile "gewann" hing von der Router-internen Tabellen-
|
||
Reihenfolge ab, nicht von Korrektheit. Fix: `status=reachable`
|
||
gewinnt jetzt immer gegen jeden anderen Status, bei Gleichstand ein
|
||
konkreter Port gegen "bridge". Zwei neue Regressionstests
|
||
(`testReachableArpRowWinsOverStaleFailedRowForSameMAC`/
|
||
`...RegardlessOfOrder`, letzterer prüft explizit, dass der Fix nicht
|
||
nur für die live beobachtete Reihenfolge zufällig funktioniert).
|
||
|
||
**Nebenbefund beim Debuggen von Punkt 2, kein Code-Bug, aber ein
|
||
Betriebshinweis:** der Traffic-Monitor zeigte zunächst dauerhaft nichts
|
||
an — `InterfaceTrafficMonitor.fetchTraffic` scheiterte still (`try?`)
|
||
mit `untrustedSSHHostKey`. Ursache: der Router hatte seit dem letzten
|
||
Werksreset (2026-09-15) einen neuen SSH-Host-Key, aber da die Haupt-
|
||
verbindung dieser Session durchgehend über REST lief (M22-Fix), kam der
|
||
bereits vorhandene "Unbekannter SSH-Schlüssel"-Bestätigungsdialog nie
|
||
zum Zug — der lief bisher nur im SSH-*Fallback*-Pfad von
|
||
`ConnectionService.connect`. Dedizierte SSH-Verbindungen (Backup,
|
||
Netzwerk-Tools, Traffic-Monitor, Update, Werksreset) haben *keinen*
|
||
eigenen UI-Bestätigungspfad für einen neuen Host-Key, scheitern bei
|
||
einem Mismatch einfach still. Behoben durch `www-ssl` kurz deaktiviert
|
||
(erzwingt den SSH-Fallback-Pfad in der Haupt-Verbindung), Nutzer hat
|
||
den neuen Fingerprint einmalig über den bestehenden Dialog bestätigt
|
||
(`SSHHostKeyTrustStore` ist ein einziger, über `UserDefaults.standard`
|
||
geteilter Speicher — einmal dort vertraut gilt für jede dedizierte
|
||
SSH-Verbindung mit), danach `www-ssl` wieder aktiviert. **Bleibt ein
|
||
echter, noch offener Schwachpunkt:** falls der SSH-Host-Key sich künftig
|
||
nochmal ändert (z.B. nach einem weiteren Werksreset) UND REST zu diesem
|
||
Zeitpunkt bereits verbunden ist, würden dieselben dedizierten Dienste
|
||
wieder wortlos nichts tun, ohne Hinweis auf die Ursache — siehe
|
||
"Nächste Schritte".
|
||
|
||
**M25: Einrichten-Wizard-Politur** (2026-09-16, mehrere kleine
|
||
Nutzerwünsche in einer Runde):
|
||
|
||
- **WAN-Schritt: "Zurück"-Button ergänzt** — fehlte seit jeher (nur
|
||
"Weiter"), in beiden Modi, fiel im Experte-Modus nur eher auf, da
|
||
dort mehr Schritte durchgeklickt werden. Live bestätigt.
|
||
- **"Abbrechen" (Einrichten) + "Jetzt sichern" (Sicherungen) prominent
|
||
gemacht** — dieselbe `.buttonStyle(.borderedProminent)`-Behandlung
|
||
wie zuvor schon "Neu scannen" (Bug: "leicht zu übersehen"), jetzt
|
||
konsequent auf alle **einzelnen, gleichrangigen** Toolbar-Aktions-
|
||
buttons der App angewendet. Übersicht-Tabs Toolbar-Gruppe (Zoom/
|
||
Zurücksetzen/Aktualisieren, mehrere gleichrangige Buttons
|
||
nebeneinander) bewusst ausgenommen — dort würde durchgängiges
|
||
Prominent-Styling überladen wirken, ein anderes UI-Muster als "eine
|
||
einzelne leicht übersehene Hauptaktion".
|
||
- **LAN- und VLAN-Schritt: Adressfelder starten leer** (Nutzerwunsch:
|
||
"die auszufüllenden Felder leer lassen, nur Beispiele zeigen ...
|
||
so sieht man das Schema dahinter") — `LanDhcpConfig`/`VlanEntry`
|
||
hatten bisher konkrete Default-Werte (z.B. "192.168.88.1/24" bzw.
|
||
eine aus der VLAN-ID abgeleitete Adresse); da jedes TextField-Label
|
||
("Router-Adresse (z.B. 192.168.88.1/24)") ohnehin als Platzhalter
|
||
dient, sobald das Feld leer ist, versteckte ein vorbelegter Wert
|
||
dieses Beispiel bisher komplett. Betrifft beide Modi (LAN-Schritt
|
||
wird von Einfach und Experte geteilt). `interfaceName` (Picker) und
|
||
`leaseTimeHours` (Stepper) bleiben unverändert vorbelegt — beides
|
||
Auswahl-UI ohne "Beispiel tippen"-Konzept. Notwendige Begleitänderung:
|
||
`VlanStepView`s "Weiter" hatte bisher **keine** Validierungssperre
|
||
(unnötig, solange die Defaults immer gültig waren) — jetzt analog zum
|
||
längst vorhandenen `LanStepView`-Muster ergänzt, sonst hätte ein leer
|
||
gelassenes VLAN-Feld einen Befehl mit leeren Argumenten zum Router
|
||
geschickt. Zwei Tests entsprechend angepasst
|
||
(`testDefaultAddressesStartEmptyForExamplePlaceholder` ersetzt die
|
||
alte Ableitungs-Prüfung; `testLanDhcpCommandsCoverAddressPoolServerAndNetwork`
|
||
setzt die Adressfelder jetzt explizit statt sich auf Modell-Defaults
|
||
zu verlassen). Live bestätigt ("gut soweit").
|
||
|
||
**M26: "Bekannte Router" hinterlegt jetzt die Seriennummer**
|
||
(2026-09-16). Nutzer-Szenario, live so passiert: mit einem zweiten
|
||
Router desselben Modells verbunden (Werks-Adresse+Zugangsdaten
|
||
identisch zum bisherigen Testrouter, `192.168.88.1`/`admin`) — die App
|
||
hat daraufhin den *bestehenden* "Bekannte Router"-Eintrag des alten
|
||
Testrouters einfach für den neuen umbenannt (`SavedRoutersStore.
|
||
recordSuccessfulConnection` matchte bisher nur auf Host+Benutzername,
|
||
kein Feld unterschied zwei physisch verschiedene Geräte an derselben
|
||
Werks-Adresse). Klick auf den alten Eintrag verband danach zwangsläufig
|
||
mit dem tatsächlich erreichbaren Gerät (dem neuen Router), nicht mit
|
||
dem gemeinten alten. Fix (Nutzervorschlag: "ist es nicht besser die
|
||
Seriennummer des Gerätes mit zu hinterlegen?"): `SavedRouter` bekam ein
|
||
neues optionales `serialNumber`-Feld (`/system routerboard`s
|
||
"serial-number", bereits seit M14 als `RouterBoardInfo.serialNumber`
|
||
ausgelesen — "unbekannt" wird dabei wie `nil` behandelt, sonst würden
|
||
zwei Geräte ohne auslesbare Seriennummer fälschlich als "gleich"
|
||
gelten). `recordSuccessfulConnection` matcht jetzt gestuft: exakte
|
||
Seriennummer gewinnt zuerst; ein einzelner bestehender Eintrag ohne
|
||
gespeicherte Seriennummer (alte, vor diesem Feld gespeicherte Einträge,
|
||
oder Geräte ohne physisches Routerboard) wird beim nächsten Connect
|
||
einmalig nachträglich damit befüllt statt dupliziert; alles andere legt
|
||
einen neuen, getrennten Eintrag an. In `SavedRouterRow` als kleine
|
||
"SN: ..."-Zeile sichtbar gemacht, damit zwei gleich benannte/adressierte
|
||
Einträge auch optisch unterscheidbar sind. Drei neue Tests
|
||
(`testDifferentSerialAtSameHostUsernameCreatesSeparateEntry`,
|
||
`testLegacyEntryWithoutSerialGetsUpgradedInPlaceNotDuplicated`,
|
||
`testSameSerialReconnectUpdatesRecencyNotDuplicated`). Migrationshinweis
|
||
für den konkreten Fall: der bereits verschmolzene Bestandseintrag zeigt
|
||
aktuell die Seriennummer des neuen Routers — der nächste Connect zum
|
||
alten Testrouter legt automatisch einen neuen, eigenen Eintrag an (da
|
||
dessen Seriennummer nicht mehr passt), bei Bedarf danach umbenennen.
|
||
Live bestätigt ("passt, funktioniert") — allerdings dabei zwei weitere,
|
||
eng verwandte Bugs gefunden, siehe M27 direkt danach.
|
||
|
||
**M27: Passwort-Anzeige-Button, Schlüsselbund-Trennung, `terse`-Fallback
|
||
verallgemeinert** (2026-09-16, beim Live-Test von M26 gefunden):
|
||
|
||
1. **Passwort-Anzeige-Button** (Nutzerwunsch: "setze hinter das Passwort
|
||
eine Auge-Symbol zum aufdecken des Passwortes, so kann ich prüfen, ob
|
||
die Daten übernommen wurden") — Augen-Icon neben dem Passwortfeld im
|
||
Verbinden-Tab, schaltet zwischen `SecureField` und `TextField` um.
|
||
War das Diagnose-Werkzeug, das die beiden folgenden Bugs erst
|
||
aufdeckte.
|
||
2. **Bug 35: Schlüsselbund teilte sich einen Eintrag zwischen zwei
|
||
Routern** — beim Umschalten zwischen zwei "Bekannte Router"-Einträgen
|
||
(seit M26 korrekt als getrennte Einträge geführt) blieb das
|
||
angezeigte Passwort trotzdem identisch. Aufwendig eingegrenzt: ein
|
||
temporärer Debug-Print bestätigte zunächst, dass `ConnectViewModel`s
|
||
`@Published`-Werte bei jedem Klick korrekt gesetzt wurden (auch die
|
||
View selbst rendert nachweislich jedes Mal frisch, per zweitem
|
||
Debug-Print in `mainContent` bestätigt) — zwei SwiftUI-`.id()`-
|
||
Reparaturversuche (erst auf Section-, dann auf Feld-Ebene) liefen
|
||
deshalb ins Leere, weil das eigentliche Problem gar keins der
|
||
Anzeige war. Tatsächliche Ursache: `KeychainService` speichert
|
||
Passwörter nur nach `"username@host"` — zwei physisch verschiedene
|
||
Router mit identischem Host+Benutzername (Werks-Adresse) teilen sich
|
||
dadurch denselben Schlüsselbund-Eintrag, unabhängig davon, dass
|
||
`SavedRoutersStore` sie seit M26 bereits korrekt als getrennte
|
||
Einträge führt. Fix: `KeychainService.save`/`loadPassword`/
|
||
`deletePassword` bekommen einen optionalen `serialNumber`-Parameter,
|
||
der den Account-Key auf `"username@host#seriennummer"` erweitert;
|
||
`loadPassword` fällt auf den alten, unqualifizierten Key zurück,
|
||
wenn kein qualifizierter Eintrag existiert (Rückwärtskompatibilität
|
||
für vor diesem Fix gespeicherte Passwörter). `ConnectViewModel.
|
||
connect()`s Passwort-Speicherung musste dafür hinter den
|
||
erfolgreichen Verbindungsaufbau verschoben werden (die Seriennummer
|
||
ist vorher noch nicht bekannt). Die beiden zuvor eingebauten
|
||
`.id()`-Umwege wieder entfernt, da wirkungslos und irreführend
|
||
(falsche Fehlerursache im Kommentar).
|
||
3. **Bug 36, direkt danach beim selben Live-Test gefunden: neuer Router
|
||
zeigte gar keine Routerboard-Infos, keine Seriennummer** — derselbe
|
||
Root Cause wie der vorherige "Jetzt prüfen"-Fehler (`/system package
|
||
update`, siehe oben): `/system routerboard print ... terse` schlägt
|
||
auf diesem Router/dieser RouterOS-Version mit `"expected end of
|
||
command (line 1 column N)"` fehl statt mit dem bisher einzig
|
||
abgefangenen `"bad parameter terse"`. `fetchMenuItems`s
|
||
Singleton-Fallback (Bug 8/20) griff deshalb nicht,
|
||
`routerBoardInfo` blieb `nil` (best-effort, Fehler wurde still
|
||
verschluckt) — betraf gleichzeitig die Seriennummer (M26) und die
|
||
komplette Routerboard-Anzeige im Verbinden-Tab. Fix:
|
||
`SSHTransport.fetchMenuItems`s Erkennung in eine neue
|
||
`rejectsTerse(_:)`-Hilfsfunktion ausgelagert, die jetzt beide
|
||
bekannten Formulierungen abfängt. Sicher eingegrenzt, da die
|
||
geprüfte Fehlermeldung ausschließlich vom exakten Aufruf `"<menuPath>
|
||
print without-paging terse"` stammt — jeder Parserfehler dort betrifft
|
||
per Konstruktion das anhängende `"terse"`. Behebt nebenbei auch den
|
||
"Jetzt prüfen"-Fehler von vorhin (`/system package update`), da
|
||
beide über denselben Code-Pfad laufen. Live bestätigt
|
||
("passt, funktioniert").
|
||
|
||
## Nächste Schritte
|
||
|
||
1. ~~M16: restliche `RouterOSSchemaCatalog.swift`-Sektionen übersetzen~~,
|
||
~~Verbinden-Tab zweisprachig~~ und ~~`L10n.t(...)`-Umstellung auf die
|
||
übrigen vier Tabs (Einrichten/Übersicht/LAN-Scanner/Sicherungen)~~ —
|
||
alles erledigt, live bestätigt ("funktioniert"). Damit sind alle
|
||
fünf Tabs vollständig zweisprachig (DE/EN).
|
||
2. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
|
||
2. ~~Rest von M7: REST-Schreibtest an einem Gerät mit aktivem
|
||
`www-ssl`~~ — erledigt (2026-09-16), siehe "Bekannte Einschränkungen"
|
||
oben (Bug 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 `<Zahl><d/h/m/s>` bzw. eine nackte
|
||
Sekundenzahl, kein `HH:MM:SS`; der alte Default wäre beim Öffnen
|
||
stillschweigend als "leer" geparst worden). Vor der Umstellung live
|
||
geprüft (Test-Einträge angelegt und sofort wieder entfernt): RouterOS
|
||
akzeptiert das Suffix-Format bei allen drei Feldern
|
||
(`persistent-keepalive=25s`, Scheduler-`interval=30s`,
|
||
Netwatch-`interval=30s`, jeweils im `print terse` bestätigt). Live in
|
||
der App bestätigt ("passt, live bestätigt"). Geräte-Tab hat weiterhin
|
||
keine Zeitfelder, daher nicht betroffen.
|
||
6. ~~Firewall-NAT-/Filter-`add`-Befehle sind bei wiederholtem Wizard-Lauf
|
||
weiterhin nicht idempotent~~ — behoben (2026-09-16): im Gegensatz zu den bei Bug 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.
|