49 KiB
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
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
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
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):
-
Horizontale Trennlinie über den Canvas; ausgewählter Node + Kette werden unterhalb dieser Trennlinie dargestellt (eigener Bereich, getrennt von der normalen Graph-Darstellung darüber).
-
Fokus-Modus ist temporär — aufhebbar (Klick auf leere Fläche / erneuter Klick), keine dauerhafte Layout-Änderung,
nodeOffsetsbleibt unberührt. -
Ausfaden = abdunkeln (nicht ausblenden).
-
Komplette verbundene Kette (nicht nur 1 Ebene) — deckt sich mit dem bereits vorhandenen
highlightedNodeIDs(rekursiv). -
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 vonhighlightedNodeIDs, 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 anviewModel.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 (effectivePositionsverschiebt nur Kettenknoten dorthin, alle anderen bleiben an ihrer normalen Position). - Ausfaden: nicht-Ketten-Nodes bekommen
opacity(0.25);EdgesCanvasbekommt die Kette alshighlightedNodeIDsü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:
EdgesCanvasbekommt einen neuenonBackgroundTap-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
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 ConnectViewModels 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
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): zentraleAppStorage-Key-Konstanten +AppPreferences.registerDefaults()(inRouterOSAssistantApp.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 altenOverviewStyle.color(for:)-Funktionen (die jetzt nur nochexplanation(for:)/icon(for:)halten).AppTextSize(Klein/Standard/Groß/Sehr groß) — Skalierungsfaktor (0,85/1,0/1,2/1,4), per.environment(\.appFontScale, ...)an derWindowGroup-Wurzel gesetzt.UIDensity(Kompakt/Standard/Komfortabel) — mappt aufControlSize, 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/DevicesViewlesencolorThemeper@AppStorageund geben es anEdgesCanvas/NodeCardView/NodeDetailView/EdgeDetailView/EdgeTooltipView/LegendView/DeviceRow/PortScanRowdurch.DevicesViewModel.startTrafficPolling/appendTrafficHistorylesen Intervall/Zeitfenster bei jedem Tick frisch ausUserDefaults(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 dieselbecheckForUpdates(for:)auf, die sonst nur der manuelle Button im Verbinden-Tab auslöst. - L10n.swift um alle neuen Settings-Strings ergänzt (DE/EN).
.xcodeprojmanuell 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 instandardundhighContrastexakt 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 AufrufbaseSize * scalereal 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, festenodeWidth/nodeHeight) undDevicesView.DeviceRow/DeviceColumnHeader(LAN-Scanner-Tabellenspalten mit festenDeviceColumn-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
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 BackupServices 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
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
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:<a id="schema-...">-Anker automatisch vor jede Experte-Menü-Überschrift eingefügt (schema_anchor(menuPath), deterministisch aus dem RouterOS-Menüpfad).Manual.mdbekam zusätzlich 12 handgesetzte<a id="tab-..."/"step-...">-Anker vor den 6 Haupt-Tab- und 6 Wizard-Schritt-Überschriften. Neue Funktionrender_app_html()erzeugt ein eigenständigesRouterOSAssistant/Resources/Manual.html(Bilder als Base64 eingebettet, keinManual-assets/-Ordner nötig im App-Bundle) — MarkdownIt jetzt mithtml=True, sonst wären die<a id>-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:)(spiegeltschema_anchor()exakt — kein generiertes Mapping nötig, beide Seiten wenden dieselbe simple Transformation an).ManualNavigator(ObservableObject, app-weit als@StateObjectinRouterOSAssistantApp) hält den angeforderten Anker.ManualWebView(WKWebView viaNSViewRepresentable) lädt das gebündelteManual.htmleinmalig, springt bei Anker-Änderung per JavaScriptscrollIntoView(kein Reload/Flackern). NeuesWindow("Handbuch", id: "manual")inRouterOSAssistantApp.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), undExpertMenuDetailViewbekommt 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 wieManual.md. Kapitel 8 "English summary" (bisheriger Behelf) ausManual.mdentfernt, da jetzt redundant.build-manual.pygeneralisiert: neuesLANGUAGES-Dict ({"de": Manual.md, "en": Manual.en.md}, für weitere Sprachen nur ein neuer Eintrag +Manual.<code>.mdnötig, kein neuer Python-Code). Kapitel 5 (Experte-Referenz, automatisch generiert) wird jetzt pro Sprache übersetzt, indemL10n.swifts 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:ManualWebViewlädt jetztManual.html(de) oderManual_<sprache>.html(alles andere), abhängig von@AppStorage("appLanguage"), mit Fallback auf Deutsch falls eine Sprache (noch) keine Übersetzung hat. Lädt bei Sprachwechsel neu (Coordinatormerkt 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
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 Forms 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
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). statebleibt bewusst durchgehend.connected, damit andere Tabs während eines kurzen Aussetzers nicht auf ihre "Nicht verbunden"-Platzhalter umspringen — nurisReconnecting(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:
- 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. FirewallStepView-Titel behauptete "Firewall (optional)" auch im Einfach-Modus, wo der Grundschutz laut Text direkt darunter fest aktiv ist. Fix: Titel modusabhängig.- Zwei fehlende Englisch-Übersetzungen (skriptgeprüft gegen alle
L10n.t(...)-Aufrufstellen): "Netzwerk-Test fehlgeschlagen", "Fokus-Ansicht schließen". Ergänzt inL10n.swift. - 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: neuerbeginWrite()/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.cliLinequotete Werte nur bei Leerzeichen und escapte nie eingebettete". Ein Kommentar wietest" ; :log warning "Xschloss das Quoting vorzeitig und ließ RouterOS den Rest als zweiten, unabhängigen Befehl ausführen — live exploitiert (Log zeigt injiziertenscript,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 istwww-ssldeaktiviert, 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 tersequotet 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, BackupServices 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.fetchFieldValueshatte 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.sha256fiel 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:
ReviewApplyViews "Anwenden fehlgeschlagen"-Alert: OK-Button-Action war leer, dieisPresented-Bindings-Setter-Closure ebenfalls ein No-Op —applyErrorwurde nie zurückgesetzt, der Alert konnte sich nach dem Schließen theoretisch sofort wieder öffnen.ConnectViews "Unbekanntes Zertifikat"-Alert: "Abbrechen"-Button-Action war komplett leer —connectionService.stateblieb für immer auf.needsCertificateConfirmationhängen, es gab keinen Weg zurück außer dem Zertifikat zu vertrauen.ConnectViews "Unbekannter SSH-Schlüssel"-Alert: "Abbrechen" rief nurdismissPendingSSHTrust()(löschte nurpendingSSHTrustFingerprint) — 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
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). Noch nicht live im UI durchgeklickt (UI-Automatisierung nicht verfügbar) — die einzelnen Bausteine (Konflikt-Erkennung, Resolution-Commands inkl. Bridge-Detach) sind unit-getestet, der volle Ablauf im Experte-Tab selbst noch nicht.