M13: Backup-Wiederherstellung, mit Modell-Schutz und Login-Erhalt

Letzter offener Punkt aus HANDOFF.md: gespeicherte .rsc-Backups lassen
sich jetzt wieder einspielen. Weg: Backup-Datei per SFTP auf den Router
hochladen (Citadel, die bereits eingebundene SSH-Bibliothek, hat einen
SFTP-Client), dann RouterOS' offiziell dokumentierter Restore-Weg in
einem Rutsch: /system reset-configuration no-defaults=yes
run-after-reset=<datei> (kompletter Wipe + sofortiges Wiederanwenden,
sicherer als ein Re-Import auf eine bestehende, andere Config).

Vor dem Bestätigungsdialog wird das Routermodell abgeglichen
(/system routerboard prints "model"-Feld gegen die "# model = ..."-
Kopfzeile der Sicherung) und bei Mismatch komplett blockiert, um ein
Brick-Risiko durch falsches Modell zu vermeiden - der Dialog selbst warnt
zusaetzlich prominent davor.

Ein ernster Bug live gefunden und gefixt: der erste echte Restore-Test
sperrte den Router komplett aus (RouterOS exportiert nie Passwoerter,
no-defaults=yes loescht zusaetzlich den Werks-Admin-Account), nur per
Hardware-Reset behebbar. Fix: das aktuell verwendete App-Login wird jetzt
vorne ins Restore-Skript eingefuegt, noch vor dem eigentlichen
Sicherungsinhalt, da RouterOS den Import beim ersten Fehler irgendwo im
Skript komplett abbricht. Zwei Tests fuer die Escaping-Logik ergaenzt.

Nebenbei: BackupListView mit den neuen Restore-Dialogen liess sich nicht
mehr kompilieren (SwiftUI-Typpruefung timeoutete bei der langen
Modifier-Kette) - Restore-Dialoge in eine eigene @ViewBuilder-Property
ausgelagert.

HANDOFF.md/CHATLOG.md aktualisiert: M13, Bug 18+19, "Backup-
Wiederherstellung fehlt" aus den offenen Punkten entfernt, Hinweis auf
den zweiten (ungewollten) Werksreset waehrend der Session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ
This commit is contained in:
Kay
2026-09-14 15:21:05 +02:00
co-authored by Claude Sonnet 5
parent 8e99ab8e41
commit 20f8d5e0bf
7 changed files with 502 additions and 70 deletions
+114 -62
View File
@@ -13,23 +13,28 @@ beide live gegen den hEX-Testrouter verifiziert, sieben Bugs dabei
gefunden und gefixt (Bug 713). Alles bis hierhin committed
(`c9ecd3a`).
**Neu in dieser Session:** zwei weitere Tabs. **M11 "Übersicht"** zeigt
die komplette aktuelle Router-Konfiguration als Diagramm (Interfaces →
IP-Adressen → DHCP/Pools → Routen → Firewall/NAT, mit echten, aus
RouterOS' eigenen Referenzfeldern abgeleiteten Verbindungslinien,
farblich nach Verbindungsart getrennt, Hover/Klick hebt Zusammenhänge
hervor). **M12 "Geräte"** ist ein LAN-Scanner (DHCP-Leases + ARP +
Bridge-Host-Tabelle, gruppiert als eine Tabelle pro physischem Port) mit
Rechtsklick-Aktion "Feste IP zuweisen" (RouterOS' "Make Static", per
`/ip dhcp-server lease make-static`) 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). Beide Tabs live gegen den hEX-Testrouter verifiziert — M12
dabei besonders gründlich, weil die Fest/Dynamisch-Erkennung und die
Zuweisung selbst sich als überraschend zäh erwiesen: **vier** echte Bugs
gefunden und gefixt (Bug 1417 unten), am Ende vom Nutzer selbst
bestätigt ("das funktioniert jetzt super", und für den Rückweg "das hat
sofort funktioniert"). Release-Build unter
**Neu in dieser Session:** drei weitere Tabs/Features. **M11 "Übersicht"**
zeigt die komplette aktuelle Router-Konfiguration als Diagramm
(Interfaces → IP-Adressen → DHCP/Pools → Routen → Firewall/NAT, mit
echten, aus RouterOS' eigenen Referenzfeldern abgeleiteten
Verbindungslinien, farblich nach Verbindungsart getrennt, Hover/Klick
hebt Zusammenhänge hervor). **M12 "Geräte"** ist ein LAN-Scanner
(DHCP-Leases + ARP + Bridge-Host-Tabelle, gruppiert als eine Tabelle pro
physischem Port) mit Rechtsklick-Aktion "Feste IP zuweisen" (RouterOS'
"Make Static", per `/ip dhcp-server lease make-static`) sowie ihr
Gegenstück "Feste Zuweisung entfernen" (RouterOS kennt kein
`make-dynamic`, nur `remove`vorher per Recherche-Agent gegen die
offizielle Doku geprüft statt geraten). **M13 Backup-Wiederherstellung**
im Sicherungen-Tab: `.rsc`-Backup per SFTP hochladen, dann RouterOS'
dokumentierter `reset-configuration`+`run-after-reset`-Restore-Weg, mit
Modell-Abgleich vor dem Bestätigungsdialog (Brick-Schutz bei falschem
Routermodell) und automatischem Login-Erhalt (siehe Bug 18). Alle drei
Tabs/Features live gegen den hEX-Testrouter verifiziert, dabei M12 (vier
Bugs, 1417) und M13 (ein besonders ernster Bug, 18: erster Testlauf
sperrte den Router komplett aus, nur per Hardware-Reset behebbar) beide
zäher als erwartet, am Ende aber jeweils vom Nutzer selbst bestätigt
("das funktioniert jetzt super" / "das hat sofort funktioniert" / "gut,
das hat funktioniert"). Release-Build unter
`/Applications/RouterOS Assistant.app` ist auf aktuellem Stand.
## Ziel
@@ -92,13 +97,13 @@ RouterOSAssistant/
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)
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
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
Features/
@@ -307,6 +312,37 @@ erreichen (siehe Bug 1 unten).
— 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.
**Lehren:** Citadel/NIOSSH-Fehler immer mit `String(describing:)` loggen,
nie `.localizedDescription`. Jede View, die ein ObservableObject aus einem
@@ -345,7 +381,18 @@ nicht, dass `get [find ...] feld` als eine Einheit genauso funktioniert.
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.
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.
## `xcodebuild test` hängt — Gatekeeper, kein Code-Bug
@@ -364,15 +411,21 @@ wiederholen.
## Bekannte Einschränkungen (bewusst, nicht vergessen)
- **Backup-Wiederherstellung fehlt komplett** — Nutzer wies explizit
darauf hin (2026-09-14): `BackupService` kann Backups nur erzeugen
(`createBackup`) und auflisten (`listBackups`), es gibt **keine**
Funktion, ein gespeichertes `.rsc`-Backup zurück auf den Router zu
spielen. Einzige verwandte Funktion ist "Werkseinstellungen
wiederherstellen" (`FactoryResetService`, Gefahrenzone im
Sicherungen-Tab) — das ist RouterOS' eigener Werksreset, kein Zurück-
spielen einer konkreten Sicherung. Noch nicht umgesetzt, für später
vorgemerkt (siehe Nächste Schritte).
- **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** — auf beiden bisherigen
Testgeräten war `www-ssl` (Port 443) aus, jeder Schreibtest lief über
SSH. Der REST-`apply()`-Pfad (`POST`/`PATCH`, `findItemID` für `.set`)
@@ -612,6 +665,18 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
`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.
## Nächste Schritte
@@ -632,42 +697,29 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
bestätigt (siehe oben), der Moduswechsel im Wizard noch nicht.
6. M10: WLAN-Schemas (an Gerät mit WLAN-Chip), Bonding, PPPoE-Client
(mit echten oder Test-ISP-Zugangsdaten) noch gegen Hardware verifizieren.
7. `dhcp5`/`pool5` auf `ether5` sind Test-Überbleibsel aus einer früheren
Session — mit dem Nutzer klären, ob die noch gebraucht werden oder
aufgeräumt werden können (aktuell blockieren sie `ether5` für einen
neuen direkt-auf-dem-Port angelegten DHCP-Server).
8. **Nicht anrühren:** `test-vlan` (VLAN-ID 20, ether5), `testpool`
(192.168.90.10-250) und `testdhcp` (DHCP-Server auf `test-vlan`) sind
**echte, vom Nutzer selbst über die App angelegte Konfiguration** aus
dieser Session, keine Test-Artefakte zum Aufräumen.
9. Optional, kleinere Politur: den neuen Dauer-Editor
7. Optional, kleinere Politur: den neuen Dauer-Editor
(`RouterOSFieldSchema.Kind.duration`) auch auf weitere Zeitwert-Felder
anwenden, die bisher nur Text mit Beispiel-Tooltip sind — WireGuard-
Peer "Keepalive", Scheduler "Intervall", Netwatch "Prüf-Intervall".
10. **Backup-Wiederherstellung bauen** — vom Nutzer explizit gewünscht,
noch nicht begonnen. Vermutlich: `BackupService` um eine
`restoreBackup(_:for:)`-Funktion erweitern, die den Inhalt eines
gewählten `.rsc`-Backups über SSH auf den Router einspielt (RouterOS
kann `.rsc`-Skripte per `/import file-name=...` ausführen, dazu muss
die Datei vorher aufs Gerät hochgeladen werden — Weg noch nicht
verifiziert, vor dem Bauen live prüfen statt zu raten), plus ein
"Wiederherstellen"-Button pro Eintrag im Sicherungen-Tab
(`BackupListView`) mit Bestätigungsdialog (ähnlich der bestehenden
Werksreset-Gefahrenzone).
11. **`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
prüfen** — live als falsch bestätigt für `/ip dhcp-server lease`
(Bug 15). Betrifft potenziell jedes `.set`/`.remove` im Experte-Tab.
Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig
vorhandenen Einträgen `:put [<Pfad> find]` und `<Pfad> print terse`
unabhängig ausführen und die Reihenfolgen von Hand vergleichen.
12. Optional: den neuen Dauer-Editor auch für den Geräte-Tab nutzen, falls
dort künftig Zeitfelder (z.B. eine Lease-Zeit-Anzeige) dazukommen.
13. `Kay-Uwes-iMac` (ether2, `defconf`) und der Test-Laptop `DEDELLB2M6GK3`
(ether3, `dhcp5`/`defconf` je nach Testrunde) sind **echte Geräte des
Nutzers**, mit denen M12 live durchgetestet wurde — keine
Test-Artefakte, nicht aufräumen. Beide Geräte am Ende der Session
wieder dynamisch (kompletter Kreislauf fest→entfernt→dynamisch vom
Nutzer selbst am Laptop bestätigt).
Optional auch für den Geräte-Tab, falls dort künftig Zeitfelder
dazukommen.
8. **`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
prüfen** — live als falsch bestätigt für `/ip dhcp-server lease`
(Bug 15). Betrifft potenziell jedes `.set`/`.remove` im Experte-Tab.
Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig
vorhandenen Einträgen `:put [<Pfad> find]` und `<Pfad> print terse`
unabhängig ausführen und die Reihenfolgen von Hand vergleichen.
9. **Router wurde zwischen M12 und M13 vom Nutzer komplett auf
Werkseinstellungen zurückgesetzt** ("damit wir sauber weitermachen
können"), und im Zuge des ersten M13-Testlaufs (Bug 18, Login-Sperre)
nochmal per Hardware-Reset zurückgesetzt. `test-vlan`/`testpool`/
`testdhcp`/`dhcp5`/`pool5` aus früheren Sessions existieren dadurch
nicht mehr — **nicht mehr relevant, nicht danach suchen.** Aktueller
Stand: `Kay-Uwes-iMac` an ether2 und Test-Laptop `DEDELLB2M6GK3` an
ether3, beide dynamisch, plus was auch immer die zuletzt erfolgreich
getestete Backup-Wiederherstellung zurückgespielt hat — vor
Annahmen über den genauen aktuellen Stand lieber neu per Geräte-/
Übersicht-Tab prüfen statt auf ältere Einträge hier zu vertrauen.
Kein Gitea-Remote vorhanden — falls der Nutzer später eine Gitea-Instanz
aufsetzt (z.B. selbst gehostet auf der vorhandenen OMV-NAS, siehe