focusPanel nutzte dieselbe Spalten-Layout-Logik wie das Hauptdiagramm, hatte aber nie deren Header-Zeile (Interfaces/IP-Adressen/...) übernommen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
28 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)
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)
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)
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: offen (implementiert, bitte live testen)
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")
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)
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)
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: offen
8. Abwechselnde Farbkombis bei Tabellenansichten
Status: offen
Bessere Lesbarkeit durch alternierende Zeilenfarben in Tabellenansichten (LAN-Scanner, Experte-Listen, etc.).