M17-M19: Bekannte Router, Live-Traffic-Anzeige, Übersicht-Animation+Drag

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
This commit is contained in:
Kay
2026-09-15 21:40:46 +02:00
co-authored by Claude Sonnet 5
parent 9b26677dc2
commit 31607b7270
24 changed files with 1400 additions and 61 deletions
+137
View File
@@ -1066,6 +1066,143 @@ Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und
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").
## Nächste Schritte