# Fundzettel
Notizzettel für Feedback/Funde des Users. Neue Einträge unten anhängen, Status pflegen.
**Status:** `offen` | `postponed` | `fixed`
---
## 2026-09-16
### 1. NTP Server bricht mit Error HTTP 400 ab
**Status:** fixed (live bestätigt)
**Gitea-Issue:** [#3](http://192.168.178.222:3500/kay/RouterOS/issues/3)
Ursache 1 (behoben): Experte-Schema PATCHte `servers` als kommagetrenntes Textfeld direkt auf `/system/ntp/client` — Feld existiert dort in RouterOS 7.x nicht, Server liegen in eigener Liste `/system ntp client servers`. Schema aufgeteilt: Singleton-Menü (enabled/mode) bleibt, neues Listen-Menü `/system ntp client servers` (Feld `address`) dazu.
Ursache 2 (behoben): Live-Test meldete beim Anlegen eines Server-Eintrags `HTTP 400: unknown parameter enabled` — Listen-Einträge in RouterOS nutzen durchgängig `disabled` (nicht `enabled`) zum Ein-/Ausschalten, wie der Rest des Schemas es auch macht. Feld in `RouterOSSchemaCatalog.swift` von `enabled`/"yes" auf `disabled`/"no" korrigiert. Build grün, noch nicht erneut live getestet.
### 2. Interfaces-Ansicht zu unübersichtlich
**Status:** fixed (live bestätigt)
**Gitea-Issue:** [#4](http://192.168.178.222:3500/kay/RouterOS/issues/4)
Interfaces-Einträge sehr umfangreich beim Öffnen. Zweispaltige Ansicht gewünscht, um Scrollen zu reduzieren.
Ursache: Der eigentliche Umfang kommt nicht von den kuratierten Feldern (bei "Alle Interfaces (generisch)" nur 3: Name/Kommentar/Deaktiviert), sondern vom "Weitere Parameter (frei)"-Block — jedes von RouterOS zurückgelieferte Feld, das nicht kuratiert ist, landet dort als eigene Zeile. Bei einem Ethernet-Port sind das leicht 30+ Zeilen. Fix: `ExpertMenuDetailView.swift` — dieser Block ist jetzt ein zweispaltiges `Grid` statt einer Liste (halbiert die Zeilenzahl), Sheet-Breite von 420 auf 640 erhöht. Betrifft alle Menüs mit vielen ungekuratierten Feldern, nicht nur Interfaces.
Nachbesserung 1 (User-Feedback: Zeilen zu eng, Spalten-Trennlinie gewünscht): verticalSpacing 8→16, horizontalSpacing 12→20, dünne vertikale Trennlinie zwischen den Spalten ergänzt.
Nachbesserung 2 (User-Feedback: Zeilen jetzt abgeschnitten, Fenster breiter machen und voll ausnutzen): Sheet-Breite minWidth 640→900 + idealWidth 900.
Nachbesserung 3 (User-Screenshot: an den äußeren Rändern noch viel ungenutzter Platz). Ursache gefunden: `TextField(titleKey:text:)` zeigt auf macOS den Titel als feste Beschriftung vor dem Wert an (kein iOS-Platzhalter, der beim Tippen verschwindet) — bei 30+ Zeilen fraß "Parameter"/"Wert" so bei jeder einzelnen Zeile erneut Breite. Fix: echte Grid-Spalten für Parameter/Wert mit einmaliger Kopfzeile statt Pro-Zeile-Label, leere TextField-Titel (`.textFieldStyle(.plain)`), Wert-Spalte mit `maxWidth: .infinity`.
Nachbesserung 4 (User-Feedback: letzte Zeile überschneidet sich mit dem Button "Parameter hinzufügen", zwischen Zeilen soll eine Trennlinie sein): horizontale `Divider()` nach jeder Zeile ergänzt (spannt alle 5 Grid-Spalten), 12pt Abstand unter dem Grid vor dem Button. Build grün, noch nicht visuell/live geprüft (kein Router hier erreichbar).
### 3. Übersicht: Fokus-Modus beim Anklicken eines Items
**Status:** fixed (live bestätigt)
**Gitea-Issue:** [#5](http://192.168.178.222:3500/kay/RouterOS/issues/5)
Wunsch: Klick auf einen Node in der Übersicht (Graph-Ansicht, `OverviewView.swift`) soll den Node in den freien Bereich schieben, alle anderen Nodes ausfaden, und die Parents/Childs des aktuell gewählten Nodes nach unten schieben und sauber neu anordnen.
Ist-Zustand (`OverviewView.swift`): Klick setzt aktuell nur `selectedNodeID` und highlightet die verbundenen Kanten/Nodes (`OverviewGraph.highlightedNodeIDs`) — Positionen ändern sich nicht. Es gibt bereits ein Spalten-Layout (`OverviewLayout`, feste `columnOrder`/`columnWidth`) plus manuelle Drag-Offsets pro Node (`nodeOffsets`), die on top der berechneten Grid-Positionen liegen und über einen "Zurücksetzen"-Button löschbar sind (nicht persistiert, geht bei Neuladen verloren).
Klärung (User-Antworten):
1. Horizontale Trennlinie über den Canvas; ausgewählter Node + Kette werden unterhalb dieser Trennlinie dargestellt (eigener Bereich, getrennt von der normalen Graph-Darstellung darüber).
2. Fokus-Modus ist temporär — aufhebbar (Klick auf leere Fläche / erneuter Klick), keine dauerhafte Layout-Änderung, `nodeOffsets` bleibt unberührt.
3. Ausfaden = abdunkeln (nicht ausblenden).
4. Komplette verbundene Kette (nicht nur 1 Ebene) — deckt sich mit dem bereits vorhandenen `highlightedNodeIDs` (rekursiv).
5. Ketten-Layout im unteren Bereich: gleiches Spalten-Layout wie oben (`OverviewLayout`-Logik wiederverwendet, nur auf die Kette beschränkt) — kein neuer Layout-Algorithmus nötig.
Umsetzung (`OverviewGraph.swift`, `OverviewView.swift`, `OverviewGraphTests.swift`, `L10n.swift`):
- Neue Methode `OverviewGraph.connectedChain(startingAt:)` — volle rekursive Kette unabhängig vom Node-Typ (bewusst getrennt von `highlightedNodeIDs`, das nur bei Interfaces mehr als 1 Hop expandiert — eine andere, bereits live bestätigte, bewusst enger gefasste Funktion). Mit Test abgedeckt.
- `OverviewView`: Fokus-Modus hängt direkt an `viewModel.selectedNodeID` (kein separater Zustand) — Klick auf Node selektiert wie bisher, macht das aber jetzt gleichzeitig zum Fokus-Trigger; erneuter Klick auf denselben Node hebt ihn wieder auf (bestehende Toggle-Logik, kein neuer Code nötig).
- Kette wird per `OverviewLayout.layout(for:)` auf ein gefiltertes Sub-Graph-Objekt neu angeordnet (`focusSubLayout`) und unterhalb einer Trennlinie eingeblendet (`effectivePositions` verschiebt nur Kettenknoten dorthin, alle anderen bleiben an ihrer normalen Position).
- Ausfaden: nicht-Ketten-Nodes bekommen `opacity(0.25)`; `EdgesCanvas` bekommt die Kette als `highlightedNodeIDs` übergeben, wodurch dessen bereits vorhandene Dimm-Logik für Kanten automatisch mitgreift (keine Kante kann je über die Trennlinie laufen, da eine Kette per Definition die transitive Hülle ist).
- Leere-Fläche-Klick hebt Fokus auf: `EdgesCanvas` bekommt einen neuen `onBackgroundTap`-Callback, der bei ihrem bereits vorhandenen Tap-Gesture-Handler greift, wenn kein Edge getroffen wurde.
- Dragging ist für Nodes deaktiviert, solange sie im Fokus-Streifen sitzen (Position wird bei jeder Auswahl neu berechnet, ein gespeicherter Drag-Offset würde sonst nach Verlassen des Fokus unerwartet wieder auftauchen).
Build grün, alle Tests (inkl. neuem `testConnectedChainExpandsTransitivelyForEveryNodeKind`) grün, App startet ohne Absturz. Noch nicht visuell/live geprüft — kein Tool für native-macOS-UI-Interaktion hier verfügbar, bitte im Übersicht-Tab gegentesten (Klick auf Node → Fokus-Streifen unten, Klick auf leere Fläche/erneuter Klick → Fokus verlassen, anderen Kettenknoten anklicken → Fokus wechselt).
Nachbesserung 1 (User-Feedback: nach Klick soll die Fokus-Ebene in den sichtbaren Viewport geschoben werden, ohne Scrollbars) — inzwischen durch Nachbesserung 2 ersetzt: `viewportSize` per `GeometryReader` mitverfolgt, Auto-Zoom+Auto-Scroll auf einen `"focusAnchor"`-Marker im Fokus-Streifen.
Nachbesserung 2 (User-Feedback: statt Trennlinie/eingebettetem Bereich ein separates Panel mit Breite des Original-Viewports, Höhe automatisch an Inhalt angepasst, eigener Close-Button) — ersetzt Nachbesserung 1 komplett: Der Divider-Ansatz (Kette in denselben zoombaren Canvas verschoben, Auto-Fit-Zoom+Auto-Scroll) ist raus. Hauptdiagramm bleibt jetzt unverändert an Ort und Stelle (keine Repositionierung mehr, kein `combinedCanvasSize`/`fitScale`/`ScrollViewReader` mehr) — nicht zur Kette gehörende Nodes werden weiterhin abgedunkelt (`opacity(0.25)`). Neues `focusPanel(subGraph:subLayout:)` erscheint als eigener Bereich unterhalb der Haupt-`ScrollView` (in einem `VStack`, kein Overlay): Breite = `viewportSize.width` (Breite der Haupt-Diagramm-Scrollview, per `GeometryReader` gemessen), Höhe = `subLayout.canvasSize.height + 56` (passt sich der Kettengröße an, kein künstliches Clamping). Eigene Kopfzeile mit "Fokus"-Titel + `xmark.circle.fill`-Button zum Schließen. Kette selbst per eigenem `EdgesCanvas`+Node-Loop gerendert (gleiches `OverviewLayout`, alle internen Kanten hell da komplett als `highlightedNodeIDs` durchgereicht), eigener Hover/Tooltip-State (`panelHoveredEdge`/`panelHoverPoint`), da eigener Koordinatenraum, getrennt von der Haupt-Diagramm-Tooltip-Logik. Klick auf einen Node im Panel wechselt den Fokus (kein Toggle-Aus mehr dort, dafür gibt's jetzt den Close-Button); Hintergrund-Klick/erneuter Klick auf denselben Node oben im Hauptdiagramm schließen weiterhin zusätzlich. Drag-Deaktivierung für Kettenknoten ist wieder raus (nicht mehr nötig, da Hauptdiagramm nicht mehr verschoben wird). Build+Tests grün, App startet ohne Absturz. Noch nicht live geprüft.
Nachbesserung 3 (User-Feedback: von Panel auf Popup wechseln) — inzwischen durch Nachbesserung 4 ersetzt: eigenes `ZStack`-Overlay mit Scrim + abgerundeter Karte statt fester VStack-Zeile.
Nachbesserung 4 (User-Feedback: "passt nicht", stattdessen wie beim Bearbeiten-Formular — Größe/Breite automatisch anpassen, Scrollbalken verhindern, Close-Button) — Sizing-Teil bleibt, Präsentation wurde in Nachbesserung 5 wieder korrigiert: natives `.sheet(isPresented:)` (gleiches Muster wie `ExpertItemEditView`), kein `ScrollView` mehr, `.fixedSize()` auf `subLayout.canvasSize` (exakte Content-Größe statt fixer Viewport-Breite).
Nachbesserung 5 (User-Feedback: Seitenleiste muss gleichzeitig bedienbar bleiben, um Knoten zu bearbeiten): `.sheet` ist ein echtes macOS-Modal — blockiert das gesamte Fenster inkl. der rechten Seitenleiste (`detailPanel`, "Bearbeiten"-Button), das war der Bug. Zurück auf nicht-modales `ZStack`-Overlay + Scrim (wie Nachbesserung 3), aber mit der exakten Content-Größe aus Nachbesserung 4 (kein `ScrollView`, `.fixedSize()` auf `subLayout.canvasSize`) statt der alten festen Viewport-Breite. `isFocusPresented`-Binding wieder entfernt. Sidebar bleibt jetzt durchgehend bedienbar, da sie eine eigene `HSplitView`-Pane ist, die vom Overlay nie erreicht wird. Build+Tests grün, App startet ohne Absturz.
Nachbesserung 6 (User-Wunsch: den Close-Button-Stil aus dem Fokus-Popup bei allen anderen Formularen/Sheets ebenfalls nachziehen) — betrifft nicht mehr nur Fund #3, sondern alle Sheet-Formulare der App: `ExpertItemEditView` (Experte-Bearbeiten-Sheet, `ExpertMenuDetailView.swift`) bekommt eine neue Kopfzeile mit Menü-Titel + `xmark.circle.fill`-Button (schließt via `cancelEditing()+dismiss()`, wie das bestehende "Abbrechen" unten — bleibt zusätzlich erhalten, da es semantisch mehr ist als nur schließen). Die drei Devices-Sheets (`RawFieldsSheet`, `NetworkToolResultSheet`, `PortScanResultSheet` in `DevicesView.swift`) hatten je nur einen "Schließen"-Textbutton unten — durch dieselbe Kopfzeile mit X-Button ersetzt (kein doppelter Close mehr). Neue L10n-Zeile "Schließen, ohne zu speichern" ergänzt.
Nachbesserung 7 (User-Feedback: alle Close-Buttons müssen in der Header-Leiste des Popups liegen, nicht doppelt): das untere "Abbrechen" in `ExpertItemEditView` war seit Nachbesserung 6 doppelt zum neuen Kopfzeilen-X — entfernt, einziger Close-Button ist jetzt der im Header. "Anlegen"/"Speichern" bleibt unten als reine Save-Aktion (kein Close). Geprüft: alle übrigen "Abbrechen"-Vorkommen in der App gehören zu nativen `.confirmationDialog`/`.alert`-Dialogen (keine Header-Leiste, nicht betroffen).
Nachbesserung 8 (User-Wunsch: Verhalten+Layout des Close-Buttons bei ALLEN Popups exakt wie beim Fokus-Popup der Übersicht) — Rückfrage gestellt, ob der Header beim Experte-Bearbeiten-Sheet dafür aus der scrollenden Form raus und fest oben stehen soll (mehr Umbau) oder nur optisch angeglichen wird; User: fest oben, wie Fokus-Popup. Umgesetzt in allen vier Popups (`ExpertItemEditView`, `RawFieldsSheet`, `NetworkToolResultSheet`, `PortScanResultSheet`): identische Struktur wie `OverviewView.focusPanel` — `HStack{Titel; Spacer; xmark.circle.fill-Button}.padding(10)` gefolgt direkt von `Divider()`, außerhalb jeder Form/ScrollView/Section, bleibt beim Scrollen des restlichen Inhalts immer sichtbar. Beim Experte-Sheet wanderte der Header dafür vor die `Form` (Mindesthöhe leicht erhöht: 480→520/idealHeight 620→660, damit durch den zusätzlichen festen Header nichts gestaucht wirkt). Bei den drei Devices-Sheets wurde die bisher gemeinsame Erklärungs-Caption+Divider+Content-Gruppe vom Header getrennt und bekam eigenes Padding. Build+Tests grün, App startet ohne Absturz. Noch nicht live geprüft — bisher nur der Fokus-Popup-Teil (Fund #3) wurde live bestätigt "sieht gut", der Rest noch nicht.
### 4. LAN-Scanner: Sparkline-Breite + Abfrage-Intervall
**Status:** fixed (live bestätigt: "ja, alles flüssig" — auch 0,1s-Takt keine spürbare CPU-Last)
**Gitea-Issue:** [#6](http://192.168.178.222:3500/kay/RouterOS/issues/6)
Wunsch: Sparkline-Breite verdoppeln, Traffic-Abfrage auf 0,5s setzen (statt 3s).
Umsetzung: `TrafficSparkline`-Frame in `DevicesView.swift` von 50pt auf 100pt Breite; `DevicesViewModel.startTrafficPolling` von `.seconds(3)` auf `.milliseconds(500)`. Bewusst nur den LAN-Scanner-Poll geändert, nicht `ConnectViewModel`s separate 3s-Schleife (die nur den Link-Punkt treibt, nicht die Sparkline) — User-Scope war explizit "im lan-scanner".
Nachbesserung 1 (User-Wunsch: nochmal doppelt so breit): 100pt → 200pt.
Nachbesserung 2 (User-Wunsch: Trafficanzeige in MB, Sparkline-Zeitfenster auf 30s): `formatBitsPerSecond` (kbps/Mbps/Gbps auto-skaliert) ersetzt durch festes `formatMegabytesPerSecond` (Bits/8/1.000.000, 2 Nachkommastellen, "MB/s"). Sparkline-Fenster in `DevicesViewModel.appendTrafficHistory` von 10s auf 30s (`cutoff`). Manual.md-Textstelle + `build-manual.py`-Lauf (PDF neu) mit aktualisiert.
Nachbesserung 3 (User-Wunsch: Aktualisierungsrate auf 0,1s): `startTrafficPolling` von `.milliseconds(500)` auf `.milliseconds(100)` — bei 30s-Fenster damit bis zu 300 Punkte pro Sparkline/Port (bisher ~60). Nicht weiter geprüft, ob das bei vielen Ports spürbar CPU kostet — bei Bedarf zurückmelden. Manual.md "Abfrage alle 0,1s" + PDF mit aktualisiert. Build grün, App startet ohne Absturz. Noch nicht live geprüft.
### 5. App-Einstellungen / Settings — eigener Menüpunkt
**Status:** fixed (live bestätigt: "passt, lassen wir so")
**Gitea-Issue:** [#7](http://192.168.178.222:3500/kay/RouterOS/issues/7)
Wunsch: eigener Einstellungen-Bereich für Personalisierung, Layout, Farbverwaltung, Refreshraten, Updates, Schriftgrößen, Responsiveness, etc.
Rückfrage vorab geklärt (AskUserQuestion): natives macOS-Settings-Fenster (⌘,), nicht eigener Tab; alle genannten Bereiche jetzt sofort umsetzen, nicht nur ein Kernstück; Farbverwaltung als 2 vordefinierte Themes ("Standard"/"Kontrastreich"), kein freier Color-Picker pro Kategorie.
Umsetzung:
- **`Core/Models/AppPreferences.swift`** (neu): zentrale `AppStorage`-Key-Konstanten + `AppPreferences.registerDefaults()` (in `RouterOSAssistantApp.init()` aufgerufen), plus drei Enums:
- `ColorTheme` (`standard`/`highContrast`) — Farben für Übersicht-Node-Kategorien, Kanten-Arten, und LAN-Scanner-Status (Traffic aktiv, Fest/Dynamisch, offener/geschlossener Port). Ersetzt die alten `OverviewStyle.color(for:)`-Funktionen (die jetzt nur noch `explanation(for:)`/`icon(for:)` halten).
- `AppTextSize` (Klein/Standard/Groß/Sehr groß) — Skalierungsfaktor (0,85/1,0/1,2/1,4), per `.environment(\.appFontScale, ...)` an der `WindowGroup`-Wurzel gesetzt.
- `UIDensity` (Kompakt/Standard/Komfortabel) — mappt auf `ControlSize`, ebenfalls an der Wurzel via `.controlSize(...)`. Deckt den "Responsiveness"-Wunsch pragmatisch als Bedienelement-Größe ab (die App ist eine feste macOS-Fensterlayout-App ohne eigentliche responsive Breakpoints — eine echte "Responsiveness"-Funktion in dem Sinn gibt es nicht, siehe unten).
- **`Features/Settings/SettingsView.swift`** (neu): `Settings { }`-Scene mit 3 Tabs — Allgemein (Sprache, Auto-Update-Check beim Verbinden), Darstellung (Farbschema, Textgröße, Bedienelemente), Netzwerk (LAN-Scanner Aktualisierungsrate/Sparkline-Zeitfenster/-Breite). Alle Controls sind reine `@AppStorage`-Bindings, kein eigenes ViewModel.
- **Live-Wirkung ohne Neustart**: `OverviewView`/`DevicesView` lesen `colorTheme` per `@AppStorage` und geben es an `EdgesCanvas`/`NodeCardView`/`NodeDetailView`/`EdgeDetailView`/`EdgeTooltipView`/`LegendView`/`DeviceRow`/`PortScanRow` durch. `DevicesViewModel.startTrafficPolling`/`appendTrafficHistory` lesen Intervall/Zeitfenster bei jedem Tick frisch aus `UserDefaults` (nicht einmalig beim Start), damit eine Änderung im Settings-Fenster sofort greift, auch während der Tab schon pollt.
- **"Beim Verbinden automatisch nach Updates suchen"** (neuer Toggle, Default aus): `ConnectViewModel.connect()` ruft nach erfolgreicher Verbindung automatisch dieselbe `checkForUpdates(for:)` auf, die sonst nur der manuelle Button im Verbinden-Tab auslöst.
- L10n.swift um alle neuen Settings-Strings ergänzt (DE/EN).
- `.xcodeproj` manuell um beide neue Dateien ergänzt (PBXBuildFile/PBXFileReference/PBXGroup/Sources-Build-Phase).
Bewusst nicht umgesetzt: Übersicht-Zoom-Standardwert und Fokus-Popup-Größe/Verhalten sind nicht in Settings aufgenommen — beides sind reine `@State`-Werte ohne bestehenden Persistenz-Mechanismus und wurden in der Rückfrage nicht explizit als "jetzt" gefordert; bei Bedarf als vierte Kategorie nachziehbar.
Nachbesserung 1 (Live-Test: "Bedienelemente und Sparkling funktioniert, bei Textgröße und Farbschemas sehe ich keine Änderung"):
- Farbschema-Bug gefunden: 3 Farben (`.service`-Kategorie, `.dhcp`- und `.wireguardPeer`-Kanten) waren in `standard` und `highContrast` exakt identisch definiert — Kopierfehler beim ersten Entwurf. Behoben, beide Paletten jetzt komplett überschneidungsfrei.
- Textgröße nachgebessert (Bereich auf macOS-Accessibility-Stufen erweitert für einen unübersehbaren Sprung).
Nachbesserung 2 (Live-Test: erneut "unverändert" bei beidem, trotz Fix 1) — Ursache mit Beweis gefunden statt weiter geraten: per `ImageRenderer`-Snapshot (Scratchpad-Skript) nachgewiesen, dass `.environment(\.dynamicTypeSize, ...)` auf macOS **gar keine Wirkung** hat — identisches Pixel-Rendering bei `.xSmall` und `.accessibility3`. Anders als iOS skalieren SwiftUI-Textstile auf macOS nicht über Dynamic-Type-Kategorien. Rückfrage an User: 3 Alternativen zur Wahl gestellt, User wählte "Echtes Text-only Scaling".
Umsetzung (komplett neuer Mechanismus, `AppTextSize.dynamicTypeSize` entfernt):
- Neuer `\.appFontScale`-Environment-Key (`CGFloat`, Default 1.0) + `AppFontStyle`-Enum mit expliziten macOS-Basis-Punktgrößen pro Textstil (title2/title3/headline/body/callout/subheadline/caption/caption2) + `.appFont(_:weight:bold:design:)`-View-Modifier, der bei jedem Aufruf `baseSize * scale` real rendert.
- Alle ~110 `.font(...)`-Aufrufe in der App auf `.appFont(...)` umgestellt (automatisiert per Scratchpad-Skript, textbasierte 1:1-Ersetzung pro bekanntem Font-Ausdruck) — **außer** zwei Stellen mit fest-breiten Pixel-Layouts, die beim Skalieren brechen würden: `OverviewView.NodeCardView` (Diagramm-Knotenkarten, feste `nodeWidth`/`nodeHeight`) und `DevicesView.DeviceRow`/`DeviceColumnHeader` (LAN-Scanner-Tabellenspalten mit festen `DeviceColumn`-Breiten + `lineLimit(1)`). Diese bleiben bewusst fix, sonst Text-Clipping/Überlappung.
- Mechanismus per `ImageRenderer`-Snapshot verifiziert (Basis-Text bei Skalierungsfaktor 0,85 vs. 1,4 rendert nachweislich unterschiedlich groß), bevor erneut um Live-Test gebeten wurde.
Build grün, alle 98 Unit-Tests grün. Bitte nochmal live testen — diesmal an Form-lastigen Stellen (z.B. Settings-Fenster selbst, LAN-Scanner-Überschriften/Fehlertexte, nicht die zwei bewusst fixen Ausnahmen).
### 6. Mode-Button Setup
**Status:** fixed (live bestätigt)
**Gitea-Issue:** [#8](http://192.168.178.222:3500/kay/RouterOS/issues/8)
Wunsch: an der Seite des Router ist eine Taste "Mode", diese ist belegbar (Scripte, Deaktivierung, etc). Einlesen, was geht, und bauen.
Recherche (help.mikrotik.com, nicht geraten): `/system routerboard mode-button` ist ein Singleton-Einstellungsmenü (kein Listen-Menü) mit drei echten Feldern:
- `enabled` (yes/no, Standard no)
- `on-event` (Name eines Skripts aus `/system script`)
- `hold-time` (Zeitspanne Min..Max, z.B. `3s..5s`, verfügbar ab RouterOS 6.47beta60)
Wichtiger Fund: ab RouterOS 7.1rc4 muss jede Aktivierung/Änderung dieser Einstellung zusätzlich per physischem Tastendruck (Reset- oder Mode-Taste) am Gerät selbst innerhalb von 60 Sekunden bestätigt werden — eine Änderung allein über die App reicht nicht. Als `warning` im Schema hinterlegt, damit das im Experte-Tab sichtbar ist.
Umsetzung: neuer Eintrag in `RouterOSSchemaCatalog.systemFamily` (`/system routerboard mode-button`, `category: .system`, `isSingleton: true`). `on-event` nutzt `.menuItemPick(menuPath: "/system script", ...)` (wie beim bestehenden Zeitplaner) statt Freitext — zeigt eine Auswahl der bereits angelegten Skripte. `hold-time` als `.text` (kein eigener Range-Editor gebaut, dafür wäre ein neuer `RouterOSFieldSchema.Kind` nötig — Skalierungsaufwand für ein einzelnes Feld aktuell nicht gerechtfertigt, Format steht im Hilfetext). DE/EN-Übersetzungen ergänzt. Erreichbar direkt im Experte-Tab unter "System".
Build grün, alle 98 Unit-Tests grün. Nicht live geprüft (kein Router mit Mode-Taste hier erreichbar) — bitte am echten Gerät: Skript unter "Skripte" anlegen, dann unter "Mode-Taste" auswählen + aktivieren, danach die physische Bestätigung am Gerät nicht vergessen (siehe Warnhinweis).
Nachbesserung 1 (Live-Test beim Skript-Anlegen, nicht Mode-Taste selbst — Bug 40): Skript-Anlegen im Experte-Tab hing dauerhaft an "Unbekannter SSH-Schlüssel ... Bitte bestätigen", auch nach mehrfachem "Anlegen"-Klick. Ursache gefunden (Code gelesen, nicht geraten): `ExpertViewModel.saveEditingItem()`/`confirmRemoval()` und `DevicesViewModel.confirmStaticAssignment()`/`confirmStaticRemoval()` rufen vor dem eigentlichen Schreiben `ensureSessionBackup()` — die erste Sicherung pro Sitzung läuft über `BackupService`s eigene, dedizierte SSH-Verbindung. `ConnectionService.verifySSHTrust` prüft den SSH-Host-Key zwar proaktiv direkt nach jedem erfolgreichen REST-Connect (Bug 13/37-Fix), aber wenn dieser proaktive Check aus irgendeinem Grund nicht (mehr) greift, hatte der Schreibpfad selbst keine Möglichkeit, `pendingSSHTrustFingerprint` zu setzen — der Fehler landete nur als toter Text in `applyError`, ohne Trust-Dialog, exakt derselbe Grundfehler wie Bug 13/37, nur diesmal beim Schreiben statt beim Verbinden.
Fix: neue `ConnectionService.noteUntrustedSSHHostKey(_:)` — alle vier oben genannten Catch-Blöcke rufen sie bei `RouterOSError.untrustedSSHHostKey` auf, wodurch derselbe Trust-Dialog im Verbinden-Tab erscheint, den `verifySSHTrust` auch nutzt. Bitte nach Rebuild: nochmal "Anlegen" versuchen, diesmal im Verbinden-Tab nach einer Trust-Bestätigung schauen, dort bestätigen, dann im Experte-Tab erneut "Anlegen".
Build grün, alle 98 Unit-Tests grün. Nicht live geprüft (kein Router hier erreichbar).
Nachbesserung 2 (Live-Test: Mode-Taste speichern → "Fehlermeldung HTTP 500", gesendeter Befehl laut Vorschau-Anzeige `hold-time=""`): mit echtem Diagnose-Unit-Test nachgestellt statt weiter spekuliert — bei realistischer Datenlage (Router-GET ohne "hold-time"-Feld) entsteht kein leerer Wert, also kein einfacher "Feld war nie gesetzt"-Fall. Tatsächliche Ursache bleibt ungeklärt (kein Router zum Nachstellen erreichbar), aber der Effekt ist reproduzierbar sichtbar: ein leeres `hold-time=""` geht raus und lässt RouterOS' REST-API mit HTTP 500 abbrechen (Time-Interval-Range-Typ akzeptiert wohl keinen leeren String, anders als ein reines Textfeld).
Fix: neuer `RouterOSFieldSchema.clearable`-Parameter (Default `true`, bestehendes Verhalten für alle anderen Felder unverändert) — ein Feld mit `clearable: false` schickt nie ein explizites Leeren (`feld=""`), selbst wenn die Diff-Logik das sonst täte. `hold-time` ist jetzt `clearable: false`. Neuer Test `testNonClearableFieldNeverSendsExplicitEmptyValue` deckt das ab, bestehender `testClearingACuratedFieldSendsExplicitEmptyValue` (Kommentar-Feld leeren) bleibt unverändert grün — betrifft also nur `hold-time`, nicht die generelle "Feld leeren"-Funktion.
Build grün, alle 99 Unit-Tests grün. Bitte nochmal live testen: Mode-Taste mit "Aktiviert" + Skript speichern, "Haltedauer" leer lassen.
Nachbesserung 3 (Live-Test: Fehler blieb identisch, obwohl `hold-time` laut Vorschau-Anzeige jetzt korrekt weggelassen wurde — bewies, dass Nachbesserung 2 zwar wirkt, aber nicht die Ursache war): User führte denselben Befehl direkt per SSH-Terminal am Router aus (`/system routerboard mode-button set enabled=yes on-event="Button test"`) — dort lief er sofort fehlerfrei, `print` bestätigte alle drei Felder korrekt gesetzt (inkl. Werks-Default `hold-time: 0s..1m`, den es demnach doch gibt — passt zur ursprünglichen Vermutung in Nachbesserung 2, war aber am Ende nicht die eigentliche Fehlerursache). Damit zweifelsfrei bewiesen: `/system routerboard mode-button` funktioniert über SSH einwandfrei, aber nicht über REST — eine echte Deckungslücke in RouterOS' REST-API für dieses Hardware-Menü, kein App-seitiger Datenfehler.
Fix: neuer `RouterOSMenuSchema.writesRequireSSH`-Parameter (Default `false`) + `ConnectionService.applyViaSSH(_:)` (dedizierte SSH-Verbindung, exakt dasselbe Muster wie `BackupService`/`UpdateService`). `ExpertViewModel.saveEditingItem()` nutzt für als `writesRequireSSH: true` markierte Schemas jetzt immer diese dedizierte SSH-Verbindung statt der Session-Transport (REST oder SSH, je nachdem wie verbunden wurde). Mode-Taste-Schema entsprechend markiert.
Build grün, alle 99 Unit-Tests grün. Live bestätigt: Mode-Taste-Speichern über die App lief jetzt fehlerfrei (kein HTTP 500 mehr).
Nachbesserung 4 (Log zeigte nach Tastendruck: `script,error executing script Button test from sys2 failed, please check it manually (not enough permissions)`) — separates, tieferes RouterOS-Problem, nicht mehr die App/REST-Frage: Skripte, die vom Mode-Button ("sys2"-Kontext) ausgelöst werden, laufen mit eingeschränkten Rechten, nicht mit den vollen Standard-Skript-Rechten. Zwei Versuche schlugen fehl, weil sie nie wirklich beim Router ankamen: der Skriptname war tatsächlich "Button test" (großes B), meine Terminal-Befehle zielten auf "button test" (klein) bzw. nutzten ungültige `set`-Syntax ohne `[find ...]`/Index — RouterOS matchte nichts, `print detail` zeigte danach unverändert `dont-require-permissions=no`. Mit korrektem Index-Befehl (`/system script set 0 dont-require-permissions=yes`) griff es sofort — Log zeigt seither saubere `script,info Mode-Taste gedrückt: ...`-Einträge bei jedem Tastendruck.
**Fazit für jedes künftige Mode-Taste-Skript**: `dont-require-permissions=yes` setzen, sonst schlägt die Ausführung vom Button aus mit "not enough permissions" fehl, obwohl dasselbe Skript manuell/per Scheduler einwandfrei liefe. Die App hat dafür aktuell noch kein Feld im "Skripte"-Schema (`/system script` kennt nur Name+Inhalt) — bei Bedarf als eigener Punkt ergänzbar (`policy`/`dont-require-permissions`-Felder).
Mode-Taste (Fund #6) ist damit vollständig live verifiziert: Anlegen über App → Skript korrekt zugewiesen → Speichern ohne Fehler → Tastendruck löst Skript korrekt aus.
### 9. Übersicht: Spaltenüberschriften fehlten im Fokus-Popup
**Status:** fixed (live bestätigt)
**Gitea-Issue:** [#11](http://192.168.178.222:3500/kay/RouterOS/issues/11)
Wunsch: Fokus-Popup (Klick auf Knoten in der Übersicht) soll dieselben Spaltenüberschriften (Interfaces, IP-Adressen, Pools & DHCP, Routen, Firewall & NAT) über den Knoten zeigen wie das Hauptdiagramm.
Ursache: `focusPanel(subGraph:subLayout:)` in `OverviewView.swift` rendert Kanten+Knoten der Kette per `OverviewLayout.layout(for:)` (gleiche Spalten-Logik wie oben), hatte aber nie den Header-`ForEach` über `OverviewLayout.columnOrder` übernommen, der im Hauptdiagramm dafür sorgt. `OverviewLayout.topInset` (44pt) reserviert bereits ausreichend Platz oberhalb der Knoten — Header-Block 1:1 aus dem Hauptdiagramm übernommen, keine Größenanpassung nötig.
Build+alle 99 Unit-Tests grün, live bestätigt.
### 7. Manual direkt in die App integrieren
**Status:** fixed (live bestätigt)
**Gitea-Issue:** [#9](http://192.168.178.222:3500/kay/RouterOS/issues/9)
Wunsch: Manual so in die App integrieren, dass ein Hilfepunkt in der aktuell geöffneten Sektion direkt zur passenden Stelle im Manual springt (Textanker).
Rückfrage vorab geklärt (AskUserQuestion): eingebettetes HTML (WebView) statt PDF/PDFKit; eigenes Hilfe-Fenster (wie Settings ⌘,) statt eigener Tab; Sprungmarken pro Wizard-Schritt + pro Experte-Schema, nicht nur pro Haupt-Tab.
Umsetzung:
- **`build-manual.py`**: ``-Anker automatisch vor jede Experte-Menü-Überschrift eingefügt (`schema_anchor(menuPath)`, deterministisch aus dem RouterOS-Menüpfad). `Manual.md` bekam zusätzlich 12 handgesetzte ``-Anker vor den 6 Haupt-Tab- und 6 Wizard-Schritt-Überschriften. Neue Funktion `render_app_html()` erzeugt ein eigenständiges `RouterOSAssistant/Resources/Manual.html` (Bilder als Base64 eingebettet, kein `Manual-assets/`-Ordner nötig im App-Bundle) — MarkdownIt jetzt mit `html=True`, sonst wären die ``-Tags als Text escaped statt als echte DOM-IDs zu rendern.
- **`ManualView.swift`** (neu, `Features/Manual/`): `ManualAnchor`-Enum mit den 12 statischen Ankern + `schema(menuPath:)` (spiegelt `schema_anchor()` exakt — kein generiertes Mapping nötig, beide Seiten wenden dieselbe simple Transformation an). `ManualNavigator` (ObservableObject, app-weit als `@StateObject` in `RouterOSAssistantApp`) hält den angeforderten Anker. `ManualWebView` (WKWebView via `NSViewRepresentable`) lädt das gebündelte `Manual.html` einmalig, springt bei Anker-Änderung per JavaScript `scrollIntoView` (kein Reload/Flackern). Neues `Window("Handbuch", id: "manual")` in `RouterOSAssistantApp.swift`.
- **`ManualHelpButton`**: wiederverwendbarer "?"-Toolbar-Button, öffnet/fokussiert das Handbuch-Fenster und springt zum übergebenen Anker. Eingebaut in: alle 6 Haupt-Tabs (Verbinden, Übersicht, LAN-Scanner, Experte, Sicherungen — je Toolbar), alle 6 Wizard-Schritte (WAN/LAN/VLAN/WLAN/Firewall/Review), und `ExpertMenuDetailView` bekommt pro geöffnetem Schema automatisch den passenden Anker (`ManualAnchor.schema(schema.menuPath)`) — alle 45 Experte-Menüs sind damit einzeln verlinkt, nicht nur der Experte-Tab pauschal.
Bewusst nicht umgesetzt: Settings-Fenster (⌘,) hat noch keinen Hilfe-Button — macOS-Settings-Fenster haben konventionell keine Toolbar, ein Button dort hätte nicht ins native Bild gepasst; bei Bedarf nachrüstbar (Kapitel 7 im Manual existiert bereits, Anker `tab-settings` ist gesetzt).
Build grün, alle 99 Unit-Tests grün, `Manual.html` bestätigt im App-Bundle (`Contents/Resources/Manual.html`). Noch nicht live geprüft — bitte: Hilfe-Buttons in mehreren Tabs/Wizard-Schritten/Experte-Menüs anklicken, prüfen ob das Handbuch-Fenster öffnet und zur richtigen Stelle springt.
Nachbesserung 1 (User-Feedback: "das manual schaltet aber nicht die Sprache in englisch um, denk dran weitere Sprachen folgen"): Rückfrage geklärt — komplette Handübersetzung aller Kapitel (nicht nur Kapitel 5 automatisch), Mechanismus generisch für beliebig viele Sprachen statt hart DE/EN.
Umsetzung:
- **`Manual.en.md`** (neu): vollständige Handübersetzung aller Fließtext-Kapitel (0,1,2,3,4,6,7) — dieselbe Struktur/Anker wie `Manual.md`. Kapitel 8 "English summary" (bisheriger Behelf) aus `Manual.md` entfernt, da jetzt redundant.
- **`build-manual.py`** generalisiert: neues `LANGUAGES`-Dict (`{"de": Manual.md, "en": Manual.en.md}`, für weitere Sprachen nur ein neuer Eintrag + `Manual..md` nötig, kein neuer Python-Code). Kapitel 5 (Experte-Referenz, automatisch generiert) wird jetzt pro Sprache übersetzt, indem `L10n.swift`s eigenes DE→EN-Übersetzungs-Dictionary der App selbst wiederverwendet wird (per Regex aus dem Swift-Quelltext geparst, 714 Einträge) — Feldlabels/Hilfetexte im Manual stimmen dadurch exakt mit dem überein, was die App in Englisch zeigt, keine zweite, separat gepflegte Übersetzung. Bug beim ersten Anlauf: Klammersuche fand die falsche `[` (die des Typannotation `[String: String]`, nicht die des Array-Literals) — 0 Übersetzungen geladen, dadurch blieb Kapitel 5 komplett Deutsch. Gefixt, Gegenprobe: 714 Einträge geladen.
- **`ManualView.swift`**: `ManualWebView` lädt jetzt `Manual.html` (de) oder `Manual_.html` (alles andere), abhängig von `@AppStorage("appLanguage")`, mit Fallback auf Deutsch falls eine Sprache (noch) keine Übersetzung hat. Lädt bei Sprachwechsel neu (`Coordinator` merkt sich die zuletzt geladene Sprache), springt danach weiterhin zum aktuellen Anker.
Build grün, alle 99 Unit-Tests grün, `Manual.html` + `Manual_en.html` beide bestätigt im App-Bundle. Bitte nochmal live testen: Sprache auf Englisch umschalten, Hilfe-Button klicken, prüfen ob Handbuch-Fenster jetzt englisch anzeigt.
### 8. Abwechselnde Farbkombis bei Tabellenansichten
**Status:** fixed (live bestätigt: "passt")
**Gitea-Issue:** [#10](http://192.168.178.222:3500/kay/RouterOS/issues/10)
Bessere Lesbarkeit durch alternierende Zeilenfarben in Tabellenansichten (LAN-Scanner, Experte-Listen, etc.).
Umsetzung: neuer `TableZebra`-Helper (`AppPreferences.swift`) nutzt AppKits eigenes `NSColor.alternatingContentBackgroundColors` (dieselbe Farbpaar, das `NSTableView` fürs Zebra-Streifenmuster verwendet) statt einer selbst gewählten Grauabstufung — passt sich automatisch an Hell/Dunkelmodus an. Angewendet über `.listRowBackground(TableZebra.color(for: index))` auf: LAN-Scanner-Geräte-Zeilen (`DevicesView.swift`, pro Port-Gruppe neu bei Index 0 startend) und Experte-Tab-Eintragsliste (`ExpertMenuDetailView.swift`).
Bewusst nicht umgesetzt: "Weitere Parameter (frei)"-Grid im Experte-Bearbeiten-Sheet (Bug 2) — dort läuft die Zeilenzählung zweispaltig (zwei Einträge pro Grid-Zeile), Zebra-Streifen würden dort eher verwirren als helfen; bei Bedarf separat nachziehbar.
Build grün, alle 99 Unit-Tests grün.
Nachbesserung 1 (Live-Test: "keine Änderungen erkennbar") — Ursache mit Beweis gefunden statt weiter geraten: erst per `ImageRenderer`-Snapshot versucht (schlug fehl, `Form` rendert im Headless-Kontext leer — kein Ergebnis dazu), dann per Doku-Recherche bestätigt: `.formStyle(.grouped)` malt einen eigenen blickdichten Hintergrund über jede Section, der `.listRowBackground` komplett verdeckt — bekanntes, dokumentiertes SwiftUI-Verhalten, kein Fehler in der Farbberechnung. Fix: `.scrollContentBackground(.hidden)` auf beiden betroffenen `Form`s ergänzt (`DevicesView.swift`, `ExpertMenuDetailView.swift`), Standard-Kombination für genau diesen Fall.
Build grün, alle 99 Unit-Tests grün. Achtung beim Live-Test: kann auch den "Karten"-Hintergrund der übrigen Form-Bereiche (nicht nur die Zeilen) sichtbar verändern — bitte Gesamteindruck prüfen, nicht nur ob Streifen da sind.
Nachbesserung 2 (User-Feedback: auch das "Weitere Parameter"-Grid im Experte-Bearbeiten-Sheet soll Zeilen abwechselnd einfärben — Zeile 1,3,5... —, "bitte für das komplette Projekt umsetzen"): Grid-Zeilen (2 Parameter pro sichtbarer Zeile) bekommen jetzt `.background(TableZebra.color(for: rowIndex))` pro `GridRow`. Zusätzlich auf jede weitere echte Tabellen-/Listenansicht im Projekt ausgeweitet: "Bekannte Router"-Liste + Interface-Liste (Verbinden-Tab), Sicherungsliste (Sicherungen-Tab), Rohfelder-Sheet + Port-Scan-Ergebnisliste (LAN-Scanner), geplante Änderungen + Ablauf-Log (Einrichten-Wizard Review/Apply), Feld-Details + Verbindungsliste im Übersicht-Knoten-Detailpanel. Nicht angefasst: Experte-Sidebar (Navigationsliste, keine Datentabelle) und die Setup-Wizard-Konfigurationsformulare (VLAN/WLAN/LAN/WAN — je Zeile ein Mehrfeld-Unterformular, keine gleichförmigen Datenzeilen, Zebra-Streifen würden dort eher verwirren).
Build grün, alle 99 Unit-Tests grün. Bitte erneut live testen.
### 10. Selbständiger Wiederverbindungsversuch nach Disconnect
**Status:** fixed (live bestätigt, zusätzlich beim echten Firmware-Update-Neustart mitgetestet)
**Gitea-Issue:** [#12](http://192.168.178.222:3500/kay/RouterOS/issues/12)
Wunsch: fällt die Verbindung zum Router weg (z.B. während einer laufenden Sitzung), soll die App selbständig versuchen, die Verbindung wiederherzustellen, statt einfach im getrennten Zustand zu bleiben.
Ist-Zustand vor dem Fix: `ConnectionService.state` blieb `.connected`, bis der Nutzer explizit "Trennen" klickte — es gab überhaupt keinen Mechanismus, der einen tatsächlichen Verbindungsverlust (Router aus, Kabel raus, WLAN weg) erkannt hätte. Einzelne fehlschlagende Anfragen zeigten nur lokale Fehlermeldungen pro Tab, der App-weite Zustand blieb unberührt.
Umsetzung (`ConnectionService.swift`):
- Neuer Hintergrund-Task (`healthMonitorTask`), gestartet bei jeder erfolgreichen Verbindung: alle 10s ein minimaler Lese-Test (`/system identity`, kleinstmögliches Singleton-Menü) als Herzschlag.
- Schlägt der Herzschlag fehl: `isReconnecting = true`, danach Retry-Schleife alle 5s (REST zuerst, dann SSH-Fallback, dieselbe Reihenfolge wie ein normaler Connect) — läuft unbegrenzt weiter, bis entweder die Verbindung wiederhergestellt ist oder der Nutzer explizit "Trennen" klickt (bricht den Task sofort ab).
- `state` bleibt bewusst durchgehend `.connected`, damit andere Tabs während eines kurzen Aussetzers nicht auf ihre "Nicht verbunden"-Platzhalter umspringen — nur `isReconnecting` (neues `@Published`) spiegelt den Zustand, sichtbar als oranger Hinweis mit Spinner oben im Verbinden-Tab ("Verbindung unterbrochen — versuche automatisch, erneut zu verbinden…").
- Reconnect nutzt bei Erfolg denselben `finishConnecting(using:)`-Pfad wie ein normaler Connect — Geräte-/Routerboard-Info und Interface-Liste werden dabei automatisch neu geladen.
Build grün, alle 99 Unit-Tests grün. Live bestätigt ("funktion").
Nachbesserung 1 (User-Wunsch: "ein countdown, während wiederverbindens noch mit einbauen und wieviel versuche bereits gelaufen sind"): zwei neue `@Published`-Werte in `ConnectionService` — `reconnectAttemptCount` (hochgezählt pro REST+SSH-Runde) und `secondsUntilNextReconnectAttempt` (sekündlich runtergezählt zwischen den Versuchen, `nil` während ein Versuch tatsächlich läuft). Banner im Verbinden-Tab zeigt jetzt eine zweite, kleinere Zeile: "Versuch 3 · nächster in 4s" (bzw. "Versuch 3 …" während der Verbindungsversuch selbst läuft).
Build grün, alle 99 Unit-Tests grün. Bitte nochmal live testen.
### 11. Systematischer Tester-Durchgang (bugs.md, 2026-09-17)
**Status:** fixed (Build + Tests grün, noch nicht live gegenreproduziert)
Auf Nutzerwunsch ("teste alle Funktionalitäten, suche bugs") Code-Audit gegen den echten
hAP-lite-Testrouter durchgeführt (UI selbst nicht klickbar, keine macOS-UI-Automatisierung
verfügbar — stattdessen Code-Review + Router-Ground-Truth per SSH). Vier Funde in `bugs.md`
dokumentiert und direkt gefixt:
1. Race Condition bei der Port-Konflikt-Prüfung im LAN-Schritt (`SetupViewModel.checkPortConflict`)
— schneller Portwechsel konnte ein überholtes Prüfergebnis über ein aktuelleres schreiben.
Fix: Generation-Zähler pro LAN-Config, überholte Antworten werden verworfen.
2. `FirewallStepView`-Titel behauptete "Firewall (optional)" auch im Einfach-Modus, wo der
Grundschutz laut Text direkt darunter fest aktiv ist. Fix: Titel modusabhängig.
3. Zwei fehlende Englisch-Übersetzungen (skriptgeprüft gegen alle `L10n.t(...)`-Aufrufstellen):
"Netzwerk-Test fehlgeschlagen", "Fokus-Ansicht schließen". Ergänzt in `L10n.swift`.
4. Health-Check-Herzschlag (`ConnectionService`, alle 10s) prüfte laufende Setup-Wizard-/
Experte-Schreibvorgänge nicht mit — auf schwacher Hardware (hAP lite: 1 Kern, 650MHz) konnte
ein langsamer, aber erfolgreicher Apply fälschlich als Verbindungsverlust gewertet werden.
Fix: neuer `beginWrite()`/`endWrite()`-Zähler, Herzschlag pausiert währenddessen.
Build grün, alle 99 Unit-Tests grün. Details je Fund in `bugs.md`.
### 12. Erneuter Deep-Dive-Gegencheck mit echtem Exploit-Nachweis (bugs.md #5-#7, 2026-09-17)
**Status:** fixed (2 kritische Live-Exploits nachgewiesen+gefixt+gegenverifiziert, 1 Beobachtung offen dokumentiert)
Auf Nutzerwunsch ("erneuter Gegencheck mit echtem Deep Dive, alles testen") tiefer weitergesucht,
diesmal mit echten Exploit-Versuchen gegen den Router statt nur Code-Lesen:
- **#5 RouterOS-CLI-Injection** (schwerwiegendster Fund der ganzen Session): `RouterOSCommand.cliLine`
quotete Werte nur bei Leerzeichen und escapte nie eingebettete `"`. Ein Kommentar wie
`test" ; :log warning "X` schloss das Quoting vorzeitig und ließ RouterOS den Rest als zweiten,
unabhängigen Befehl ausführen — live exploitiert (Log zeigt injizierten `script,warning`-Eintrag)
und live als behoben bestätigt (kein injizierter Eintrag mehr, Kommentar korrekt gespeichert).
Betraf jeden Text im Setup-Wizard/Experte-Tab, sobald über SSH geschrieben wird — und auf dem
aktuellen Testrouter ist `www-ssl` deaktiviert, REST also gar nicht erreichbar: jede
Schreiboperation lief bereits über SSH, der Bug war aktiv, nicht nur theoretisch.
- **#6 Generischer Parser trunkiert mehrwortige Werte**: `print terse` quotet in RouterOS 7.24.4
gar keine mehrwortigen Werte (live an zwei Menüs bestätigt) — der alte Regex-Parser schnitt
jeden solchen Wert beim ersten Leerzeichen ab, unbemerkt seit Session-Beginn (war bereits im
eigenen Live-Dump-Testcode sichtbar, fiel aber nie auf). Fix: Token-Scan statt Regex.
- **#7 Beobachtung, kein Fix**: Netzwerk-Isolation wirkt nicht rückwirkend auf bereits bestehende
(conntrack-getrackte) Verbindungen — Standardverhalten jeder stateful Firewall, nie getestet,
bewusst nicht automatisch "gefixt" (Conntrack-Flush wäre eine Produktentscheidung, kein reiner
Bugfix).
Build grün, alle 101 Unit-Tests grün (2 neue Regressionstests). Details in `bugs.md`.
### 13. Dritter Durchgang: gezielte Sicherheitshärtung (bugs.md #8-#9, 2026-09-17)
**Status:** fixed (2 Härtungsfixes, keine weiteren Live-Exploits gefunden)
Auf Nutzerwunsch ("finale Test für maximale Sicherheit, test alles was du finden kannst") gezielt
Zugangsdaten-Speicherung und TOFU-Mechanismen geprüft. Positiv bestätigt: Keychain-Nutzung korrekt,
keine Passwörter in UserDefaults/JSON, `BackupService`s eigene Skript-Escaping-Logik war bereits
vor diesem Durchgang korrekt. Zwei Härtungslücken gefunden und geschlossen (beide defensiv, nicht
live exploitiert — anders als #5/#6 im vorigen Durchgang):
- **#8**: `RestTransport.fetchFieldValues` hatte dieselbe ungeschützte String-Interpolation wie das
SSH-Pendant aus Fund #5, nur als URL-Query-String statt CLI-Zeile — beim ersten Fix übersehen.
Jetzt RFC-3986-konform percent-encoded.
- **#9**: `CertificateFingerprint.sha256` fiel bei Extraktionsfehlern auf einen festen String
"unbekannt" zurück statt einen echten Fingerabdruck zu liefern — theoretisches TOFU-Bypass-Fenster
(zwei verschiedene, beide extraktions-fehlschlagende Zertifikate hätten sich denselben
"Fingerabdruck" geteilt). Rückgabetyp auf optional geändert, Extraktionsfehler führt jetzt zu
hartem Verbindungsabbruch statt einem Vertrauens-Dialog mit unverifizierbarer Kennung.
Build grün, alle 101 Unit-Tests grün. Details in `bugs.md`.
### 14. README-Nachcheck: offener Milestone (Port-Konflikt/"Fertig") + 3 Alert-Dismiss-Bugs
**Status:** fixed (Build + 102 Unit-Tests grün, Live-Klicktest weiterhin offen)
Nutzer wies auf einen noch offenen Milestone in der README hin (LAN-Port-Konflikt-Prüfung +
"Fertig"-Button, nie live durchgeklickt). Beim erneuten Code-Review dieses Bereichs (u.a. wegen
des in dieser Session bereits gefixten Race-Bugs in genau dieser Feature) drei echte,
eigenständige Bugs derselben Klasse gefunden:
1. `ReviewApplyView`s "Anwenden fehlgeschlagen"-Alert: OK-Button-Action war leer, die
`isPresented`-Bindings-Setter-Closure ebenfalls ein No-Op — `applyError` wurde nie
zurückgesetzt, der Alert konnte sich nach dem Schließen theoretisch sofort wieder öffnen.
2. `ConnectView`s "Unbekanntes Zertifikat"-Alert: "Abbrechen"-Button-Action war komplett leer —
`connectionService.state` blieb für immer auf `.needsCertificateConfirmation` hängen, es gab
keinen Weg zurück außer dem Zertifikat zu vertrauen.
3. `ConnectView`s "Unbekannter SSH-Schlüssel"-Alert: "Abbrechen" rief nur
`dismissPendingSSHTrust()` (löschte nur `pendingSSHTrustFingerprint`) — funktionierte nur für
einen von zwei möglichen Auslöse-Pfaden dieses Alerts, beim anderen (`state ==
.needsSSHHostKeyConfirmation`) blieb der Dialog hängen.
Fix: neue `ConnectionService.cancelPendingTrustConfirmation()` (setzt `state` bei beiden
"needs...Confirmation"-Fällen auf `.idle` zurück, löscht zusätzlich
`pendingSSHTrustFingerprint`), neue `SetupViewModel.dismissApplyError()`. Alle drei Alerts
verdrahtet — sowohl der jeweilige Cancel/OK-Button als auch die Bindings-Setter-Closure (deckt
auch Tap-Outside/Esc-Dismiss ab). Totes `dismissPendingSSHTrust()` entfernt. Ein neuer
Regressionstest (`testCancelPendingTrustConfirmationResetsCertificateConfirmationToIdle`).
Build grün, alle 102 Unit-Tests grün. Der ursprünglich gemeldete Milestone (Port-Konflikt-Prüfung
selbst, inkl. Warndialoge, "Weiter"-Sperre, "Fertig"-Button) bleibt beim Status "Code-Review
bestätigt" — ein echter Live-Klicktest durch den Nutzer in der App-UI steht weiterhin aus, da
UI-Automatisierung in dieser Session nicht verfügbar ist.
### 15. bugs.md #7 bearbeitet: Isolation trennt jetzt auch bereits bestehende Verbindungen
**Status:** fixed (best-effort, Mechanismus teilweise live verifiziert)
**Gitea-Issue:** [#19](http://192.168.178.222:3500/kay/RouterOS/issues/19)
Auf Nutzerwunsch ("bearbeite #7") den zuvor bewusst zurückgestellten Punkt jetzt umgesetzt: neue
`SSHTransport.flushConnections`/`ConnectionService.flushConnections` entfernen per `/ip firewall
connection remove [find where (src-address in A) and (dst-address in B)]` bereits getrackte
Verbindungen zwischen zwei Netzen, sobald sie als isoliert angewendet werden — aufgerufen direkt
nach den Firewall-Befehlen in `SetupViewModel.apply()`.
Bemerkenswert am Weg dorthin: eine erste Live-Verifikation sah erfolgreich aus (Test-Verbindung
verschwand nach `remove`), erwies sich bei genauerem Hinsehen aber als Messfehler — die
ICMP-Test-Verbindung war einfach von selbst abgelaufen (RouterOS' sehr kurzer ICMP-Conntrack-
Timeout), nicht durch den `remove`-Befehl entfernt worden. Ein sauberer Nachtest an einer
tatsächlich noch aktiven TCP-Verbindung zeigte den Eintrag sofort wieder auftauchen. Root Cause
geklärt (Web-Recherche + Verhalten selbst nachvollzogen): Connection-Tracking-Removal sendet kein
RST, eine aktiv weiterlaufende Verbindung wird beim nächsten Paket einfach neu getrackt — kein
Beweis, dass `remove` nichts tut, aber auch kein Beweis, dass die neue Isolations-Regel das neu
getrackte Paket abfängt. Für den vollständigen Beweis fehlen zwei echte, getrennte Testnetze mit
echten Endgeräten. Dokumentation entsprechend ehrlich mit dem tatsächlichen Verifikationsstand
statt einer überzogenen "live bestätigt"-Behauptung versehen (siehe `bugs.md` #7 für die volle
Herleitung).
Build grün, alle 103 Unit-Tests grün (1 neuer Regressionstest für `FirewallConfig.isolatedNetworkPairs`).
### 16. Experte-Tab: Port-Konflikt-Prüfung wie im Einrichten-Assistenten
**Status:** fixed (Build + 111 Unit-Tests grün, 6 neue Regressionstests)
Live-Anlass: Nutzer legte über den Experte-Tab manuell ein eigenes Netz auf `ether4` an
(IP-Adresse, Pool, DHCP-Server) — `ether4` blieb dabei unbemerkt Bridge-Mitglied der Haupt-Bridge,
weshalb zwei DHCP-Server im selben Broadcast-Domain konkurrierten (kein App-Bug, aber genau der
Fall, den die Wizard-eigene Port-Konflikt-Prüfung normalerweise abfängt — beim manuellen Anlegen
über den Experte-Tab gab es diese Warnung bisher nicht).
Auf Nutzerwunsch ("die Abfrage vom Einrichten-Assistenten auf Expert anwenden ... mit allen
Warnungen") dieselbe Prüfung samt "Port freimachen?"-Dialog jetzt auch im Experte-Tab, angewendet
auf das `.interfacePick`-Feld des jeweils geöffneten Schemas (`/ip address`, `/ip dhcp-server`
etc.). `PortConflictWarningView` aus `LanStepView.swift` in `Features/Shared/` extrahiert, neuer
`immediateApply`-Parameter für die kontextabhängige Abschluss-Meldung (Wizard: erst bei "Jetzt
anwenden"; Experte-Tab: sofort bei "Anlegen"/"Speichern", da es dort keinen separaten Review-
Schritt gibt). Neue `PortConflict.resolutionCommandsIncludingBridgeDetach()` — anders als der
Wizard hat der Experte-Tab keinen automatischen, unbedingten Bridge-Detach-Schritt
(`DhcpServerCommandBuilder`), muss die Bridge-Entfernung bei Bestätigung also selbst mit
ausführen. `ExpertViewModel` bekommt dieselbe race-sichere Generation-Zähler-Logik wie
`SetupViewModel` (bugs.md #1). Speichern-Button gesperrt, bis der Konflikt bestätigt oder ein
anderer Port gewählt wurde.
Build grün, alle 111 Unit-Tests grün (6 neue Regressionstests). Live im Experte-Tab durchgeklickt
vom Nutzer bestätigt ("funktioniert").