M26: SavedRouter bekommt ein optionales serialNumber-Feld. Zwei
physisch verschiedene Router mit identischem Host+Benutzername
(Werks-Adresse) blieben bisher ein gemeinsamer "Bekannte Router"-
Eintrag; recordSuccessfulConnection matcht jetzt zusätzlich nach
Seriennummer, mit sauberer Migration bestehender Einträge ohne
Seriennummer. Drei neue Tests.
M27, beim Live-Test von M26 gefunden:
- Passwort-Anzeige-Button (Augen-Symbol) im Verbinden-Tab
- Bug 35: KeychainService speicherte Passwörter nur nach
"username@host" — beide Router teilten sich denselben
Schlüsselbund-Eintrag trotz getrennter SavedRouter-Einträge. Fix:
optionaler serialNumber-Parameter qualifiziert den Account-Key,
mit Fallback auf den alten Key für bereits gespeicherte Passwörter.
- Bug 36: neuer Router zeigte keine Routerboard-Infos/Seriennummer —
derselbe Root Cause wie der zuvor gemeldete "Jetzt prüfen"-Fehler:
/system routerboard und /system package update scheitern auf diesem
Router mit "expected end of command" statt dem bisher einzig
abgefangenen "bad parameter terse". SSHTransport.fetchMenuItems
erkennt jetzt beide Formulierungen.
Alles live bestätigt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
M17: "Bekannte Router" im Verbinden-Tab (SavedRouter/SavedRoutersStore),
Standort-Freitextfeld, Scroll-Cap ab 4 Einträgen. Bugfix: Umbenennen-
TextField steckte in einem sich selbst deaktivierenden Button.
M18: Live-Traffic-Punkt an Interfaces (InterfaceTrafficMonitor, eigene
SSH-Verbindung, monitor-traffic-Polling). Dabei zwei reale CLI-Parser-Bugs
gefunden und gefixt: running/disabled-Flags werden als Buchstaben vor dem
ersten Feld codiert, nicht als key=value; monitor-traffic liefert
"50.7kbps" statt einer reinen Zahl.
M19: Übersicht-Tab — animierte Flussrichtung auf allen Verbindungslinien
(TimelineView+dashPhase), frei verschiebbare Knoten mit Live-folgenden
Linien, Zurücksetzen-Button.
Zusätzlich (noch nicht live getestet, nur Build+Unit-Tests grün):
LAN-Port-Konflikt-Prüfung im Einrichten-Assistenten mit doppelter
Sicherheitsbestätigung, "Fertig"-Button nach erfolgreichem Anwenden.
82 Tests grün. HANDOFF.md/README.md/Manual.md/CHATLOG.md aktualisiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
Verbinden-Tab zeigt jetzt die volle Routerboard-Info (Modell/Revision/
Seriennummer/Firmware, aus /system routerboard) sowie einen
RouterOS-Software-Update-Check (/system package update: Kanal/
installierte/neueste Version/Status, "Jetzt pruefen"/"Update
installieren"), dazu "Firmware aktualisieren" fuer die
Routerboard-Bootloader-Firmware und "Jetzt neu starten" danach. Alle
vier Aktionen live bestaetigt, inklusive der zuvor unsicheren Frage, ob
/system routerboard upgrade's normalerweise interaktive Bestaetigung
den nicht-interaktiven SSH-Weg dieser App blockiert (tut es nicht).
Bug 20 gefunden und gefixt: fetchMenuItems' Singleton-Fallback (Bug 8)
reagierte nur auf eine geworfene Exception fuer "bad parameter terse",
aber RouterOS liefert diesen Fehler fuer /system routerboard mit
Exit-Code 0 zurueck (dasselbe Bug-10-Muster, diesmal beim Lesen statt
Schreiben) - die Routerboard-Sektion blieb dadurch leer, ohne Fehler.
Fix: zusaetzlich den Output-Text selbst pruefen, nicht nur die Exception.
Design-Durchgang: Verbinden-Detailseite/Sicherungen/Geraete liefen auf
nackter List ohne Rahmen - umgestellt auf Form+.formStyle(.grouped),
denselben nativen macOS-Karten-Look, den Wizard und Experte-Tab schon
hatten, fuer eine einheitliche App. Dark Mode auf Nachfrage gepueft und
ohne Codeaenderung bestaetigt funktionierend.
HANDOFF.md/CHATLOG.md aktualisiert: M14, Bug 20, Design-Durchgang,
Dark-Mode-Bestaetigung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ
ConnectionService.disconnect() existierte schon (bisher nur intern
nach Werksreset genutzt), aber ohne UI-Zugang im Verbinden-Tab selbst
-- Nutzer musste die App neu starten, um eine Verbindung sauber zu
beenden. Jetzt als Button direkt neben dem "Verbunden"-Status.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
Größte offene Härtungslücke geschlossen: SSHTransport nutzte bisher
.acceptAnything() für Host-Key-Validierung, akzeptierte also jeden
Schlüssel ohne Prüfung -- ein Man-in-the-Middle im lokalen Netz wäre
unbemerkt geblieben. Jetzt Trust-on-first-use wie beim REST-Zertifikat:
- SSHHostKeyFingerprint: SHA256 über NIOSSHPublicKey.write(to:) (die
SSH-Wire-Format-Bytes des Schlüssels) -- exakt die Bytes, die auch
OpenSSH für seine SHA256:-Fingerabdrücke hasht. Per Unit-Test gegen
einen echten ssh-keygen-erzeugten Testschlüssel kreuzgeprüft
(SHA256:Hllxv6LLoHl2XTIXGGjUYJHbPFoH2F7iMrR74C5J95g), nicht geraten.
- SSHHostKeyTrustStore: UserDefaults-Persistenz pro Host, Pendant zu
CertificateTrustStore.
- SSHTransport conformt jetzt selbst zu NIOSSHClientServerAuthentication-
Delegate (wie RestTransport zu URLSessionDelegate) und übergibt sich
selbst als .custom(self) Host-Key-Validator.
- ConnectionService: neuer State .needsSSHHostKeyConfirmation, eigener
Bestätigungs-Retry-Pfad (trustCurrentSSHHostKeyAndRetry), analog zum
bestehenden Zertifikat-Flow.
- ConnectView: zweiter Bestätigungsdialog mit Warnhinweis, dass ein
geänderter Fingerabdruck bei zuvor schon verbundenen Routern auf ein
manipuliertes Netzwerk hindeuten könnte.
BackupService/FactoryResetService bekommen die TOFU-Prüfung automatisch
mit (SSHTransport-Default-Parameter, gleicher UserDefaults-Speicher),
ohne eigene Bestätigungs-UI -- in der Praxis unkritisch, da der
Verbinden-Tab das Vertrauen immer zuerst herstellt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
"Jetzt sichern"-Button erscheint sobald verbunden, noch bevor man in
den Einrichten-Tab wechselt -- Nutzer-Wunsch, um vor Änderungen ohne
Tab-Wechsel sichern zu können. Zeigt zusätzlich "Zuletzt gesichert: ..."
für den aktuell verbundenen Host, falls schon eine Sicherung existiert
(gefiltert auf den aktuellen Host, damit es bei mehreren Routern nicht
verwirrt). Nutzt dieselbe BackupViewModel/BackupService-Logik wie der
Sicherungen-Tab, eigene Instanz.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
.help(...)-Tooltips auf jedem Eingabefeld/Picker/Toggle in Connect-
sowie allen Einrichten-Schritten (WAN/LAN/VLAN/WLAN/Firewall) --
kurze Erklärung was der Wert bedeutet und welche Auswirkung er hat,
passend zum Laien-Anspruch der App.
Sicherungen-Tab: Speicherort jetzt änderbar (Ordner wählen über
nativen macOS-Dialog, Zurücksetzen auf Standard). BackupService hält
den gewählten Pfad in UserDefaults (BackupService.customDirectoryURL),
fällt ohne Auswahl weiter auf ~/Library/Application Support/... zurück.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
connectionService war nur über viewModel.connectionService erreichbar,
ein zwei Ebenen tief verschachteltes ObservableObject. SwiftUI abonniert
automatisch nur Objekte, die eine View direkt per @StateObject/
@ObservedObject hält — nicht transitiv erreichte. Dadurch blieb die
Oberfläche beim echten Verbindungstest (physisches Mikrotik-Gerät)
komplett unverändert hängen, obwohl die Verbindung im Hintergrund
korrekt lief oder fehlschlug.
Fix: ConnectView hält connectionService jetzt zusätzlich selbst als
@ObservedObject, direkt von der App-Ebene durchgereicht.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
Neuer "Einrichten"-Tab führt durch Internet-Anschluss (DHCP/statisch/
PPPoE) und Heimnetzwerk+DHCP-Server, zeigt vor dem Anwenden eine
Klartext-Übersicht (optional mit den exakten RouterOS-Befehlen) und
erstellt automatisch eine Sicherung, bevor Änderungen geschrieben
werden. Änderungen laufen über beide Transporte: CLI-Zeile für SSH,
JSON-POST für REST — beide aus einem gemeinsamen RouterOSCommand
gebaut. RouterOS-CLI-Syntax ist Standard und langjährig stabil, aber
nicht gegen ein echtes Gerät verifiziert; deshalb die Detailanzeige
im Übersichtsschritt vor dem Anwenden.
ConnectionService ist jetzt der einzige App-weite Zustand (ersetzt
SessionStore) und wird explizit an alle drei Tabs durchgereicht.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
Config-Export (/export terse) über eine dedizierte SSH-Verbindung,
unabhängig vom aktiven Live-Transport (REST oder SSH) — RouterOS' REST-API
hat keinen generischen Export-Endpunkt. Lokale Sicherungen unter
~/Library/Application Support/RouterOSAssistant/Backups/. Neuer
"Sicherungen"-Tab mit manuellem "Jetzt sichern"-Button, SessionStore
teilt die Zugangsdaten der aktiven Verbindung zwischen den Tabs.
Der bestehende Connect-Schritt dient bereits als reiner Lese-Modus
(nur GET-Aufrufe) — ein zusätzlicher Dry-Run-Schalter entfällt, da es
vor M3 noch keine Schreibpfade gibt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
XcodeGen-basiertes SwiftUI-Projekt für den RouterOS-Interview-Assistenten.
Erster Wizard-Schritt: Verbindung zu Mikrotik-Geräten per REST-API
(RouterOS >=7.1) mit SSH-CLI-Fallback für ältere Firmware, Zertifikats-
TOFU-Bestätigung, Zugangsdaten im Keychain. Unit-Tests für CLI-Parser
und Fallback-Logik.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW