Files
RouterOS/HANDOFF.md
T
KayandClaude Sonnet 5 e2b303963a M12: Geräte-Tab — LAN-Scanner mit Static-IP-Zuweisung
Neuer Tab: eine Tabelle pro physischem Ethernet/WLAN-Port mit den dort
gefundenen Geräten (Name, IP, MAC, Fest/Dynamisch/Kein-DHCP), gebaut aus
DHCP-Leases + ARP + Bridge-Host-Tabelle. Rechtsklick auf ein dynamisches
Gerät -> "Feste IP zuweisen" (RouterOS' "Make Static", per
/ip dhcp-server lease make-static), mit Bestätigungsdialog und
Session-Backup vor dem ersten Schreibvorgang (geteilter Mechanismus mit
dem Experte-Tab).

Vier reale Bugs live gefunden und gefixt (siehe HANDOFF.md Bug 14-17):

- "print terse" gibt das "dynamic"-Feld von /ip dhcp-server lease nie
  aus, in keinem Zustand -> Status kommt jetzt über RouterOS' find/get
  gegen die interne Eigenschaft, nicht aus gelesenen Feldern.
- fetchMenuItems' .id-Positionsüberlagerung ordnete für dieses Menü die
  falsche .id der falschen Zeile zu -> Erkennung und make-static-Ziel
  laufen jetzt über die MAC-Adresse statt .id.
- Ein SwiftUI-.confirmationDialog löschte sein eigenes Ziel-Objekt vor
  der Ausführung der bestätigten Aktion (Setter feuert bei jedem
  Knopfdruck, nicht nur Abbrechen) -> Dialog-Sichtbarkeit und
  Nutzlast entkoppelt, wie in BackupListView.
- Die eigene Verifikations-Abfrage (get [find ...] feld als ein
  kombinierter Befehl) war selbst eine nie verifizierte Annahme und
  lieferte falsche Negative -> ersetzt durch :foreach aus zwei einzeln
  bestätigten Bausteinen (find, get <id> feld).

RouterOSCommand bekommt einen neuen .action-Operationstyp für
RouterOS-"Menü-spezifische Befehle" jenseits von add/set/remove (aktuell
nur make-static). HANDOFF.md/CHATLOG.md mit allen vier Bugs, neuen
Milestones M11/M12 und offenen Punkten aktualisiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ
2026-09-14 13:55:46 +02:00

44 KiB
Raw Blame History

RouterOS Assistant — Handoff

Stand: App-Icon fertig, alle Kern-Features (M1M6) gebaut und mindestens teilweise gegen zwei echte physische Mikrotik-Testgeräte verifiziert (ein generisches Gerät für M1M4, ein hEX-Router für M5/M6 — dabei ein kritischer Parser-Bug gefunden und gefixt, siehe unten). Danach noch mehrere Nutzer-Wünsche umgesetzt: Tooltips, wählbarer Backup-Ordner, Schnell-Backup im Verbinden-Tab, "Werkseinstellungen wiederherstellen", eigenes App-Icon. Anschließend M9 (Einfach/Experte-Modusschalter im Wizard) und M10 (Experte-Tab: kuratiertes Formular für einen großen Teil von RouterOS plus generischer "eigener Menüpfad"-Zugriff für den Rest) — beide live gegen den hEX-Testrouter verifiziert, sieben Bugs dabei gefunden und gefixt (Bug 713). Alles bis hierhin committed (c9ecd3a).

Neu in dieser Session: zwei weitere Tabs. M11 "Übersicht" zeigt die komplette aktuelle Router-Konfiguration als Diagramm (Interfaces → IP-Adressen → DHCP/Pools → Routen → Firewall/NAT, mit echten, aus RouterOS' eigenen Referenzfeldern abgeleiteten Verbindungslinien, farblich nach Verbindungsart getrennt, Hover/Klick hebt Zusammenhänge hervor). M12 "Geräte" ist ein LAN-Scanner (DHCP-Leases + ARP + Bridge-Host-Tabelle, gruppiert als eine Tabelle pro physischem Port) mit Rechtsklick-Aktion "Feste IP zuweisen" (RouterOS' "Make Static", per /ip dhcp-server lease make-static). Beide Tabs live gegen den hEX-Testrouter verifiziert — M12 dabei besonders gründlich, weil die Fest/Dynamisch-Erkennung und die Zuweisung selbst sich als überraschend zäh erwiesen: vier echte Bugs gefunden und gefixt (Bug 1417 unten), am Ende vom Nutzer selbst bestätigt ("das funktioniert jetzt super"). Release-Build unter /Applications/RouterOS Assistant.app ist auf aktuellem Stand.

Ziel

Native macOS-App (SwiftUI), die Laien per geführtem Interview-Wizard durch die häufigsten Mikrotik-RouterOS-Konfigurationen führt: Internet-Anschluss, Heimnetzwerk/DHCP, zusätzliche VLAN-Netzwerke, WLAN, Firewall-Grundschutz, plus Backup/Wiederherstellung. Vollständiger ursprünglicher Plan/Kontext: ~/.claude/plans/purrfect-puzzling-bunny.md (seither an vielen Stellen weiterentwickelt, siehe unten).

Setup / Bauen

cd RouterOS
xcodegen generate                 # erzeugt RouterOSAssistant.xcodeproj (nicht in git)
open RouterOSAssistant.xcodeproj  # oder direkt per xcodebuild

# Debug (Xcode Cmd+R empfohlen für manuelle/Live-Tests, siehe Gatekeeper-Hinweis unten)
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
  -destination 'platform=macOS' build

# Nur kompilieren, nicht ausführen (umgeht den Gatekeeper-Hang, siehe unten)
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
  -destination 'platform=macOS' build-for-testing

# Release + Deploy nach /Applications
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
  -configuration Release build
rm -rf "/Applications/RouterOS Assistant.app"
cp -R "<DerivedData>/Build/Products/Release/RouterOSAssistant.app" \
  "/Applications/RouterOS Assistant.app"
killall Dock   # Icon-Cache auffrischen, falls sich nur das Icon geändert hat

xcodegen ist via Homebrew installiert. Projekt-Struktur/Targets/Package- Abhängigkeiten stehen in project.yml, das ist die Quelle der Wahrheit — nicht das generierte .xcodeproj von Hand editieren.

Git: lokales Repo, kein Remote (Nutzer hat aktuell keine Gitea-Instanz). .xcodeproj, DerivedData, .build, Firmware-Dateien (*.npk, *.cpgz) und Router-Backups (*.rsc, /Backups/) sind gitignored.

Architektur

RouterOSAssistant/
  App/RouterOSAssistantApp.swift       — 3 Tabs, teilen sich EINE ConnectionService-Instanz
  Core/
    Models/
      RouterOSCommand.swift            — eine Änderung, drei Operationen (.add/.set/.remove), zwei Renderer (CLI-Zeile / REST-JSON)
      WanConfig.swift, LanDhcpConfig.swift, VlanEntry.swift, WifiNetworkConfig.swift, FirewallConfig.swift  — bauen je RouterOSCommand-Listen
      DhcpServerCommandBuilder.swift   — geteilte "Adresse+Pool+Server+Netzwerk"-Logik (LAN + VLAN)
      RouterOSModels.swift             — Credentials, DeviceInfo, Interface, RouterOSError
      RouterOSMenuItem.swift           — generisches Item (id + [String:String] fields) für JEDEN RouterOS-Menüpfad (M10)
      RouterOSSchema.swift             — RouterOSFieldSchema/RouterOSMenuSchema/RouterOSMenuCategory: kuratiertes Formular-Schema fürs Experte-Tool (M10)
      RouterOSSchemaCatalog.swift      — die eigentliche Menü-Liste: Firewall-Familie voll kuratiert, Rest teils kuratiert/teils generisch (M10)
      OverviewGraph.swift              — OverviewNode/OverviewEdge/OverviewEdgeKind/DevicePortGroup: reine Datenmodelle fürs Übersicht- und Geräte-Diagramm (M11/M12)
      LanDevice.swift                  — ein LAN-Gerät (DHCP-Lease + ARP + Bridge-Host verschmolzen), fürs Geräte-Tab (M12)
    Networking/
      RouterOSTransport.swift          — Protocol: connect/fetchDeviceInfo/fetchInterfaces/fetchFirewallRuleCounts/fetchMenuItems/fetchFieldValues/apply/disconnect
      RestTransport.swift              — REST-API (RouterOS ≥7.1), Zertifikats-TOFU, .set via GET+PATCH (findItemID), .remove via GET+DELETE, fetchMenuItems (Array- oder Singleton-Objekt), fetchFieldValues (Query-Filter, unverifiziert)
      SSHTransport.swift               — SSH-Fallback via Citadel, CLI-Text-Parsing, resetToFactoryDefaults(), eigene Hostkey-TOFU, fetchMenuItems (generisch, inkl. Singleton-Fallback), fetchFieldValues (`:foreach`+`find`+`get`, M12/Bug 14+17)
      RouterOSCliParser.swift          — parst `/system resource print`, `/interface print terse`, und generisch jeden Menüpfad (parseGenericItems/parseSingletonItem, M10)
      CertificateTrustStore.swift / CertificateFingerprint.swift  — TOFU für REST-Zertifikate
      SSHHostKeyTrustStore.swift / SSHHostKeyFingerprint.swift    — TOFU für SSH-Hostkeys (M7)
    Services/
      ConnectionService.swift          — zentraler App-State: REST-zuerst-SSH-Fallback, hält credentials/interfaces/deviceInfo, plus generisches fetchMenuItems
      BackupService.swift              — Config-Export (`/export terse`) über eigene SSH-Verbindung, wählbarer Zielordner
      FactoryResetService.swift        — /system reset-configuration über eigene SSH-Verbindung ("Gefahrenzone")
      KeychainService.swift            — Passwort-Speicherung
  Features/
    Wizard/Steps/Connect/              — Verbinden-Tab (inkl. Schnell-Backup-Button nach erfolgreicher Verbindung)
    Wizard/Steps/Setup/                — Einrichten-Tab: Mode → Wan → Lan → (Vlan nur Experte) → Wifi → Firewall → Review/Apply
    Expert/                            — Experte-Tab (M10): ExpertView (Kategorie-/Menüliste + "eigener Pfad"), ExpertViewModel, ExpertMenuDetailView (Item-Liste + Add/Edit-Sheet)
    Overview/                          — Übersicht-Tab (M11): OverviewView (Diagramm+Legende+Detailpanel), OverviewViewModel (buildGraph, rein), OverviewLayout (Spalten/Zeilen-Geometrie)
    Devices/                           — Geräte-Tab (M12): DevicesView (Port-Tabellen+Rechtsklick+Rohdaten-Sheet), DevicesViewModel (buildDevices/buildPortGroups, rein; confirmStaticAssignment)
    Backup/                            — Sicherungen-Tab (Ordner wählen, Gefahrenzone: Werkseinstellungen wiederherstellen)
  Resources/Assets.xcassets/AppIcon.appiconset/  — App-Icon "Signal Router" (16px1024px)
RouterOSAssistantTests/                — reine Unit-Tests (Command-Builder, CLI-Parser, Fallback-Logik via Mock-Transport)

Architekturprinzip RouterOSCommand (.add/.set): .add legt neue Einträge an (CLI ... add key=value, REST POST). .set ändert bestehende Einträge, per matchField/matchValue gefunden — CLI löst das inline über set [find field=value] ..., REST muss dafür erst per GET das Item suchen, seine RouterOS-interne .id auslesen, dann PATCH restPath/<id> schicken (RestTransport.findItemID). Eingeführt für WLAN (M5), weil Wireless-Interfaces schon vor jeder Konfiguration existieren. VLAN (M4) hat trotzdem weiterhin keine Port-Zuweisung (Access/Trunk) — technisch jetzt machbar, aber bewusste Scope-Entscheidung, nicht nachgezogen.

System-Aktionen (Backup, Werksreset) laufen immer über eine eigene SSH-Verbindung, unabhängig vom aktiven Live-Transport — BackupService und FactoryResetService folgen demselben Muster: es sind einmalige System-Kommandos, kein Menü-Item zum Hinzufügen/Ändern, passen also nicht ins RouterOSCommand-Modell und haben keine verifizierte REST-Entsprechung.

ConnectionService ist der einzige geteilte State. Alle drei Tabs bekommen dieselbe Instanz von der App-Ebene injiziert (kein @EnvironmentObject, explizite Übergabe im Init). SwiftUI-Falle: jede View, die connectionService-Felder liest, muss ihn selbst als @ObservedObject halten — nicht nur transitiv über ein anderes ViewModel erreichen (siehe Bug 1 unten).

Gefundene Bugs (chronologisch, alle gegen echte Geräte)

  1. ConnectView zeigte nie VerbindungsstatusconnectionService war nur über viewModel.connectionService erreichbar (zwei Ebenen tief verschachteltes ObservableObject), SwiftUI abonniert das nicht automatisch. Fix: ConnectView hält connectionService zusätzlich selbst als @ObservedObject.

  2. NIOSSHError.localizedDescription nutzlos — bridged auf generisches NSError ("error 1"). Fix: String(describing:).

  3. SSH scheiterte mit keyExchangeNegotiationFailure — RouterOS bietet nur Legacy-Algorithmen an, Citadel-Default enthält sie nicht. Fix: algorithms: .all.

  4. Citadel.SSHClient.CommandFailed zeigte nur den Exit-CodeexecuteCommand() verwirft die gesammelte Ausgabe bei Exit-Code ≠ 0. Fix: eigene Sammlung über executeCommandStream().

  5. Picker-Warnungen durch Platzhalter-InterfacenamenprepareDefaults() korrigiert jetzt WAN und LAN, läuft schon im init.

  6. Kritisch: CLI-Parser verlor 6 von 7 Interfaces auf einem hEX-RouterparseInterfaces/parseDeviceInfo splitteten nur auf "\n"; die SSH-Ausgabe dieses Geräts trennte Zeilen anders, sodass alle Interface-Zeilen zu einer verschmolzen — beim key=value-Parsen überschrieb jedes Feld den vorherigen Wert, übrig blieb nur das letzte Interface im Text (lo, Loopback) mit vermischten Werten. Folge: jede WAN-Interface-Referenz in den angewendeten Firewall-Regeln zeigte auf lo statt den echten WAN-Port — auf diesem Testgerät folgenlos, weil RouterOS bereits eine vollständige eigene Standard-Firewall (defconf) mitbrachte, die weiterhin schützte; auf einem Gerät ohne bestehende Firewall wäre das eine wirkungslose, aber als aktiv angezeigte Firewall gewesen. Fix: split(whereSeparator: \.isNewline) (robust gegen \n/\r/\r\n) + zusätzliches Sicherheitsnetz: Loopback-Interfaces werden in SetupView jetzt grundsätzlich aus allen WAN/LAN/VLAN- Auswahllisten gefiltert. Regressionstest mit echten \r\n-getrennten hEX-Daten in RouterOSCliParserTests.

  7. /ip service: Feld heißt available-from, nicht address — beim Kuratieren des Experte-Schemas geraten statt geprüft. Live gegen den hEX bestätigt: /ip service print zeigt tatsächlich available-from=; ein set ... address=... hätte den Zugriffsfilter stillschweigend nicht gesetzt. Schema korrigiert (RouterOSSchemaCatalog.swift).

  8. RouterOS 7.24.2 zeigt .id gar nicht in print terse, und Singleton-Menüs verweigern terse komplett — Annahme beim Bau des generischen Experte-Motors (M10) war, .id=*N sei Teil jeder print terse-Zeile (wie bei REST-JSON). Live-Test zeigte: RouterOS 7.24.2 liefert dort gar kein .id-Feld; Menüs mit genau einem Eintrag statt einer Liste (/ip dns, /system identity, /system clock, /system ntp client, /tool e-mail) lehnen terse sogar komplett ab ("bad parameter terse"). Fix: :put [<Menüpfad> find] liefert die echten internen IDs (z.B. *C;*1;*2) — live bestätigt, dass die Reihenfolge exakt der von print terse entspricht, damit per Position zuordenbar. Für Singleton-Menüs neuer RouterOSMenuSchema.isSingleton- Schalter: Fetch fällt auf normales print (Doppelpunkt-Format, parseSingletonItem) zurück, .set läuft ohne [find ...] direkt (matchField: "" als Sentinel in RouterOSCommand/beiden Transports). Beides live gegen /system identity durchgespielt (Name testweise geändert, Fehlertext exakt geprüft, zurückgesetzt).

  9. Cross-Referenz-Felder (z.B. DHCP-Server → Adress-Pool) waren reiner Freitext — Nutzer meldete: "kann den vorher angelegten Adress-Pool nicht in der Auswahl sehen". Betraf jedes Feld, das den Namen eines Eintrags aus einem anderen Menü referenziert (DHCP-Server→Pool, PPP-Benutzer→Profil, Scheduler→Skript, Firewall-Filter→Adress-Liste, WireGuard-Peer→Interface) — alle als .text statt als Auswahl gebaut. Fix: neuer Feldtyp RouterOSFieldSchema.Kind.menuItemPick(menuPath: restPath:valueField:), lädt beim Öffnen eines Menüs live die existierenden Namen aus dem referenzierten Menü (ExpertViewModel.loadCrossReferenceOptions) und zeigt sie als Picker. Auf alle fünf betroffenen Felder angewendet.

  10. Kritisch, app-weit: RouterOS' SSH-CLI gibt bei fehlgeschlagenen Befehlen Exit-Code 0 zurück — Nutzer meldete: DHCP-Server-Anlage zeigte keine Fehlermeldung, wurde aber auch nicht angelegt. Live reproduziert: /ip dhcp-server add ... auf einem Interface mit bereits bestehendem Server lieferte "failure: server or relay with such interface already exists" und Exit-Status 0; ein ungültiger action=-Wert lieferte "syntax error (...)", ebenfalls Exit-Status 0 (per expect-Skript mit catch wait result geprüft). SSHTransport. apply() warf bisher nur bei einem von Citadel gemeldeten Nicht-Null-Exit-Code — also nie. Betraf jeden .apply()-Aufruf über SSH app-weit, nicht nur den Experte-Tab: der komplette Einrichten- Wizard (WAN/LAN/VLAN/Firewall) lief bei jedem SSH-Schreibvorgang mit demselben blinden Fleck, seit M3. Fix: apply() behandelt jede nicht-leere Ausgabe eines add/set/remove-Befehls als Fehler — laut allen in dieser Session live getesteten Befehlsfamilien (15+) sind erfolgreiche mutierende Befehle immer still. Nach dem Fix live end-to- end bestätigt: komplette Kette VLAN→IP-Adresse→Pool→DHCP-Server→ DHCP-Netzwerk auf einem freien Interface angelegt, lief fehlerfrei durch, danach vollständig wieder entfernt.

  11. Bool-Felder im Experte-Schema als "true"/"false" statt "yes"/"no" geschrieben — Nutzer meldete nach Bug-10-Fix: VLAN-Anlage zeigte jetzt "Unerwartete Antwort vom Router: syntax error (line 1 column 30)". Live reproduziert mit der exakt von der App erzeugten Befehlszeile (disabled=false) — identischer Fehler; mit disabled=no lief es durch. RouterOS' CLI akzeptiert nur yes/no für Bool-Parameter, nicht true/false. Betraf praktisch jedes kuratierte Schema (23× "false", 4× "true" als defaultValue). Fix zweigleisig: ExpertViewModel.pendingCommand normalisiert jeden .bool-Feldwert vor dem Senden auf yes/no (Schutz unabhängig von der Quelle), zusätzlich Katalog-Strings selbst bereinigt. Live erneut end-to-end bestätigt (VLAN→IP→Pool→DHCP-Server mit disabled=no).

  12. Lösch-Buttons fehlten komplett im Experte-Tab — Nutzer: "mir fehlen komplett die Löschen-Buttons". Ursache: .swipeActions(...) ist eine iOS/iPadOS-Wischgeste ohne Entsprechung auf macOS-Listen — dort erscheint schlicht nichts, unabhängig vom Code. Fix: sichtbarer Papierkorb-Icon-Button je Zeile statt Wisch-Geste (nur bei Nicht-Singleton-Menüs, wo Löschen überhaupt Sinn ergibt). Lösch-Pfad (ID-Ermittlung + remove [find .id=...]) live gegen das vom Nutzer selbst angelegte VLAN nachvollzogen, funktioniert.

  13. Interface-Auswahl (.interfacePick) war einmalig beim Verbinden eingefroren — Nutzer legte ein VLAN an, danach fehlte es in der Interface-Auswahl beim Anlegen eines DHCP-Servers. Ursache: .interfacePick las aus ConnectionService.interfaces, nur einmal bei finishConnecting() befüllt, nie danach aktualisiert — anders als die neueren .menuItemPick-Felder (Pool/Profil/Skript), die bei jedem Menü-Öffnen frisch laden. Fix: neues ExpertViewModel.liveInterfaceNames, per /interface print terse bei jedem reloadItems() aktualisiert; availableInterfaces-Parameter- Durchreichung (ExpertViewExpertMenuDetailViewExpertItemEditView) komplett entfernt, da überflüssig. Danach vom Nutzer selbst in der App bestätigt: neu angelegtes VLAN erscheint sofort in der DHCP-Server-Interface-Auswahl.

  14. /ip dhcp-server lease gibt das dynamic-Feld über print terse nie aus, in keinem Zustand — beim Bau des Geräte-Tabs (M12) zwei falsche Annahmen nacheinander gemacht: erst "fehlt = fest" (Standard- Annahme falsch herum, jedes echte DHCP-Gerät zeigte "Fest"), dann nach Korrektur "fehlt = dynamisch, dynamic=no erscheint explizit bei festen Leases" — beides falsch. Live an echten Rohdaten geprüft (Nutzer schickte per "Rohdaten anzeigen" wiederholt komplette Feldlisten, u.a. für einen zweifelsfrei dynamischen UND einen zweifelsfrei festen Eintrag): das Feld taucht in terse überhaupt nie auf, unabhängig vom Status — nur /ip dhcp-server lease print (ohne terse) zeigt es als Flag-Buchstabe D. Fix: Status kommt nicht mehr aus gelesenen Feldern, sondern aus einer gezielten Abfrage gegen RouterOS' interne Eigenschaft (RouterOSTransport.fetchFieldValues, siehe Bug 17 für die korrekte Syntax).

  15. fetchMenuItemss .id-Positions-Überlagerung ordnete bei /ip dhcp-server lease die falsche .id der falschen Zeile zu — zwei Leases (iMac dynamisch, Laptop nach Umstellung fest), aber die App zeigte beide als "Fest". Ursache: fetchMenuItems liest die .ids über einen separaten :put [<Pfad> find]-Aufruf und ordnet sie nach Position den print terse-Zeilen zu (Mechanismus aus Bug 8, dort für ein anderes Menü live bestätigt) — für dieses Menü stimmte die Reihenfolge zwischen beiden Aufrufen offenbar nicht überein, die feste .id landete auf der iMac-Zeile. Fix ausschließlich im Geräte-Tab: Erkennung und der make-static-Zielauswahl laufen jetzt komplett über die MAC-Adresse (steht direkt in derselben terse-Zeile, kein Cross-Command-Abgleich nötig) statt über .id. Nicht behoben: derselbe .id-Überlagerungsmechanismus wird auch vom Experte-Tab für .set/.remove auf beliebigen Menüs benutzt — ob das dort ebenfalls (menü- oder situationsabhängig) falsch zuordnen kann, ist ungeprüft. Siehe Nächste Schritte.

  16. SwiftUI-.confirmationDialog löschte sein eigenes Ziel, bevor die bestätigte Aktion es lesen konnte — "Feste IP zuweisen" tat sichtbar nichts: kein Fehler, kein Erfolg, Dialog schloss normal. Ursache: isPresented war eine berechnete Binding, deren Setter bei jedem Schließen (auch durch den bestätigenden Knopf selbst — SwiftUI ruft den Setter mit false bei jedem Tap in einem .confirmationDialog, unabhängig vom gedrückten Knopf) pendingStaticAssignment = nil setzte — also genau das Objekt, das confirmStaticAssignment() Sekundenbruchteile später lesen wollte. Fix: Dialog-Sichtbarkeit (showStaticConfirmation, ein einfaches @State Bool) und Ziel- Objekt (pendingStaticAssignment) komplett entkoppelt — genau das Muster, das BackupListViews Werksreset-Dialog schon die ganze Zeit korrekt verwendet (eigenes @State-Bool statt abgeleitetem Binding).

  17. Ein erster Fix-Versuch für Bug 14 geriet zu einer weiteren, nie verifizierten Befehls-Annahme<Menü> get [find feld=wert] rückgabefeld] als ein kombinierter Befehl geraten (Annahme: get akzeptiert eine find-Ergebnisliste direkt). Live widerlegt: eine per Winbox bestätigte, echte feste Zuweisung wurde von dieser Abfrage trotzdem nicht gefunden (falsches Negativ). Fix: nur noch aus zwei einzeln diese Session mehrfach bestätigten Bausteinen zusammengesetzt — nacktes find mit einer Bedingung, plus get <id> feld auf eine einzelne bekannte .id, verbunden über eine :foreach-Schleife (RouterOSTransport.fetchFieldValues/SSHTransport.fetchFieldValues).

Lehren: Citadel/NIOSSH-Fehler immer mit String(describing:) loggen, nie .localizedDescription. Jede View, die ein ObservableObject aus einem ViewModel liest, muss es selbst separat als @ObservedObject halten. Zeilen-Splitting bei SSH-Ausgaben grundsätzlich mit \.isNewline statt festem "\n", da sich das je nach Gerät/Version unterscheiden kann. Bei generischem RouterOS-Tooling (M10) nichts über CLI-Ausgabeformate annehmen, was nicht live geprüft ist — .id-Verhalten und terse-Support unterscheiden sich zwischen Menütypen und RouterOS-Versionen. RouterOS' SSH-Exit-Code ist kein verlässliches Erfolgssignal — Ausgabetext ist die einzige verlässliche Fehlerquelle über diesen Transport (Bug 10); REST ist davon nicht betroffen (prüft HTTP-Statuscodes korrekt). RouterOS' CLI will yes/no für Bools, nie true/false (Bug 11) — an einer einzigen Stelle normalisieren (pendingCommand) statt an jedem Schema-Eintrag hoffen, es richtig geschrieben zu haben. SwiftUI- Interaktionsmuster nicht plattform-blind übernehmen — .swipeActions sieht in der Doku harmlos aus, ist aber faktisch iOS/iPadOS-only (Bug 12). Jeder Zustand, der sich während der Nutzung ändern kann (Interface-Liste nach Anlegen eines VLANs), gehört live nachgeladen, nicht einmalig beim Verbinden gecacht (Bug 13) — dasselbe Prinzip wie crossReferenceOptions für Pool/Profil/Skript, nur nicht von Anfang an konsequent auf alle Auswahllisten angewendet. print terse gibt nicht jede Eigenschaft als Feld aus — manche (z.B. dynamic bei DHCP-Leases) erscheinen nur als Flag-Buchstabe im menschenlesbaren print, nie als key=value (Bug 14); wenn ein Feld partout nicht auftaucht, RouterOS' find/get gegen die interne Eigenschaft fragen statt weiter nach Formulierungen im Text zu suchen, die es nicht gibt. fetchMenuItemss .id-Überlagerung ist nicht für jedes Menü verlässlich (Bug 15) — bei mehreren möglichen Identifikatoren lieber über ein Feld matchen, das direkt in derselben Zeile steht (MAC, Name), statt über eine per Position aus einem zweiten Befehl zugeordnete .id. Jede kombinierte RouterOS-Skript-Syntax vor dem Einsatz live prüfen, auch wenn die Einzelteile schon bestätigt sind (Bug 17) — find allein und get <id> feld allein zu kennen heißt nicht, dass get [find ...] feld als eine Einheit genauso funktioniert. Bei SwiftUI-Bestätigungsdialogen Sichtbarkeit und Nutzlast nie an dasselbe State-Feld koppeln (Bug 16) — isPresenteds Setter feuert bei jedem Knopfdruck, nicht nur bei Abbrechen; ein eigenes, unabhängiges Bool fürs Zeigen/Verstecken hält die Nutzlast so lange am Leben, wie die bestätigte Aktion sie braucht.

xcodebuild test hängt — Gatekeeper, kein Code-Bug

xcodebuild ... test kann exakt beim App-Start für ~5 Minuten hängen, dann "The test runner hung before establishing connection.". Ursache (per log show, Filter auf syspolicyd/amfid): ad-hoc-signierte Binaries lösen bei syspolicyd einen Live-Netzwerk-Check (GK performScan) gegen Apples Gatekeeper-Server aus; jeder Neu-Build ändert den Binary-Hash und kann einen neuen, manchmal langsamen Check auslösen. Betrifft nur den CLI-Weg — Xcodes Cmd+R-Weg bekommt vom Debugger-Start eine Gatekeeper-Ausnahme. Deshalb: bei Hängern erst xcodebuild ... build-for-testing (kompiliert nur, kein App-Start, kein Gatekeeper-Trigger) für den Compile-Check nutzen, echte Testläufe/manuelle Verifikation dem Nutzer über Xcode überlassen statt CLI-Versuche zu wiederholen.

Bekannte Einschränkungen (bewusst, nicht vergessen)

  • Backup-Wiederherstellung fehlt komplett — Nutzer wies explizit darauf hin (2026-09-14): BackupService kann Backups nur erzeugen (createBackup) und auflisten (listBackups), es gibt keine Funktion, ein gespeichertes .rsc-Backup zurück auf den Router zu spielen. Einzige verwandte Funktion ist "Werkseinstellungen wiederherstellen" (FactoryResetService, Gefahrenzone im Sicherungen-Tab) — das ist RouterOS' eigener Werksreset, kein Zurück- spielen einer konkreten Sicherung. Noch nicht umgesetzt, für später vorgemerkt (siehe Nächste Schritte).
  • REST-Pfad ungetestet für Schreibvorgänge — auf beiden bisherigen Testgeräten war www-ssl (Port 443) aus, jeder Schreibtest lief über SSH. Der REST-apply()-Pfad (POST/PATCH, findItemID für .set) ist nur gegen Mocks getestet. Der REST-Zweig von fetchFieldValues (M12/Bug 14) ist zusätzlich auf einer nirgends offiziell dokumentierten Annahme aufgebaut (Query-String-Filter ?feld=wert auf einem GET) — komplett unverifiziert.
  • "Zurück auf dynamisch" fehlt im Geräte-Tab (M12) — es gibt nur "Feste IP zuweisen" (Winbox' "Make Static"), keine Umkehrung. Kein Bug, einfach noch nicht gebaut; Nutzer hat danach gefragt.
  • fetchMenuItemss .id-Positions-Überlagerung: Verlässlichkeit für andere Menüs ungeprüft (siehe Bug 15) — bei /ip dhcp-server lease live als falsch bestätigt (.id landete auf der falschen Zeile), für den Geräte-Tab per MAC-Adress-Matching umgangen. Derselbe Mechanismus läuft aber weiterhin im Experte-Tab für .set/.remove auf jedem Menü — ob und wo das dort ebenfalls falsch zuordnen kann, ist nicht untersucht. Sollte bei unerklärlichem Verhalten dort (falsches Item geändert/gelöscht) als erste Verdachtsquelle geprüft werden.
  • WLAN-.set-Pfad (M5) weiterhin ungetestet gegen echte Hardware — nur der "kein WLAN"-Zweig ist bestätigt (zwei Testgeräte, beide ohne WLAN-Chip). Sicherheitsprofil-Anlage + SSID/Passwort-.set auf einem echten /interface wireless-Interface noch nie live gelaufen.
  • Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut bestätigt — der erste Live-Test (M6) lief technisch durch, traf aber wegen Bug 6 oben lo statt des echten WAN-Ports. Nach dem Parser-Fix wurde nur die Interface-Auswahl erneut bestätigt ("alle Interfaces auswählbar"), nicht aber ein erneuter Firewall-Apply mit korrektem Interface + Kontrolle der resultierenden Regeln.
  • Neuer RouterOS-WiFi-Treiber (/interface wifi, wifiwave2/802.11ax) nicht unterstützt — wird erkannt und im UI erklärt, aber nicht konfiguriert.
  • Kein garantiertes Auto-Rollback bei Verbindungsabbruch während "Jetzt anwenden" — nur Backup-vorher + Bestätigungspflicht.
  • App ist ad-hoc signiert, nicht notarisiert — beim ersten Start einer frisch nach /Applications kopierten Version zeigt macOS die "nicht verifizierter Entwickler"-Warnung (Rechtsklick → Öffnen nötig).
  • SSH-Hostkey-TOFU ungetestetbestätigt gegen echte Hardware: erste Verbindung zeigte den "Unbekannter SSH-Schlüssel"-Dialog, nach Bestätigen + Trennen + erneutem Verbinden lief es ohne Rückfrage durch.
  • Experte-Tool (M10): mehrere kuratierte Bereiche noch nicht gegen Hardware bestätigt — WLAN (Legacy- und neuer wifiwave2-Treiber: Testgerät hat keinen WLAN-Chip), Bonding und PPPoE-Client (beide riskant für die eigene Erreichbarkeit/brauchen echte ISP-Zugangsdaten) nur gegen RouterOS-Dokumentation kuratiert, nicht live geprüft. /ip address, /ip dhcp-client, /ip dns, /ip service nur lesend geprüft (Feldnamen bestätigt), keine Schreibtests — Risiko, sich selbst auszusperren (SSH/REST-Dienst) bzw. die WAN-Verbindung zu kappen.
  • Experte-Tool: einige RouterOS-Bereiche bewusst nur generisch, nicht kuratiert — Hotspot, Logging (beide mehrteilig, kein flaches Schema), CAPsMAN, L2TP-/OpenVPN-Server, OSPF/BGP-Routing. Grund: keine verifizierte Quelle für die genauen Feldnamen, lieber generischer Schlüssel/Wert- Zugriff als geratene Felder (siehe Bug 7 oben, warum das eine berechtigte Sorge ist).

M8: Mehrere LAN-Interfaces + Netzwerk-Isolation

Auf Nutzerwunsch: mehrere physische LAN-Interfaces je mit eigenem DHCP-Server (SetupViewModel.lanConfigs: [LanDhcpConfig], analog zum schon vorhandenen VLAN-Listen-Muster), plus ein Isolation-Toggle pro LAN-/VLAN-Eintrag ("Von anderen Netzwerken isolieren"). Bei aktivem Firewall-Grundschutz erzeugt FirewallConfig daraus paarweise Forward-Drop-Regeln zwischen jedem isolierten Netzwerk und allen anderen konfigurierten Netzwerken (beide Richtungen, Pair-Dedup bei gegenseitiger Isolation). Internetzugriff bleibt weiterhin global über die bestehende WAN-NAT-Logik, nicht pro Netzwerk abschaltbar (bewusst außerhalb Scope).

Nebenbei behoben: VlanStepView behauptete vorher im Hilfetext bereits Isolation vom Hauptnetzwerk, ohne dass eine einzige Firewall-Regel das durchsetzte — reiner Text ohne Wirkung. Jetzt ist Isolation ein echter, optionaler Schalter mit tatsächlicher Regel-Erzeugung.

Nur gegen Unit-Tests verifiziert (FirewallConfigTests: isoliertes Netzwerk gegen nicht-isoliertes, gegenseitige Isolation, keine Isolation). Noch nicht gegen echte Hardware getestet — bräuchte zwei getrennte Testnetze am selben Router, um die Drop-Regeln praktisch zu bestätigen.

M9: Einfach/Experte-Modus im Einrichten-Wizard

Nutzerwunsch: zwei Modi statt einem Wizard-Durchlauf für alle. Neuer erster Schritt SetupStep.mode (ModeStepView, Segmented Picker).

  • Einfach: WAN → LAN (nur ein Netzwerk, kein Hinzufügen/Entfernen, kein Isolations-Toggle) → WLAN → Firewall (Grundschutz fest an, kein Toggle) → Review. VLAN-Schritt wird komplett übersprungen.
  • Experte: unveränderter voller Flow (Multi-LAN, VLAN, Isolation, Firewall-Toggle) — das, was vorher der einzige Wizard-Pfad war.

Beim Umschalten auf Einfach werden evtl. im Expertenmodus gesetzte Werte zurückgesetzt (SetupViewModel.applySimpleModeConstraints). Navigation (goNext/goBack) von linearem SetupStep.rawValue+1 auf expliziten switch umgebaut (nötig wegen bedingtem VLAN-Skip). Nur gegen Unit-Tests

  • Compile/Test-Build verifiziert, nicht gegen echte Hardware (der Live-Test dieser Session lief komplett über direktes SSH, nicht über die App-UI).

M10: Experte-Tab — generischer RouterOS-Konfigurationszugriff

Nutzerwunsch: "Zugriff auf alle Funktionen, was RouterOS bietet ... alles was du findest, der komplette Umfang". RouterOS hat 300+ Menüs (siehe /console inspect / CLI-Referenz) — statt für jedes einzelne ein Swift-Modell zu bauen (unrealistisch), zweistufige Architektur:

  1. Generischer Motor (deckt technisch jeden Menüpfad ab): RouterOSMenuItem + RouterOSCliParser.parseGenericItems/ parseSingletonItem, RouterOSCommand.remove (neu, neben add/set), ConnectionService.fetchMenuItems + Transport-Implementierungen. Im UI: freies "eigener Menüpfad"-Eingabefeld — jeder RouterOS-Pfad ist erreichbar, auch ohne kuratiertes Schema, dann als rohes Schlüssel/Wert-Formular.
  2. Kuratierte Schemas (RouterOSSchemaCatalog) mit echten Formularfeldern, Tooltips, Erklärtext und Warnhinweisen: die komplette Firewall-Familie (Filter/NAT/Mangle/Raw/Adress-Listen — der explizit genannte Schwerpunkt), dazu Interfaces (Bridge, VLAN, WireGuard+Peers, PPPoE-Client, Bonding), IP (Adressen, Pools, DHCP-Server+Netzwerk, DHCP-Client, DNS, Verwaltungsdienste, Routen), VPN (PPP-Secret/Profile), WLAN (Legacy + neuer wifiwave2-Treiber + Sicherheitsprofile), Queues (Simple/Tree), System (Identity/Clock/NTP/Scheduler/Skripte/Nutzer), Werkzeuge (Netwatch/E-Mail). Bewusst nicht kuratiert (siehe oben): Hotspot, Logging, CAPsMAN, L2TP-/OVPN-Server, OSPF/BGP.

Live gegen den hEX-Testrouter verifiziert (Add→Print→Set→Remove- Zyklus je Familie, Router danach nachweislich unverändert — Config-Export vorher/nachher byte-identisch bis auf Zeitstempel): Firewall Filter/NAT/Mangle/Raw/Adress-Listen, VLAN, WireGuard+Peers, IP-Pool, DHCP-Server+Netzwerk, statische Route, PPP-Secret/Profile, Queue Simple, Scheduler+Skript, Netwatch, sowie der neue Singleton-Pfad (/system identity). Dabei zwei Bugs gefunden und gefixt, siehe Bug 7/8 oben. Nicht gegen Hardware getestet: WLAN (kein Chip im Testgerät), Bonding, PPPoE-Client (beide riskant/brauchen echte Zugangsdaten), Schreibzugriff auf /ip address//ip dhcp-client//ip dns/ /ip service (nur lesend geprüft, Risiko Selbstaussperrung/WAN-Ausfall).

Nachtrag (Nutzer-Feedback-Runde, gleiche Session): Nutzer meldete zwei reale Probleme beim gedanklichen/App-Testen des DHCP-Server-Formulars — Bug 9 (Adress-Pool-Feld ohne Auswahl) und Bug 10 (fehlgeschlagenes Anlegen ohne jede Fehlermeldung). Beide gefunden und gefixt (siehe oben). Bug 10s Fix wurde erneut end-to-end live bestätigt: komplette Kette VLAN → IP-Adresse → Pool → DHCP-Server → DHCP-Netzwerk auf einem freien Interface (statt des zuvor kollidierenden ether5) angelegt, kein Fehlertext (weil tatsächlich erfolgreich), anschließend vollständig wieder entfernt.

Zweiter Nachtrag — jetzt tatsächlich durch die App-UI getestet (Nutzer selbst, gleiche Session): Nutzer legte VLAN + DHCP-Server über den Experte-Tab live an, fand dabei drei weitere reale Bugs (11: yes/no, 12: fehlende Lösch-Buttons, 13: eingefrorene Interface-Auswahl, siehe oben) — alle gefunden, gefixt, nach jedem Fix Release neu gebaut und deployt, vom Nutzer direkt in der App erneut getestet. Damit ist die SwiftUI-Oberfläche des Experte-Tabs (VLAN anlegen, einem DHCP-Server zuweisen, Adress-Pool wählen, löschen) erstmals tatsächlich manuell durchgeklickt und vom Nutzer als funktionierend bestätigt — nicht mehr nur die zugrundeliegende RouterOS-Syntax per SSH. Bestätigt: RouterOS erlaubt nur einen DHCP-Server pro Interface — der Versuch, ihn direkt auf ether5 zu setzen, scheiterte zurecht (dort hängt bereits dhcp5 aus einer früheren Session), auf dem VLAN-Interface funktioniert es.

Dritter Nachtrag — Sicherheits-Angleichung ans übrige App-Verhalten: Nutzer fragte, ob Experte-Tab-Änderungen sofort geschrieben werden oder eine Bestätigung braucht — Antwort war "sofort, kein eigener Bestätigungs-Button" (nur Löschen hatte einen Dialog). Das wich vom Einrichten-Wizard ab (Übersicht + Pflicht-Backup vor "Jetzt anwenden"). Auf Wunsch angeglichen:

  • Bestätigungsdialog vor Anlegen/Ändern: "Anlegen"/"Speichern" öffnet jetzt zuerst einen .confirmationDialog mit dem exakten Befehl, erst ein zweiter Klick führt ihn aus (ExpertMenuDetailView).
  • Auto-Backup einmal pro Verbindung: ConnectionService. hasExpertToolBackedUpThisSession (zurückgesetzt bei disconnect()), ExpertViewModel.ensureSessionBackup() zieht vor dem allerersten Schreibvorgang im Experte-Tab automatisch ein Backup (wie der Wizard), aber nur einmal pro Sitzung, nicht vor jeder einzelnen Änderung (ein Experte-Tab-Durchgang sind oft viele kleine Edits hintereinander).

Vierter Nachtrag — Dauer-Editor: Nutzer fragte, in welcher Einheit die DHCP-Server-Lease-Zeit angegeben wird. Live geprüft: RouterOS- Zeitwerte sind Zahl+Einheit (s/m/h/d/w, kombinierbar wie 1d12h30m), eine bare Zahl gilt als Sekunden (bestätigt: 90 → intern 1m30s). Nutzerwunsch danach: "ein sinnvoll gestaltetes Auswahlfenster ... ein Tooltip wäre hilfreich" statt nur Tooltip-Text. Neuer Feldtyp RouterOSFieldSchema.Kind.duration + DurationFieldEditor (vier Stepper: Tage/Std/Min/Sek, parst bestehende Werte beim Öffnen, schreibt beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server "Lease-Zeit" und Adress-Listen "Ablaufzeit". Nutzer bestätigte danach: "funktioniert".

Stand der Milestones

  • M1M4: Projektgerüst, Connect, Backup, WAN/LAN/DHCP, VLAN — gegen echtes Testgerät verifiziert.
  • M5: WLAN-Schritt — nur "kein WLAN"-Zweig verifiziert, .set-Pfad weiterhin ungetestet (kein WLAN-Chip auf beiden Testgeräten bisher).
  • M6: Firewall-Schritt — live angewendet, dabei Bug 6 gefunden+gefixt; Interface-Auswahl danach erneut bestätigt, Regel-Ergebnis mit korrektem WAN-Port aber noch nicht erneut kontrolliert.
  • Zusatzfeatures nach M6: Tooltips auf allen Konfigurationsfeldern, wählbarer Backup-Ordner (Standard: ~/Library/Application Support/...), Schnell-Backup-Button im Verbinden-Tab, "Werkseinstellungen wiederherstellen" (Gefahrenzone im Sicherungen-Tab, /system reset-configuration no-defaults=no), eigenes App-Icon ("Signal Router"-Motiv).
  • 🔶 M7: Härtung — SSH-Hostkey-TOFU fertig, gegen echte Hardware bestätigt (inkl. neuem "Trennen"-Button im Verbinden-Tab, der dafür nötig wurde). Fehlerzustände/Politur und REST-Schreibpfad-Verifikation gegen ein Gerät mit aktivem www-ssl stehen noch aus.
  • 🔶 M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation (eigene Firewall-Regeln pro LAN/VLAN) — gebaut, nur Unit-Test-verifiziert, noch nicht gegen echte Hardware getestet.
  • 🔶 M9: Einfach/Experte-Modus im Einrichten-Wizard — gebaut, Compile/ Unit-Test-verifiziert. UI (Modusumschalter selbst) noch nicht manuell durchgeklickt — nur M10s Experte-Tab wurde das (siehe M10).
  • M10: Experte-Tab (generischer RouterOS-Zugriff + kuratierte Firewall- und weitere Schemas) — Kern-Logik live gegen Hardware verifiziert, UI (VLAN anlegen/löschen, DHCP-Server mit Adress-Pool zuweisen) vom Nutzer selbst in der laufenden App bestätigt funktionierend — sieben reale Bugs dabei gefunden und gefixt (Bug 713), danach zusätzlich Bestätigungsdialog vor Anlegen/Ändern + einmaliges Auto-Backup pro Sitzung (Angleichung an den Wizard) sowie ein Dauer-Editor (Tage/Std/ Min/Sek statt Freitext) für Lease-/Ablaufzeit-Felder ergänzt, alles vom Nutzer bestätigt. Weiterhin ungetestet: WLAN (kein Chip im Testgerät), Bonding, PPPoE-Client (beide riskant/brauchen echte Zugangsdaten), Schreibzugriff auf /ip address//ip dhcp-client//ip dns/ /ip service (Risiko Selbstaussperrung/ WAN-Ausfall).
  • M11: Übersicht-Tab (Diagramm: Interfaces→IP→DHCP/Pools→Routen→ Firewall/NAT, echte Referenz-Kanten, nach Verbindungsart eingefärbt, Hover/Klick-Hervorhebung) — live gegen Hardware verifiziert, rein lesend (kein apply()), vom Nutzer bestätigt ("das gefällt mir schon sehr gut"). Deckt bewusst nicht ab: VPN/WLAN-Sicherheitsprofile/Queues/ System/Werkzeuge/Mangle/Raw (siehe OverviewGraph.unmappedAreas), weiterhin nur über den Experte-Tab erreichbar.
  • M12: Geräte-Tab (LAN-Scanner: DHCP-Leases+ARP+Bridge-Host, eine Tabelle pro physischem Port, "Feste IP zuweisen" für dynamische Leases) — live gegen Hardware verifiziert und vom Nutzer bestätigt funktionierend ("das funktioniert jetzt super"), nach vier gefundenen und gefixten Bugs (Bug 1417, siehe oben) — die aufwendigste Live-Verifikation aller Milestones bisher, mehrere Diagnose-Runden mit Rohdaten-Vergleichen und direkten Router-Gegenchecks (SSH-CLI + Winbox). Fehlt noch: "Zurück auf dynamisch" (siehe Bekannte Einschränkungen).

Nächste Schritte

  1. Den kompletten Wizard einmal neu durchlaufen (WAN/LAN/VLAN/WLAN/ Firewall) auf dem werksresetteten hEX und dabei das Firewall-Ergebnis mit korrektem WAN-Port per /ip firewall filter print / /ip firewall nat print kontrollieren (bisher nur die Interface- Auswahl nach dem Parser-Fix bestätigt, kein erneuter Apply).
  2. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
  3. Rest von M7: Fehlerzustände/Politur, REST-Schreibtest an einem Gerät mit aktivem www-ssl.
  4. M8 gegen echte Hardware testen: zwei LAN-Interfaces/VLANs anlegen, eins isoliert, anwenden, per /ip firewall filter print kontrollieren, dass die Drop-Regeln greifen und der Rest (Internetzugriff, nicht-isolierte Netzwerke) unangetastet bleibt.
  5. M9 UI (Einfach/Experte-Modusumschalter im Einrichten-Tab selbst) noch manuell durchklicken — M10s Experte-Tab wurde bereits vom Nutzer bestätigt (siehe oben), der Moduswechsel im Wizard noch nicht.
  6. M10: WLAN-Schemas (an Gerät mit WLAN-Chip), Bonding, PPPoE-Client (mit echten oder Test-ISP-Zugangsdaten) noch gegen Hardware verifizieren.
  7. dhcp5/pool5 auf ether5 sind Test-Überbleibsel aus einer früheren Session — mit dem Nutzer klären, ob die noch gebraucht werden oder aufgeräumt werden können (aktuell blockieren sie ether5 für einen neuen direkt-auf-dem-Port angelegten DHCP-Server).
  8. Nicht anrühren: test-vlan (VLAN-ID 20, ether5), testpool (192.168.90.10-250) und testdhcp (DHCP-Server auf test-vlan) sind echte, vom Nutzer selbst über die App angelegte Konfiguration aus dieser Session, keine Test-Artefakte zum Aufräumen.
  9. Optional, kleinere Politur: den neuen Dauer-Editor (RouterOSFieldSchema.Kind.duration) auch auf weitere Zeitwert-Felder anwenden, die bisher nur Text mit Beispiel-Tooltip sind — WireGuard- Peer "Keepalive", Scheduler "Intervall", Netwatch "Prüf-Intervall".
  10. Backup-Wiederherstellung bauen — vom Nutzer explizit gewünscht, noch nicht begonnen. Vermutlich: BackupService um eine restoreBackup(_:for:)-Funktion erweitern, die den Inhalt eines gewählten .rsc-Backups über SSH auf den Router einspielt (RouterOS kann .rsc-Skripte per /import file-name=... ausführen, dazu muss die Datei vorher aufs Gerät hochgeladen werden — Weg noch nicht verifiziert, vor dem Bauen live prüfen statt zu raten), plus ein "Wiederherstellen"-Button pro Eintrag im Sicherungen-Tab (BackupListView) mit Bestätigungsdialog (ähnlich der bestehenden Werksreset-Gefahrenzone).
  11. Geräte-Tab: "Zurück auf dynamisch"-Aktion bauen — Gegenstück zu "Feste IP zuweisen". RouterOS-Weg dafür noch nicht recherchiert/live geprüft (vermutlich remove der Lease und Neuanlage, oder ein eigenes Menü-Kommando wie bei make-static — vor dem Bauen klären, nicht raten, siehe Bug 17 als Warnung).
  12. .id-Positions-Überlagerung (fetchMenuItems) auf weitere Menüs prüfen — live als falsch bestätigt für /ip dhcp-server lease (Bug 15). Betrifft potenziell jedes .set/.remove im Experte-Tab. Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig vorhandenen Einträgen :put [<Pfad> find] und <Pfad> print terse unabhängig ausführen und die Reihenfolgen von Hand vergleichen.
  13. Optional: den neuen Dauer-Editor auch für den Geräte-Tab nutzen, falls dort künftig Zeitfelder (z.B. eine Lease-Zeit-Anzeige) dazukommen.
  14. Kay-Uwes-iMac (ether2, defconf) und der Test-Laptop DEDELLB2M6GK3 (ether3, dhcp5/defconf je nach Testrunde) sind echte Geräte des Nutzers, mit denen M12 live durchgetestet wurde — keine Test-Artefakte, nicht aufräumen. Der Laptop hatte am Ende der Session testweise eine feste IP zugewiesen bekommen; ob das der gewünschte Dauerzustand ist oder nur zum Testen war, mit dem Nutzer klären.

Kein Gitea-Remote vorhanden — falls der Nutzer später eine Gitea-Instanz aufsetzt (z.B. selbst gehostet auf der vorhandenen OMV-NAS, siehe Aperto/-Projekt), Remote hinzufügen und pushen; bis dahin lokales Git.

/Applications/RouterOS Assistant.app ist der aktuell installierte Release-Build, auf dem Stand des jeweils letzten Commits auf main (App-Icon "Signal Router"). Nach jeder weiteren Code-Änderung muss dieser Build-und-Kopier-Schritt (siehe oben) wiederholt werden, damit die installierte App aktuell bleibt.