M26: SavedRouter bekommt ein optionales serialNumber-Feld. Zwei physisch verschiedene Router mit identischem Host+Benutzername (Werks-Adresse) blieben bisher ein gemeinsamer "Bekannte Router"- Eintrag; recordSuccessfulConnection matcht jetzt zusätzlich nach Seriennummer, mit sauberer Migration bestehender Einträge ohne Seriennummer. Drei neue Tests. M27, beim Live-Test von M26 gefunden: - Passwort-Anzeige-Button (Augen-Symbol) im Verbinden-Tab - Bug 35: KeychainService speicherte Passwörter nur nach "username@host" — beide Router teilten sich denselben Schlüsselbund-Eintrag trotz getrennter SavedRouter-Einträge. Fix: optionaler serialNumber-Parameter qualifiziert den Account-Key, mit Fallback auf den alten Key für bereits gespeicherte Passwörter. - Bug 36: neuer Router zeigte keine Routerboard-Infos/Seriennummer — derselbe Root Cause wie der zuvor gemeldete "Jetzt prüfen"-Fehler: /system routerboard und /system package update scheitern auf diesem Router mit "expected end of command" statt dem bisher einzig abgefangenen "bad parameter terse". SSHTransport.fetchMenuItems erkennt jetzt beide Formulierungen. Alles live bestätigt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
117 KiB
RouterOS Assistant — Handoff
Stand: App-Icon fertig, alle Kern-Features (M1–M6) gebaut und mindestens
teilweise gegen zwei echte physische Mikrotik-Testgeräte verifiziert
(ein generisches Gerät für M1–M4, 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 7–13). Alles bis hierhin committed
(c9ecd3a).
Neu in dieser Session: drei weitere Tabs/Features. 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) sowie ihr
Gegenstück "Feste Zuweisung entfernen" (RouterOS kennt kein
make-dynamic, nur remove — vorher per Recherche-Agent gegen die
offizielle Doku geprüft statt geraten). M13 Backup-Wiederherstellung
im Sicherungen-Tab: .rsc-Backup per SFTP hochladen, dann RouterOS'
dokumentierter reset-configuration+run-after-reset-Restore-Weg, mit
Modell-Abgleich vor dem Bestätigungsdialog (Brick-Schutz bei falschem
Routermodell) und automatischem Login-Erhalt (siehe Bug 18). Alle drei
Tabs/Features live gegen den hEX-Testrouter verifiziert, dabei M12 (vier
Bugs, 14–17) und M13 (ein besonders ernster Bug, 18: erster Testlauf
sperrte den Router komplett aus, nur per Hardware-Reset behebbar) beide
zäher als erwartet, am Ende aber jeweils vom Nutzer selbst bestätigt
("das funktioniert jetzt super" / "das hat sofort funktioniert" / "gut,
das hat funktioniert").
Danach, gleiche Session, M14 "Update-Check": Verbinden-Tab zeigt jetzt
volle Routerboard-Daten (Modell/Revision/Seriennummer/Firmware, aus
/system routerboard) und einen RouterOS-Software-Update-Check
(/system package update: Kanal/installierte/neueste Version/Status,
"Jetzt prüfen", "Update installieren"), plus "Firmware aktualisieren"
für die Routerboard-Bootloader-Firmware und einen "Jetzt neu starten"-
Button danach — alle vier Aktionen live bestätigt, inkl. der zuvor
unsicheren Frage, ob /system routerboard upgrades normalerweise
interaktive Bestätigung den nicht-interaktiven SSH-Weg dieser App
blockiert (tut es nicht, lief sauber durch). Dabei ein weiterer Bug im
generischen Lese-Pfad gefunden (Bug 20, siehe unten). Anschließend ein
Design-Durchgang: Verbinden-Detailseite/Sicherungen/Geräte liefen auf
nackten Lists ohne Rahmen — umgestellt auf Form+.formStyle(.grouped),
denselben nativen macOS-"Karten"-Look, den Wizard und Experte-Tab schon
die ganze Zeit hatten. Dark Mode auf Nachfrage geprüft: funktioniert ohne
Codeänderung (keine .preferredColorScheme-Überschreibung, keine feste
Farbwerte, alles adaptive System-Farben) — vom Nutzer live bestätigt.
Release-Build unter /Applications/RouterOS Assistant.app ist auf
aktuellem Stand.
Danach, neue Session: M8 komplett live verifiziert — erst manuell,
dann über den Wizard selbst. Bug 21 aus der Vorsession (WAN-DHCP-
Client/PPPoE-Apply nicht idempotent) blieb bestätigt gefixt. Isolation
zuerst manuell per SSH an ether5 nachgebaut (eigenes Subnetz, eigener
DHCP-Server, /interface list-basierte Forward-Drop-Regel) — dabei drei
reine Konfigurationsfehler der Reihe nach gefunden und behoben (Adresse
versehentlich als /32 bzw. .0-Netzadresse statt Host-Adresse
eingetragen; ether5 fehlte in der defconf-Interface-Liste "LAN",
wodurch DNS-Anfragen an den Router selbst blockiert wurden, obwohl
DHCP/Routing/Internet normal liefen). Isolation danach beidseitig
bestätigt (iMac ↔ Laptop erreichen sich nicht, Internet für beide
weiterhin ja).
Anschließend derselbe Test über den App-Wizard (Experte-Modus,
ether4) wiederholt, um die Abnahme-Bedingung aus der Vorsession
("kompletter Wizard-Durchlauf") tatsächlich zu erfüllen — dabei drei
echte App-Bugs gefunden und gefixt, alle auf denselben "Wizard wurde nie
gegen einen bereits konfigurierten Router bzw. mit bereits gebrückten
Ports erneut ausgeführt" blinden Fleck zurückzuführen:
- Bug 22:
DhcpServerCommandBuilderfügte ein neues LAN-/VLAN- Interface nie der defconf-Interface-Liste "LAN" hinzu — derselbe DNS-Bug wie oben, jetzt als App-Bug bestätigt. Fix: automatisch/interface list add name=LAN+/interface list member add list=LAN interface=<name>, beide toleriert falls schon vorhanden. - Bug 23: ein voller Wizard-Durchlauf gegen einen bereits
konfigurierten Router brach beim zweiten Befehl ab
(
/ip address add address=192.168.88.1/24 interface=bridge— "already have such address"), weil nur WAN-DHCP-Client/PPPoE idempotent behandelt wurden (Bug 21). Fix:SetupViewModel. applyIdempotentlyerweitert —/ip pool//ip dhcp-server//ip dhcp-server networkwerden bei Duplikat jetzt als.set(gematcht auf name/name/address) erneut versucht,/ip addressbei exaktem Duplikat als bereits erledigt behandelt (ein Interface darf mehrere Adressen halten, ein.setnach "interface" träfe sonst potenziell die falsche). - Bug 24: LAN-/VLAN-Schritt löste ein gewähltes physisches Interface
nie aus einer bestehenden Bridge — auf Werks-Routern sind ether2–5 ab
Werk gebridged. Als eigenes isoliertes Netz konfiguriert blieb das
Interface Bridge-"Slave", RouterOS verwarf die generierten
Isolationsregeln selbst als ungültig ("in/out-interface matcher not
possible when interface is slave - use master instead"), live an
ether4bestätigt. Fix:DhcpServerCommandBuilderstellt jetzt/interface bridge port remove [find interface=<name>]voran (übersprungen für "bridge" selbst), toleriert als bereits erledigt falls nie gebridged.
Nach allen drei Fixes lief der komplette App-Wizard-Durchlauf für
ether4 fehlerfrei durch (inkl. Neuanwendung der bereits vorhandenen
bridge/ether1-Konfiguration), Isolation + DNS + Internet vom Nutzer
am echten Gerät bestätigt. Alle 52 Unit-Tests grün. M8 damit als
einziger Milestone bisher sowohl manuell als auch über die App selbst
live verifiziert.
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: Remote origin zeigt auf eine selbst gehostete Gitea-Instanz
(git@192.168.178.222:kay/RouterOS.git).
.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), uploadScript (SFTP, M13), applyRestoreScript (reset-configuration+run-after-reset, M13)
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, restoreBackup (M13: SFTP-Upload + reset-configuration+run-after-reset + Login-Erhalt), backupModel (Modell-Header-Parsing)
FactoryResetService.swift — /system reset-configuration über eigene SSH-Verbindung ("Gefahrenzone")
KeychainService.swift — Passwort-Speicherung
UpdateService.swift — M14: RouterOS-Software-Update (/system package update check-for-updates/install) + Routerboard-Firmware (upgrade) + reboot, eigene SSH-Verbindung
Features/
Wizard/Steps/Connect/ — Verbinden-Tab (inkl. Schnell-Backup-Button, Routerboard-Info, Software-/Firmware-Update-Check (M14), 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/confirmStaticRemoval)
Backup/ — Sicherungen-Tab (Ordner wählen, Gefahrenzone: Werkseinstellungen wiederherstellen)
Resources/Assets.xcassets/AppIcon.appiconset/ — App-Icon "Signal Router" (16px–1024px)
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)
-
ConnectView zeigte nie Verbindungsstatus —
connectionServicewar nur überviewModel.connectionServiceerreichbar (zwei Ebenen tief verschachteltes ObservableObject), SwiftUI abonniert das nicht automatisch. Fix:ConnectViewhältconnectionServicezusätzlich selbst als@ObservedObject. -
NIOSSHError.localizedDescriptionnutzlos — bridged auf generisches NSError ("error 1"). Fix:String(describing:). -
SSH scheiterte mit
keyExchangeNegotiationFailure— RouterOS bietet nur Legacy-Algorithmen an, Citadel-Default enthält sie nicht. Fix:algorithms: .all. -
Citadel.SSHClient.CommandFailedzeigte nur den Exit-Code —executeCommand()verwirft die gesammelte Ausgabe bei Exit-Code ≠ 0. Fix: eigene Sammlung überexecuteCommandStream(). -
Picker-Warnungen durch Platzhalter-Interfacenamen —
prepareDefaults()korrigiert jetzt WAN und LAN, läuft schon iminit. -
Kritisch: CLI-Parser verlor 6 von 7 Interfaces auf einem hEX-Router —
parseInterfaces/parseDeviceInfosplitteten 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 auflostatt 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 inSetupViewjetzt grundsätzlich aus allen WAN/LAN/VLAN- Auswahllisten gefiltert. Regressionstest mit echten\r\n-getrennten hEX-Daten inRouterOSCliParserTests. -
/ip service: Feld heißtavailable-from, nichtaddress— beim Kuratieren des Experte-Schemas geraten statt geprüft. Live gegen den hEX bestätigt:/ip service printzeigt tatsächlichavailable-from=; einset ... address=...hätte den Zugriffsfilter stillschweigend nicht gesetzt. Schema korrigiert (RouterOSSchemaCatalog.swift). -
RouterOS 7.24.2 zeigt
.idgar nicht inprint terse, und Singleton-Menüs verweigerntersekomplett — Annahme beim Bau des generischen Experte-Motors (M10) war,.id=*Nsei Teil jederprint 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) lehnentersesogar 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 vonprint terseentspricht, damit per Position zuordenbar. Für Singleton-Menüs neuerRouterOSMenuSchema.isSingleton- Schalter: Fetch fällt auf normalesprint(Doppelpunkt-Format,parseSingletonItem) zurück,.setläuft ohne[find ...]direkt (matchField: ""als Sentinel inRouterOSCommand/beiden Transports). Beides live gegen/system identitydurchgespielt (Name testweise geändert, Fehlertext exakt geprüft, zurückgesetzt). -
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
.textstatt als Auswahl gebaut. Fix: neuer FeldtypRouterOSFieldSchema.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. -
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ültigeraction=-Wert lieferte"syntax error (...)", ebenfalls Exit-Status 0 (perexpect-Skript mitcatch wait resultgeprü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. -
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; mitdisabled=nolief es durch. RouterOS' CLI akzeptiert nuryes/nofür Bool-Parameter, nichttrue/false. Betraf praktisch jedes kuratierte Schema (23×"false", 4×"true"alsdefaultValue). Fix zweigleisig:ExpertViewModel.pendingCommandnormalisiert jeden.bool-Feldwert vor dem Senden aufyes/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 mitdisabled=no). -
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. -
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:.interfacePicklas ausConnectionService.interfaces, nur einmal beifinishConnecting()befüllt, nie danach aktualisiert — anders als die neueren.menuItemPick-Felder (Pool/Profil/Skript), die bei jedem Menü-Öffnen frisch laden. Fix: neuesExpertViewModel.liveInterfaceNames, per/interface print tersebei jedemreloadItems()aktualisiert;availableInterfaces-Parameter- Durchreichung (ExpertView→ExpertMenuDetailView→ExpertItemEditView) 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. -
/ip dhcp-server leasegibt dasdynamic-Feld überprint tersenie 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=noerscheint 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 interseüberhaupt nie auf, unabhängig vom Status — nur/ip dhcp-server lease print(ohne terse) zeigt es als Flag-BuchstabeD. 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). -
fetchMenuItemss.id-Positions-Überlagerung ordnete bei/ip dhcp-server leasedie falsche.idder falschen Zeile zu — zwei Leases (iMac dynamisch, Laptop nach Umstellung fest), aber die App zeigte beide als "Fest". Ursache:fetchMenuItemsliest die.ids über einen separaten:put [<Pfad> find]-Aufruf und ordnet sie nach Position denprint 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.idlandete auf der iMac-Zeile. Fix ausschließlich im Geräte-Tab: Erkennung und dermake-static-Zielauswahl laufen jetzt komplett über die MAC-Adresse (steht direkt in derselbenterse-Zeile, kein Cross-Command-Abgleich nötig) statt über.id. Nicht behoben: derselbe.id-Überlagerungsmechanismus wird auch vom Experte-Tab für.set/.removeauf beliebigen Menüs benutzt — ob das dort ebenfalls (menü- oder situationsabhängig) falsch zuordnen kann, ist ungeprüft. Siehe Nächste Schritte. -
SwiftUI-
.confirmationDialoglö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:isPresentedwar eine berechneteBinding, deren Setter bei jedem Schließen (auch durch den bestätigenden Knopf selbst — SwiftUI ruft den Setter mitfalsebei jedem Tap in einem.confirmationDialog, unabhängig vom gedrückten Knopf)pendingStaticAssignment = nilsetzte — also genau das Objekt, dasconfirmStaticAssignment()Sekundenbruchteile später lesen wollte. Fix: Dialog-Sichtbarkeit (showStaticConfirmation, ein einfaches@State Bool) und Ziel- Objekt (pendingStaticAssignment) komplett entkoppelt — genau das Muster, dasBackupListViews Werksreset-Dialog schon die ganze Zeit korrekt verwendet (eigenes@State-Bool statt abgeleitetem Binding). -
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:getakzeptiert einefind-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 — nacktesfindmit einer Bedingung, plusget <id> feldauf eine einzelne bekannte.id, verbunden über eine:foreach-Schleife (RouterOSTransport.fetchFieldValues/SSHTransport.fetchFieldValues). -
Kritisch, live so passiert: Backup-Wiederherstellung sperrte den Login komplett aus, nur ein Hardware-Reset half noch — beim ersten Live-Test der neuen Restore-Funktion (M13). Ursache doppelt: RouterOS'
/exportkann Benutzerpasswörter grundsätzlich nie exportieren (offiziell dokumentiert), und der für den Restore genutzteno-defaults=yes-Reset löscht zusätzlich den mitgelieferten Standard-Admin-Account komplett — nach dem Restore existierte also weder aus der Sicherung noch als Werksvorgabe ein funktionierender Login. Fix:BackupService.restoreBackupstellt dem hochgeladenen Skript jetzt einen kleinen, selbst generierten Block voran (nicht angehängt — RouterOS bricht den Import beim ersten Fehler ab, ein Login-Erhalt am Ende hätte also nie gegriffen, falls irgendwo vorher im eigentlichen Sicherungsskript ein Fehler auftritt), der exakt das aktuell in der App verwendete Login per:if ([/user find name=...] = "") do={add ...} else={set ... password=...}neu anlegt bzw. das Passwort neu setzt — das einzige Passwort, das die App zuverlässig kennt. Beim erneuten Live-Test danach vom Nutzer bestätigt ("das hat funktioniert"). -
SwiftUI-Typprüfung bricht bei zu langen Modifier-Ketten ab, kein Logikfehler —
BackupListViewwarf nach dem Hinzufügen weiterer.confirmationDialog/.alert-Modifiere für den Restore-Dialog"the compiler is unable to type-check this expression in reasonable time", mit einer irreführenden Zeilenangabe (zeigte auf einen ganz gewöhnlichen, bereits woanders im selben File funktionierendenBinding-Ausdruck). Fix: die Restore-spezifischen Dialoge/Alerts in eine eigene@ViewBuilder-Computed-Property ausgelagert, statt alles in einer einzigenbody-Kette zu verketten — teilt die Typinferenz-Last auf zwei kleinere Ausdrücke auf. Bei künftigen Views mit vielen.alert/.confirmationDialog/.sheet-Modifiern in Folge vorbeugend gleich aufteilen, statt erst bei diesem Fehler zu reagieren. -
fetchMenuItemss Singleton-Fallback (Bug 8/M10) griff für/system routerboardnicht — genau dasselbe Bug-10-Muster, nur an einer Lesestelle statt beim Schreiben. Die neue Routerboard-Sektion (M14) blieb leer, kein Fehler, nichts. Ursache: der Fallback reagiert bisher nur auf eine echte Exception ("bad parameter terse" via CitadelsCommandFailed), aber/system routerboard print terseliefert diesen Fehlertext live bestätigt mit Exit-Code 0 zurück — der Fehlertext landete also als normaler (unparsbarer) Output,fetchMenuItemsgab still[]zurück statt in den Singleton-Zweig zu fallen. Fix: zusätzlich den zurückgegebenen Output-Text selbst auf "bad parameter terse" prüfen, nicht nur die Exception — deckt beide Varianten ab, wie RouterOS diesen Fehler je nach Menü meldet. Betrifft potenziell jedes Singleton-Menü, nicht nur/system routerboard— der generische Lese-Pfad wird von Übersicht-, Geräte- und Experte-Tab gemeinsam genutzt. -
Wizard-WAN-Apply war nicht idempotent — Wizard erneut auf einem Interface durchlaufen, das schon einen DHCP-Client hat (hier: aus einer zurückgespielten Sicherung), scheiterte live mit "failure: dhcp-client on that interface already exists".
WanConfig. buildCommands()erzeugt für DHCP-Client/PPPoE immer.add, ohne vorher zu prüfen, ob auf dem Interface schon einer existiert. Fix:SetupViewModel.applyIdempotentlyfängt einen.add-Fehlschlag auf/ip dhcp-client//interface pppoe-clientab und wiederholt ihn als.set(nach "interface" gematcht) — bewusst nur für diese zwei Menüs mit "maximal ein Eintrag pro Interface"-Semantik, nicht generell für jedes.add(z.B./ip addresserlaubt legitim mehrere Adressen pro Interface). Noch nicht erneut live getestet, siehe Nächste Schritte.
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. RouterOS-Backups über /export enthalten
grundsätzlich nie Passwörter, und no-defaults=yes löscht auch den
Werks-Admin-Account (Bug 18) — jede Restore-Funktion, die einen
kompletten Reset vor dem Re-Import macht, muss den Login selbst wieder
herstellen, sonst folgt ein kompletter Aussperr-Vorfall (nur per
Hardware-Reset behebbar, live so passiert). Login-Erhalt gehört an den
Anfang des Skripts, nicht ans Ende — RouterOS bricht den Import beim
ersten Fehler irgendwo im Skript komplett ab. SwiftUI-Typprüfung kann
bei langen Modifier-Ketten mit einer irreführenden Fehlerzeile
timeouten (Bug 19) — kein Hinweis auf einen echten Logikfehler an der
genannten Stelle; bei vielen .alert/.confirmationDialog in Folge
vorbeugend in eigene @ViewBuilder-Properties aufteilen. "RouterOS'
Exit-Code ist kein verlässliches Erfolgssignal" (Bug 10) gilt auch für
Lesebefehle, nicht nur fürs Schreiben (Bug 20) — derselbe
Singleton-Fallback, der für ein Menü per Exception ausgelöst wurde
(Bug 8), blieb für ein anderes Menü aus, weil RouterOS denselben
Fehlertext dort mit Exit-Code 0 zurückgibt; robuste Erkennung muss immer
auch den Output-Text selbst prüfen, nie nur auf eine geworfene Exception
vertrauen. Wizard-Apply nutzte für WAN-DHCP-Client/PPPoE immer .add
(Bug 21) — scheiterte live mit "failure: dhcp-client on that interface
already exists" auf einem Interface, das (z.B. aus einer zurückgespielten
Sicherung) schon einen Client hatte; SetupViewModel.applyIdempotently
versucht seitdem bei .add-Fehlschlag auf diesen beiden Menüs
automatisch .set (nach "interface" gematcht). Ein neu eingerichtetes
LAN-/VLAN-Interface wurde nie der defconf-Interface-Liste "LAN"
hinzugefügt (Bug 22) — auf Routern mit Werks-Firewall
(chain=input action=drop in-interface-list=!LAN) blieb dadurch jede
Anfrage an den Router selbst (DNS, Winbox) vom neuen Netz aus blockiert,
obwohl DHCP/Routing/Internet normal liefen (reine Forward-Chain-Sache,
unberührt) — live an einer manuell nachgebauten Isolation gefunden
(Lease + Default-Route vorhanden, jede DNS-Anfrage lief trotzdem in
Timeout). Voller Wizard-Durchlauf gegen einen bereits konfigurierten
Router brach an jedem nicht-idempotenten .add ab (Bug 23) — nur
WAN-DHCP-Client/PPPoE waren idempotent (Bug 21); /ip address
(exaktes Duplikat = bereits erledigt) sowie /ip pool//ip dhcp-server//ip dhcp-server network (Retry als .set, gematcht auf
name/name/address) kamen dazu. Ein als eigenes isoliertes Netz
konfiguriertes Interface blieb Bridge-"Slave" (Bug 24) — auf
Werks-Routern sind ether2–5 ab Werk gebridged; ohne explizites Lösen aus
der Bridge verwarf RouterOS die generierten Forward-Isolationsregeln
selbst als ungültig ("in/out-interface matcher not possible when
interface is slave - use master instead"), live an ether4 bestätigt;
DhcpServerCommandBuilder stellt seitdem ein /interface bridge port remove [find interface=<name>] voran (übersprungen für "bridge"
selbst). Ein geleertes Textfeld (z.B. Kommentar) blieb nach dem
Speichern unverändert bestehen (Bug 25) — pendingCommand filterte
alle leeren formValues grundsätzlich aus den Argumenten heraus, RouterOS'
set ändert aber nur explizit übergebene Parameter, ein weggelassener
bleibt unangetastet statt geleert. Fix: ein Feld bleibt im Argument-Set,
wenn es vorher einen Wert hatte (jetzt als feld="" explizit geleert);
zusätzlich musste RouterOSCommands SSH-Zeilen-Rendering einen leeren
Wert als "" statt als nacktes feld= ausgeben. Bearbeiten einer
Route (z.B. nur den Kommentar ändern) scheiterte mit "bad parameter
immediate-gw" (Bug 26) — RouterOS liefert bei /ip route print u.a.
das berechnete, nur lesbare Feld immediate-gw mit; da dieses Feld im
Route-Schema nicht kuratiert ist, landete es unverändert in den freien
"Weitere Parameter" und wurde bei jedem Speichern blind mitgeschickt —
RouterOS lehnt immediate-gw beim set als ungültigen Parameter ab und
bricht dadurch die komplette Änderung ab, auch wenn nur ein unrelated
Feld wie der Kommentar geändert wurde. Fix: ein unkuratiertes Feld wird
nur noch mitgeschickt, wenn sein Wert sich gegenüber dem ursprünglich
geladenen Item tatsächlich geändert hat (oder neu hinzugefügt wurde) —
betrifft nicht nur Routen, sondern jedes Menü mit berechneten/nur
lesbaren Feldern im generischen Lese-Pfad. Direkt beim Nachtesten
derselbe Effekt nochmal, diesmal bei einem kuratierten Feld (Bug 27) —
eine dynamische/verbundene Route (automatisch angelegt durch eine
IP-Adresse auf dem Interface) hat distance=0; das unveränderte
Zurücksenden dieses ganz normal kuratierten Feldes beim Ändern des
Kommentars scheiterte mit "value of distance out of range (1...255)" —
RouterOS akzeptiert distance=0 nur für System-verwaltete Routen, nicht
als Eingabewert für set. Zeigt: das "nur bei Änderung senden"-Prinzip
aus Bug 26 galt bisher nur für unkuratierte Felder, kuratierte Felder
wurden weiterhin immer komplett neu gesendet. Fix generalisiert:
pendingCommand sendet jetzt für jedes Feld (kuratiert wie
unkuratiert) beim Bearbeiten eines bestehenden Items nur noch den
Diff gegen das ursprünglich geladene Item — deckt implizit auch Bug 25
(Leeren eines Feldes) mit ab, der Spezialfall dafür wurde entfernt.
Nebenbei: die neue ExpertViewModelTests.swift lief anfangs gar nicht
mit, weil xcodegen generate nach dem Anlegen der Datei nicht erneut
lief — Xcodegen erzeugt die sources-Dateiliste einmalig beim
Generieren, eine neu angelegte Datei taucht im generierten .xcodeproj
nicht automatisch auf. Merksatz: nach jeder neu angelegten Datei
xcodegen generate erneut laufen lassen, nicht nur nach Datei-Edits.
Auch nach dem Bug-27-Fix scheiterte dieselbe Route noch, jetzt mit
"no such item (4)" (Bug 28) — der /ip route print detail-Gegencheck
bestätigte: die Route ist dynamisch (D-Flag, distance=0, automatisch
angelegt durch die IP-Adresse auf bridge). RouterOS' eigenes
"dynamic"-Flag steht nicht zuverlässig in print terse (dasselbe
Problem schon bei DHCP-Leases dokumentiert, siehe LanDevice.swift),
aber distance=0 ist ein verlässliches Signal — keine echte statische
Route kann das haben (RouterOS erzwingt 1–255 bei set). Fix:
Übersicht-Tab bietet für Routen mit distance=0 gar kein "Bearbeiten"
mehr an (editTarget=nil) — die .id einer dynamischen Route ist
ohnehin nicht stabil, RouterOS kann sie jederzeit neu anlegen. 59
Unit-Tests grün (neuer Test OverviewGraphTests. testDynamicRouteHasNoEditTarget). Übersicht-Tab aktualisierte sich
nicht selbst nach Tab-Wechsel (Bug 29) — .task lief nur einmal pro
View-Lebenszeit; da SwiftUI Tab-Inhalte beim Wechsel auf macOS nicht neu
erzeugt (die View bleibt am Leben), feuerte .task beim Zurückwechseln
nie erneut. Fix: .onAppear statt .task (feuert bei jedem
Sichtbarwerden des Tabs neu) — Änderungen aus dem Experte-Tab (z.B. eine
neu angelegte Route) erscheinen jetzt automatisch beim Reinschauen, ohne
manuellen "Aktualisieren"-Klick.
Verbindungs-Label "Optionen" im Detailpanel unklar, Zeile nicht
anklickbar — Nutzerfrage nach Klick auf eine IP-Adresse: was bedeutet
"Optionen" bei der verknüpften DHCP-Netzwerk-Zeile? Umbenannt zu
"DHCP-Optionen"; die "Verbindungen"-Zeilen im Detailpanel (NodeDetailView)
sind jetzt anklickbar (springt zum verbundenen Knoten, wie schon bei
Linien-Klicks) und zeigen beim Hovern einen Tooltip mit der
Klartext-Erklärung (OverviewStyle.explanation(for:), bereits für
EdgeDetailView gebaut, hier wiederverwendet).
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)
- Restore nur für dasselbe Routermodell, RouterOS erzwingt das selbst
nicht — die App blockt bei erkanntem Modell-Mismatch (per Recherche
bestätigt: RouterOS importiert eine Fremd-Modell-Config anstandslos,
potenziell mit falsch zugeordneten Interfaces). Schutz greift nur, wenn
die Backup-Datei die
# model = ...-Kopfzeile hat (RouterOS ≥ mind. 6.49.5) — bei sehr alten/handbearbeiteten Dateien ohne diese Zeile läuft der Restore ungeprüft durch (Dialog weist explizit auf "unbekannt" hin, blockt aber nicht). - Restore-Skript-Datei bleibt nach der Wiederherstellung auf dem
Router liegen (
flash/routerosassistant-restore.rsc) — kein automatisches Aufräumen, weil der Router direkt im Zuge des Befehls neu startet und die Verbindung dabei abbricht; ein erneutes Verbinden nur zum Löschen dieser einen Datei erschien nicht aufwandsgerecht. Harmlos (überschreibt sich beim nächsten Restore selbst), aber bewusst in Kauf genommen, nicht vergessen. REST-Pfad ungetestet für Schreibvorgänge— am 2026-09-16 erstmals live getestet:www-sslauf dem hEX-Testrouter aktiviert (eigene CAlocal-ca+ davon signiertesrest-certangelegt, da direktes Selbstsignieren ohne separate CA mit "failure: CA not found" scheiterte — RouterOS braucht dafürkey-usage=key-cert-sign,crl-signauf einem eigenen CA-Zertifikat, das perca=<name>referenziert wird). Dabei drei echte, vorher unentdeckte Bugs gefunden und gefixt, alle drei ausschließlich möglich, weil kein Testgerät vorher jewww-sslan hatte:- Bug 30:
RestTransport.baseURLfehlte der abschließende/(".../rest"statt".../rest/").URL(string:relativeTo:)ersetzt bei relativer Auflösung das letzte Pfadsegment der Basis (RFC 3986 §5.3) — ohne trailing slash ist "rest" selbst das letzte Segment, ein Pfad wie "system/resource" ersetzt es also statt danach angehängt zu werden (.../rest+ "system/resource" →.../system/resource, 404). Jede REST-Verbindung scheiterte dadurch mit stillschweigend verschlucktem "HTTP 404" (der generischecatch-Zweig inConnectionService.connectloggte den Fehler nirgends), fiel auf SSH zurück, ohne je den Zertifikats-Dialog zu zeigen — sah aus wie "kein Zertifikat-Prompt trotz neuem Zertifikat", war tatsächlich ein komplett anderer, tieferliegender Bug. Gefunden über einen temporären Debug-printim Fallback-Pfad. - Bug 31:
RestTransport.apply()s.add-Fall nutztePOST, RouterOS' REST-API erwartet dafür aberPUT—POSTauf/rest/ip/firewall/address-listlieferte{"detail":"no such command","error":400,"message":"Bad Request"},PUTmit identischem Body201mit vollem Echo des neuen Items. Nebenbeisend()s Fehlerbehandlung verbessert: zeigte bisher nur den nackten Statuscode ("HTTP 400"), jetzt wird RouterOS' eigener JSON-Fehler- Body (detail-Feld) mit ausgegeben, falls vorhanden — genau dieser Body war es, der den Bug überhaupt auffindbar machte (per direktemcurl-Replay derselben Anfrage). - Bug 32, am folgenreichsten: RouterOS sendet auf jede
/rest/...-AntwortCache-Control: max-age=31536000(ein Jahr) + passendenExpires-Header — vermutlich ein Blanket-Default für statische Web-UI-Assets, der auch auf die REST-API durchschlägt.URLSession(configuration: .ephemeral)bedeutet nur "nichts auf Platte persistieren", hält aber weiterhin einen Im-Speicher-URLCacheund hält sich an diese Header — die erste GET-Antwort auf einen Pfad (z.B. eine leere Artikelliste, abgerufen durchloadCrossReferenceOptionsoder ein früheres Öffnen desselben Menüs) wurde für den Rest der App-Laufzeit unverändert zurückgegeben, komplett unabhängig von späteren.add/.set/.remove-Änderungen. Live-Symptom: ein frisch angelegter Adress-Listen-Eintrag (percurlals real bestätigt) erschien nie in der Experte-Tab-Liste, auch nach Weg-und-zurück-Navigation nicht. Fix:urlCache = nil+.reloadIgnoringLocalCacheDataauf Session- und Request-Ebene (RouterOS' Schreib-Endpunkte sind nicht idempotent genug, um sich nur auf eine der beiden Ebenen zu verlassen). Nach allen drei Fixes live durchgespielt: Anlegen (PUT), Bearbeiten inkl. Feld-Leeren (PATCH, Kommentar gesetzt dann wieder gelöscht — REST-seitig direkt percurlverifiziert, dass ein leerer String das Feld tatsächlich löscht, genau wie bei SSH/Bug 25), Löschen (DELETE) — alle drei vom Nutzer bestätigt ("funktioniert"). Damit ist der REST-Transport erstmals end-to-end gegen echte Hardware verifiziert, nicht mehr nur gegen Mocks. Der REST-Zweig vonfetchFieldValues(M12/Bug 14, Query-String-Filter?feld=wertauf einem GET) bleibt weiterhin unverifiziert — dabei nicht auf den Weg gekreuzt. - Bug 33, direkt danach gefunden: Löschen eines Eintrags im
Experte-Tab aktualisierte die sichtbare Liste nicht, bis der Nutzer
in einen anderen Tab und zurück wechselte — sah zunächst wieder wie
ein Cache-/Refresh-Problem aus, war es aber nicht: ein temporärer
Debug-
printinconfirmRemovalbestätigteitems.countgeht korrekt von 1 auf 0 nachreloadItems(), das@Published-Modell war also die ganze Zeit richtig. Reines SwiftUI-Render-Problem: Anlegen/Bearbeiten laufen überExpertItemEditViewals.sheet(item:), dessen SchließenExpertMenuDetailViewautomatisch neu mountet — genau das erzwingt implizit den Refresh, den Löschen (über ein direkt anExpertMenuDetailViewgehängtes.confirmationDialog, kein Sheet, kein Remount) nie bekam. Fix:.id(viewModel.items.count)auf der Einträge-Section, zwingt SwiftUI bei jeder Mengenänderung zu einer frischen View-Identität statt sich auf In-Place-Diffing zu verlassen. Live bestätigt ("funktioniert").
- Bug 30:
fetchMenuItemss.id-Positions-Überlagerung: Verlässlichkeit für andere Menüs ungeprüft (siehe Bug 15) — bei/ip dhcp-server leaselive als falsch bestätigt (.idlandete 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/.removeauf 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. Nachtrag 2026-09-16: live an/ip address(3 Einträge),/ip route(4 Einträge) und erneut/ip dhcp-server lease(2 Einträge) geprüft (:put [<Pfad> find]gegen einzeln per Filter ermittelte echte.ids je Zeile verglichen) — in allen drei Fällen stimmte die Positions- Reihenfolge diesmal exakt. Kein erneuter Fehlgriff reproduzierbar, aber das entkräftet Bug 15 nicht: RouterOS dokumentiert nirgends, dass die Reihenfolge von:put [<Pfad> find]undprint tersestabil identisch ist, und beide werden als zwei getrennte SSH-Befehle nacheinander abgesetzt. Arbeitshypothese, unbestätigt: bei/ip dhcp-server leasehandelt es sich um eine reine Timing-Race — die Lease-Tabelle ändert sich laufend durch verbindende/trennende Geräte, eine Umsortierung zwischen den beiden Befehlen (statt einer grundsätzlich falschen Zuordnung) würde erklären, warum Bug 15 dort auftrat, bei den heute getesteten, weitgehend statischen Menüs (Adressen, Routen) aber nicht. Für statische Menüs also vermutlich unkritisch, für Menüs mit häufig wechselnden dynamischen Einträgen (DHCP-Leases, Verbindungs- Tracking-Tabellen, falls je generisch angebunden) weiterhin als riskant einstufen.- 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-.setauf einem echten/interface wireless-Interface noch nie live gelaufen. Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut bestätigt— erledigt: beim M8-App-Test (siehe oben) lief ein vollerFirewallConfig-Apply mitether1als WAN erneut durch,/ip firewall filter printzeigte alle Basisregeln (established/ related, invalid-drop, ICMP-Accept, WAN-Drop) korrekt mitether1.- 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
/Applicationskopierten Version zeigt macOS die "nicht verifizierter Entwickler"-Warnung (Rechtsklick → Öffnen nötig). SSH-Hostkey-TOFU ungetestet— bestä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 servicenur 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.
Live gegen Hardware verifiziert (2026-09-15) — zuerst manuell per
SSH nachgebaut (siehe "Zum Sessionende" oben), danach über den
App-Wizard selbst (ether4, Experte-Modus). iMac ↔ Laptop erreichen
sich über die Netzgrenze nicht mehr, Internet + DNS funktionieren auf
beiden Seiten weiter. Dabei drei App-Bugs gefunden und gefixt (Bug
22–24, siehe Bug-Liste oben) — alle drei Voraussetzung dafür, dass ein
per Wizard eingerichtetes isoliertes Netz auf einem bereits
konfigurierten bzw. werksseitig gebridgten Router überhaupt funktioniert:
fehlende "LAN"-Interface-Listen-Mitgliedschaft (DNS zum Router blockiert),
fehlende Idempotenz bei erneutem Wizard-Lauf (Apply brach sofort ab),
und ein nie aus der Bridge gelöstes physisches Interface (RouterOS
verwarf die Isolationsregeln selbst als ungültig). Weiterhin unverändert
seit vorher: FirewallConfigTests (isoliertes Netzwerk gegen
nicht-isoliertes, gegenseitige Isolation, keine Isolation).
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:
- 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. - 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
.confirmationDialogmit dem exakten Befehl, erst ein zweiter Klick führt ihn aus (ExpertMenuDetailView). - Auto-Backup einmal pro Verbindung:
ConnectionService. hasExpertToolBackedUpThisSession(zurückgesetzt beidisconnect()),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".
M15: Übersicht-Tab — Diagramm-Fixes + Direktes Bearbeiten
Auf Nutzerwunsch zwei Themen in einer Session: erst mehrere echte Bugs im bestehenden Übersicht-Diagramm gefunden und gefixt, danach eine neue Fähigkeit ergänzt — ein Knoten direkt anklicken und bearbeiten, statt nur anzusehen.
Diagramm-Fixes (alle live an "Verbindungslinien werden links abgeschnitten" / "Hover springt ständig" diagnostiziert):
- Erst versucht, das Diagramm beim Öffnen automatisch ans Fenster
anzupassen (
GeometryReader+ einmalig gesetzter Scale) — hat den ScrollView-Scrollzustand durcheinandergebracht (Inhalt verschob sich nach dem ersten Layout, oberer Teil nicht mehr erreichbar). Wieder komplett zurückgezogen, zurück zur ursprünglichen manuellen Zoom-Steuerung (-/100%/+). - Echter Geometrie-Bug in
EdgesCanvas: VLAN-/Bridge-Port-Kanten laufen innerhalb derselben Spalte (Interface → Interface), eine Firewall/NAT-Kante läuft rückwärts (rechts nach links) — die Kurven-Berechnung nahm aber immer "rechts aus dem Start-, links in den Zielknoten" an, wodurch die Kontrollpunkte bei diesen Fällen weit über den Canvas hinausschossen (auch ins Negative) und am linken Rand abgeschnitten wurden. Fix: Kurvenaufbau jetzt richtungsabhängig (gleiche Spalte → kleine Ausbuchtung rechts, rückwärts → gespiegelte S-Kurve). - Hover-Instabilität: Mehrere Kanten verlassen denselben Knotenpunkt am selben Punkt — dort sind sie fast gleich nah am Cursor, kleinste Mausbewegung ließ die Auswahl ständig springen. Fix in zwei Schritten: (1) Trägheit — einmal gehoverte Kante bleibt aktiv, solange der Cursor nah genug bleibt, statt bei jedem Event neu die global nächste zu wählen; (2) Hover-Kandidaten auf die gerade hervorgehobenen (verbundenen) Kanten beschränkt, statt gegen alle Kanten im Diagramm zu prüfen.
- Klick-vs-Hover-Priorität vertauscht:
highlightNodeIDließ Hover über Klick gewinnen — beim Nachfahren einer Linie zu ihrem Ziel streifte der Cursor oft andere, unbeteiligte Karten, deren Hover dann die Hervorhebung des angeklickten Knotens verdrängte. Fix: Klick- Auswahl gewinnt jetzt über Hover (Hover dient nur noch als Vorschau, solange nichts ausgewählt ist). - Verbindungslinie anklicken zeigt jetzt ausführliche, verständliche
Infos im rechten Panel (
EdgeDetailView: Klartext-Erklärung pro Verbindungsart, "Von"/"Nach" mit Sprung zum jeweiligen Knoten) — vorher gab es nur den kleinen Hover-Tooltip mit den bloßen Rohdaten.
Direktes Bearbeiten: OverviewNode.EditTarget (Menüpfad, REST-Pfad,
RouterOS-eigene .id) an jedem Knoten, der sich sauber auf genau ein
Router-Item abbildet — IP-Adressen, Pools, DHCP-Server/-Netzwerke/
-Clients, Routen, Firewall-Filter-/NAT-Regeln, WireGuard-Peers. Klick auf
"Bearbeiten" im Detailpanel öffnet dieselbe ExpertItemEditView-Sheet
wie der Experte-Tab (dafür private entfernt, jetzt modulweit
sichtbar), über eine eigene ExpertViewModel-Instanz — gleiche Felder/
Tooltips, gleicher Bestätigungsdialog, gleicher Schreibpfad, keine
Duplikation der Schema-/Write-Logik. Nach dem Speichern lädt die
Übersicht automatisch neu. Bewusst nicht abgedeckt: physische/VLAN/
Bridge-Interfaces (Felder über mehrere typ-spezifische Menüs verteilt)
und Adress-Listen-Knoten (fassen mehrere Einträge zusammen, kein 1:1-
Bezug zu einem Router-Item).
Live gegen Hardware verifiziert — Nutzer bestätigte Kommentar-
Änderung an einer Firewall-Regel über den neuen Bearbeiten-Weg
("funktioniert"). Direkt danach beim Weitertesten vier echte Bugs
gefunden (Bug 25–28 — siehe Bug-Liste oben): geleertes Textfeld blieb
bestehen, Route-Bearbeiten scheiterte erst an einem nur-lesbaren Feld
(immediate-gw), dann am unveränderten distance=0 einer dynamischen
Route, schließlich an deren instabiler .id ("no such item"). Bug
25–27 betreffen ExpertViewModel.pendingCommand und damit auch den
Experte-Tab direkt, nicht nur den neuen Übersicht-Weg — gefixt
(vereinheitlicht: nur geänderte Felder werden beim Bearbeiten gesendet,
kuratiert wie unkuratiert). Bug 28 gefixt in OverviewViewModel: Routen
mit distance=0 (dynamisch) bieten gar kein "Bearbeiten" mehr an. 59
Unit-Tests grün (inkl. neuer ExpertViewModelTests und
OverviewGraphTests.testEditableNodesCarryTheirRouterOSMenuAndItemID/
testDynamicRouteHasNoEditTarget).
Stand der Milestones
- ✅ M1–M4: 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-sslstehen noch aus. - ✅ M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
(eigene Firewall-Regeln pro LAN/VLAN) — live gegen Hardware
verifiziert, sowohl manuell (SSH) als auch über den App-Wizard
selbst (
ether4), dabei drei App-Bugs gefunden+gefixt (Bug 22–24, siehe oben). Isolation, DNS und Internet vom Nutzer am echten Gerät bestätigt. - 🔶 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 7–13), 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, vom
Nutzer bestätigt ("das gefällt mir schon sehr gut"). War anfangs rein
lesend, seit M15 kann ein Knoten direkt bearbeitet werden (siehe
unten) — deckt weiterhin 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,
"Feste Zuweisung entfernen" für den Rückweg) — live gegen Hardware
verifiziert und vom Nutzer bestätigt funktionierend ("das funktioniert
jetzt super" / "das hat sofort funktioniert"), nach vier gefundenen und
gefixten Bugs beim Hinweg (Bug 14–17, siehe oben) — die aufwendigste
Live-Verifikation aller Milestones bisher, mehrere Diagnose-Runden mit
Rohdaten-Vergleichen und direkten Router-Gegenchecks (SSH-CLI + Winbox).
Rückweg ("Feste Zuweisung entfernen") lief dagegen beim ersten Versuch
fehlerfrei — RouterOS-Doku vorher per Recherche-Agent geprüft (kein
make-dynamic, nurremove+ Client muss neu verbinden), nicht geraten. Nutzer bestätigte den vollen Kreislauf (fest zuweisen → entfernen → Kabel/WLAN trennen+verbinden → wieder dynamisch). - ✅ M13: Backup-Wiederherstellung —
BackupService.restoreBackup(SFTP-Upload des.rsc-Skripts + RouterOS' dokumentierterreset-configuration no-defaults=yes run-after-reset=...-Restore-Weg), Modell-Abgleich vor dem Bestätigungsdialog (/system routerboardsmodel-Feld gegen die# model = ...-Kopfzeile der Sicherung, blockt bei Mismatch komplett), Gefahrenzone-UI mit explizitem Brick-Warnhinweis im Sicherungen-Tab. Live gegen Hardware verifiziert — erster Testlauf sperrte den Router komplett aus (Bug 18: keine Passwörter im Export +no-defaults=yeslöscht auch den Werks-Admin), Fix (Login-Erhalt vorangestellt) danach vom Nutzer bestätigt ("das hat funktioniert"). Restore-Weg selbst (Modell-Match, SFTP-Upload, reset+run-after-reset) lief dagegen schon im ersten Versuch fehlerfrei. - ✅ M14: Update-Check im Verbinden-Tab — Routerboard-Vollinfo
(
/system routerboard: Modell/Revision/Seriennummer/Firmware-Typ/ Firmware-Versionen), RouterOS-Software-Update-Check (/system package update: Kanal/installiert/neueste Version/Status, "Jetzt prüfen"+"Update installieren"), Routerboard-Firmware-Update ("Firmware aktualisieren") und "Jetzt neu starten" danach. Alle vier Aktionen live gegen Hardware verifiziert, inkl. der zuvor offenen Frage, ob/system routerboard upgrades normalerweise interaktive Bestätigung ("Do you really want to upgrade firmware? [y/n]") den nicht-interaktiven SSH-Weg dieser App blockiert — tut es nicht, Firmware-Update lief beim ersten Versuch sauber durch ("Firmware upgraded successfully, please reboot..."), ebenso der Neustart danach. Dabei ein weiterer Bug im generischen Lese-Pfad gefunden (Bug 20). Anschließend Design-Durchgang: Verbinden-Detailseite/Sicherungen/ Geräte von nackterListaufForm+.formStyle(.grouped)(macOS-typischer "Karten"-Look) umgestellt, damit einheitlich mit Wizard/Experte-Tab. Dark Mode auf Nachfrage geprüft und ohne Codeänderung bestätigt funktionierend (keine feste Farbwerte/ Appearance-Überschreibung im Code). - ✅ M15: Übersicht-Tab — mehrere Diagramm-Bugs gefixt (Verbindungslinien
am linken Rand abgeschnitten bei gleicher-Spalte-/Rückwärts-Kanten,
nervöses Hover, Klick-Auswahl wurde von Hover unbeteiligter Karten
verdrängt) plus neue Fähigkeit: Knoten (IP-Adresse, Pool, DHCP-Server/
-Netzwerk/-Client, Route, Firewall-Filter-/NAT-Regel, WireGuard-Peer)
direkt über dieselbe Sheet wie im Experte-Tab bearbeiten und
zurückschreiben. Dabei vier Bugs gefunden+gefixt (Bug 25–28): geleertes
Feld blieb bestehen statt geleert zu werden, Route-Bearbeiten
scheiterte an einem nur-lesbaren Feld (
immediate-gw), dann am unverändertendistance=0einer dynamischen Route, schließlich an deren instabiler.id. Bug 25–27 betreffen auch den Experte-Tab direkt, Bug 28 ist Übersicht-spezifisch (dynamische Routen bieten kein "Bearbeiten" mehr an). Live gegen Hardware verifiziert — Nutzer bestätigte Kommentar-Änderung an einer Firewall-Regel über den neuen Weg ("funktioniert"). - ✅ M16: Zweisprachigkeit (DE/EN) mit manuellem Umschalt-Button —
begonnen im Experte-Tab (App-weite Fortsetzung Tab für Tab in
Folgesessions geplant, wie mit dem Nutzer vereinbart). Erster Ansatz
(Apple String Catalog
Localizable.xcstrings+.environment(\.locale, ...)aufWindowGroup-Ebene) live widerlegt: der Umschalter änderte selbst nach explizitemLocalizedStringKey(...)-Umschreiben jedes betroffenenText/.help/Section/Feld-Aufrufs nichts am angezeigten Text ("bleibt auf deutsch") —.environment(\.locale)steuert offenbar nur Calendar-/DateFormatter-artige APIs, nicht welche SprachtabelleText(LocalizedStringKey)zur Laufzeit auflöst (das scheint an Bundle-interner Locale-Verhandlung beim Start festzuhängen). Fix:Localizable.xcstringskomplett entfernt, stattdessen eigenerRouterOSAssistant/Core/Localization/L10n.swiftmitL10n.t(german, appLanguage) -> String(Dictionary-Lookup, deutscher Text als Schlüssel, Rückgabe des deutschen Texts bei fehlender Übersetzung oderappLanguage == "de"). Jede betroffene View liest@AppStorage("appLanguage")selbst (kein Environment-Umweg mehr) und ruftL10n.t(...)direkt an jeder Text-/Tooltip-Stelle auf, sodass SwiftUI die Abhängigkeit beim Umschalten sieht und neu rendert. Bisher übersetzt: alle 6 Tab-Namen, alle 13RouterOSMenuCategory-Werte, die komplette Chrome vonExpertView.swift/ExpertMenuDetailView.swift(Buttons, Dialoge, Platzhalter, Feld-Labels/-Hilfetexte). Build + 60 Tests grün, vom Nutzer live in Xcode bestätigt ("das sieht gut aus"). Direkt danach den kompletten restlichen Katalog nachgezogen: alle 9 Menü-Familien inRouterOSSchemaCatalog.swift(Firewall/Interfaces/IP/Routing/VPN/WLAN/Queues/System/Werkzeuge) — jederdisplayName, jedeexplanation/warning, jedes Feld-Label und jeder Hilfetext hat jetzt einen Eintrag inL10n.swiftstranslations-Dictionary (376 Einträge). Beim Zusammenstellen auf zwei Dictionary-Key-Kollisionen geachtet (Swift-Dictionary-Literale crashen bei doppelten Schlüsseln zur Laufzeit statt beim Compilieren): "Deaktiviert" und "Nur zur eigenen Wiedererkennung." kamen in zwei Katalog-Familien mit identischem deutschen Text vor, jeweils den zweiten Eintrag entfernt (dieselbe Übersetzung greift ohnehin für beide Stellen). Build + 60 Tests grün, vom Nutzer live in Xcode bestätigt ("funktioniert"). Damit ist der komplette Experte-Tab zweisprachig. Direkt danach denselben Mechanismus auf den Verbinden- Tab (ConnectView.swift) ausgerollt: Verbindungsformular, Verbindungsstatus (inkl. dynamischem "Verbunden (REST-API/SSH)"), Geräte-/Routerboard-Infos, Software-/Firmware-Update-Bereich samt aller Bestätigungsdialoge, Zertifikats-/SSH-Hostkey-TOFU-Warnungen. DaConnectViewmehrere Nachrichten mit eingebetteten Laufzeitwerten baut (Fingerabdruck, Router-Version, Datum der letzten Sicherung), wurden diese in übersetzte statische Textteile + unverändert eingefügten dynamischen Wert zerlegt statt als ein Dictionary-Key mit Platzhalter (z.B.L10n.t("Fingerabdruck:", appLanguage) + " \(fingerprint)"). Die vierView-Extension-Hilfsfunktionen (withUpdateDialogs/withSoftwareUpdateDialogs/withFirmwareUpgradeDialogs/withRebootDialogs) bekamen dafür einen zusätzlichenappLanguage: String-Parameter, da sie außerhalb der View-Struct liegen und nicht direkt auf@AppStoragezugreifen können. Nebenbei eine Lücke im Experte-Tab-Katalog geschlossen: das Feld-Label "Passwort" (in fünf verschiedenen Schemas verwendet) hatte bisher keinen eigenen Übersetzungseintrag.L10n.swiftstranslations-Dictionary jetzt 444 Einträge, auf Duplikate per Python-Skript geprüft (keine gefunden). Build + 60 Tests grün, vom Nutzer live in Xcode bestätigt ("sieht gut aus"). Bewusst nicht übersetzt: Laufzeit-/Backend-Text, der direkt von RouterOS oder dem App-eigenen Fehler-/Ergebnis-Pfad kommt (z.B.update.status,firmwareUpgradeResultMessage, alleviewModel.*Error-Alerts) — das sind technische/englische RouterOS-Ausgaben bzw. app-generierte Fehlertexte, keine kuratierten UI-Strings; offen bleiben die übrigen vier Tabs (Einrichten/Übersicht/Geräte/Sicherungen), die noch auf festen deutschen String-Literalen laufen. - ✅ M17: "Bekannte Router" im Verbinden-Tab — Nutzerwunsch: "mehrere
Logins hinterlegen können, quasi eine Liste bekannter Router". Neue
SavedRouter/SavedRoutersStore(JSON-Array in UserDefaults, kein eigenes Verzeichnis wie bei Backups nötig) merkt sich Host+Benutzername nach jeder erfolgreichen Verbindung, mit einem beim ersten Mal automatisch gesetzten Anzeigenamen —RouterDeviceInfo.boardName(RouterOS' "board-name", die Marketing-Bezeichnung wie "hEX", die bereits beim Verbinden geladen wird), als nächstliegende Auslegung von "werksmäßige Bezeichnung". Der Name bleibt bei späteren Verbindungen zum selben Host/Benutzernamen unverändert, auch wenn sich die erkannte Modellbezeichnung ändern sollte — nur explizites Umbenennen überschreibt ihn. Passwörter bleiben unverändert im macOS-Schlüsselbund (Keychain- Service, Schlüssel "Benutzername@Host"), diese Liste speichert nur Host/Benutzername/Anzeigename/letzte Verbindungszeit. Klick auf einen Eintrag füllt Host/Benutzername/Passwort ins Formular, ohne sofort zu verbinden; ein "…"-Menü pro Eintrag bietet Umbenennen (inline) und Entfernen. Live bestätigt ("ok, funktioniert jetzt") — allerdings erst im zweiten Anlauf: die Liste füllt sich nur bei einer neuen erfolgreichen Verbindung nach dem Rebuild, eine bereits laufende Alt-Verbindung zählt nicht rückwirkend — das war der erste Verwirrungspunkt beim Testen ("ich sehe 'Bekannte Router' nicht"). 74 Tests grün (5 neueSavedRoutersStoreTests+ 5 neueConnectionServiceTests-Ergänzungen für die parallel gebaute Port-Konflikt-Prüfung, siehe unten).- Bugfix, live gefunden: Umbenennen tat nichts — das TextField steckte
innerhalb eines
Button(action: onSelect), dessen.disabled(isRenaming)auch das TextField selbst deaktivierte. Fix: kein umschließender Button mehr,.onTapGestureübernimmt die Zeilen-Auswahl, TextField bekommt eigenen@FocusState. Live bestätigt ("funktioniert"). - Erweiterung, Nutzerwunsch: freies Standort-Feld (Etage/Raum/Zweck)
pro Eintrag, "damit macht die Zuordnung bei mehreren Geräten einfacher".
SavedRouter.location: String(leer=nicht gesetzt), eigeneSavedRoutersStore.updateLocation(_:to:). Menüpunkt "Umbenennen" wurde zu "Bearbeiten" (öffnet Name + Standort gemeinsam). Rückwärtskompatibles Decoding (init(from:)mitdecodeIfPresent) für bereits gespeicherte Listen ohnelocation-Feld. 76 Tests grün (2 neue). Live bestätigt ("funktioniert"). - Erweiterung, Nutzerwunsch: Liste auf ca. 4 sichtbare Einträge
gedeckelt (eigene
ScrollViewmitmaxHeight-Cap statt das ganze Formular wachsen zu lassen) — "die Liste der gespeicherten Router soll nicht zu lang werden". Noch nicht separat live bestätigt (kein ausdrückliches Feedback, aber auch kein Einwand in den folgenden Test-Runden).
- Bugfix, live gefunden: Umbenennen tat nichts — das TextField steckte
innerhalb eines
Gleichzeitig gebaut, noch nicht live bestätigt (nur Build+Unit-Tests grün, wartet auf Test durch den Nutzer in Xcode):
-
LAN-Port-Konflikt-Prüfung im Einrichten-Assistenten — Nutzerwunsch: vor dem Anlegen eines neuen LAN prüfen, ob der gewählte Port frei ist (nicht in einer Bridge, keine bestehende IP-Adresse, kein WAN-DHCP-Client/PPPoE), und bei einem Konflikt mit doppelter Sicherheitsbestätigung nachfragen, bevor die App den Port selbständig freimacht. Neu:
PortConflict-Modell (Core/Models),ConnectionService. checkPortConflict(interfaceName:)(liest/interface bridge port,/ip address,/ip dhcp-client,/interface pppoe-clientparallel perasync let),SetupViewModel.checkPortConflict(for:)/acknowledgePortConflict(for:)/hasUnresolvedPortConflict(for:),LanStepViews neuePortConflictWarningViewmit zwei aufeinander folgenden.confirmationDialogs (Konsequenzen erklären, dann "Wirklich sicher?"). Tatsächlich ausgeführt wird die Port-Freigabe erst beim finalen "Jetzt anwenden" im Review-Schritt, nicht sofort bei der Bestätigung. "Weiter" bleibt gesperrt, bis jeder Konflikt entweder bestätigt oder durch Wahl eines anderen Ports vermieden wurde. -
"Fertig"-Button nach erfolgreichem Anwenden — vorher gab es nach "Jetzt anwenden" nur einen (weiterhin aktiven) "Zurück"-Button und den jetzt dauerhaft deaktivierten "Jetzt anwenden"-Button, keinen klaren Abschluss.
SetupViewModel.finish()setzt den Assistenten komplett zurück (frische Default-Werte,prepareDefaultserneut gegen die aktuellen Live-Interfaces) für einen sauberen nächsten Durchlauf. -
✅ M18: Live-Traffic-Anzeige im Verbinden-Tab — Nutzerwunsch: der Punkt vor jedem Interface soll erkennen lassen, ob es "aktuell in Verwendung ist und Daten überträgt", nicht nur ob der Link steht. Neue
InterfaceTraffic/InterfaceTrafficMonitor(Actor, eigene dedizierte SSH-Verbindung unabhängig von REST/SSH-Hauptverbindung — RouterOS'/ interface monitor-trafficist ein reiner CLI-Befehl ohne REST- Äquivalent, gleiche Begründung wie beiBackupService/UpdateService), pollt alle 3s pro Interface.ConnectViewModel.startTrafficPolling(...)/stopTrafficPolling(), angestoßen über.onChange(of: connectionService. state). Punkt: grau = kein Link, grün (fest) = Link aber keine Daten, grün pulsierend = überträgt gerade tatsächlich Daten.- Zwei reale Bugs beim ersten Live-Test gefunden ("alle buttons
sind grau, keine Animation") — auf Bitte des Nutzers zwei Diagnose-
Befehle direkt am Router ausgeführt statt zu raten:
RouterOSCliParser.parseInterfacessuchterunning=/disabled=alskey=value-Paare — die gibt es in echtem/interface print terse-Output gar nicht. RouterOS codiert das stattdessen als Buchstaben-Flags vor dem ersten Feld ("0 R name=ether1 ...","2 S name=ether3 ...": R=running, X=disabled, S=Bridge-Slave) — jedes Interface lasrunningbisher immer alsfalse, ein vorbestehender, nie zuvor aufgefallener Bug (nicht durch M18 verursacht, nur durch M18 erstmals sichtbar geworden). Fix: neueflagsColumn(of:)-Hilfsfunktion isoliert den Flag-Bereich vor dem ersten "=", prüft dort auf "R"/"X" statt auf nicht existierende Schlüssel./interface monitor-traffic ... onceliefert Werte wie"50.7kbps"(mit Einheit und Dezimalpunkt), keine reine Zahl —Int(...)scheiterte daran lautlos zu 0. Fix:SSHTransport. parseBitsPerSecond(_:)erkennt Gbps/Mbps/kbps/bps-Suffixe (längere Suffixe zuerst geprüft, da "kbps" selbst auf "bps" endet).
- 82 Tests grün (2 neue Parser-Tests mit dem exakten vom Nutzer eingefügten Live-Output + 5 neue Bits-pro-Sekunde-Tests). Live bestätigt ("sehr gut").
- Zwei reale Bugs beim ersten Live-Test gefunden ("alle buttons
sind grau, keine Animation") — auf Bitte des Nutzers zwei Diagnose-
Befehle direkt am Router ausgeführt statt zu raten:
-
✅ M19: Übersicht-Tab — animierte Flussrichtung + verschiebbare Knoten.
- Animierte Flussrichtung (Nutzerwunsch: "die Linien im Übersicht-
Tab auch animiert... mit einer animierten Flußrichtung"): alle
Verbindungslinien laufen jetzt gestrichelt mit über die Zeit
wandernder
dashPhasein Richtung "von → nach". Technisch überTimelineView(.animation)um den bestehendenCanvasgelegt — ein reiner@State-Wert mitwithAnimation(.repeatForever)hätte den (immediate-mode)Canvasnicht laufend neu gezeichnet,TimelineViewruft den Draw-Closure dagegen bei jedem Frame mit frischem Datum neu auf. - Verschiebbare Knoten + Zurücksetzen-Button (Nutzerwunsch: "kann
man die einzelnen Kästchen verschiebbar machen? ... Die Linien
sollten den Kästchen automatisch folgen. ein Button für den Reset
wäre super"): neuer
nodeOffsets-State pro Knoten-ID plus ein@GestureStatefür die gerade laufende Drag-Bewegung, kombiniert ineffectivePositions— sowohl Knoten-Karten als auchEdgesCanvaszeichnen auf dieser verschobenen Position, Linien folgen live mit..simultaneousGesture(nicht.gesture) fürs Draggen, damit der bestehende Klick-zum-Auswählen weiter funktioniert. Toolbar-Button "Zurücksetzen" leertnodeOffsets, nur aktiv wenn etwas verschoben wurde.- Zwei kleine UI-Nachbesserungen, beide live gefunden: Der
Reset-Button erschien zunächst gar nicht (
Labelmit Text+Icon braucht mehr Platz als die reinen Icon-Buttons davor, vermutlich von macOS in die Toolbar-Overflow verschoben) — behoben durch Icon-only, dann von Nutzer als "verwirrend, ähnlich wie Refresh" zurückgemeldet — endgültig auf einen reinen Text-Button "Zurücksetzen" umgestellt (wie der bereits funktionierende "100%"-Button).
- Zwei kleine UI-Nachbesserungen, beide live gefunden: Der
Reset-Button erschien zunächst gar nicht (
- 82 Tests grün (unverändert — reine SwiftUI-Interaktions-/Animations- Logik ohne neue reine Parser-Funktionen). Live bestätigt ("das vierschieben funktioniert super, die Linien folgen auch" / "ok, der button ist da und funktioniert" / "funktioniert").
- Animierte Flussrichtung (Nutzerwunsch: "die Linien im Übersicht-
Tab auch animiert... mit einer animierten Flußrichtung"): alle
Verbindungslinien laufen jetzt gestrichelt mit über die Zeit
wandernder
-
✅ M20: LAN-Scanner-Umbau — Umbenennung, Netzwerk-Tools, mehrere kleine UX-Nachbesserungen.
- Tab umbenannt: "Geräte" → "LAN-Scanner" (Nutzerwunsch, "oder ähnliches"), Toolbar-Button "Aktualisieren" → "Neu scannen" (macht exakt dasselbe — DHCP-Leases/ARP/Bridge-Host neu abfragen —, Umbenennung passt nur besser zum "Scanner"-Namen).
- Netzwerk-Tools-Untermenü beim Rechtsklick auf ein Gerät:
Ping/Traceroute/DNS-Auflösung (nslookup) — alle drei vom Router aus
über eine neue dedizierte SSH-Verbindung (
NetworkToolsService, gleiches Muster wieBackupService/InterfaceTrafficMonitor, da/ping//tool traceroute//resolvereine CLI-Befehle ohne REST- Äquivalent sind). Ergebnis als Rohtext im Sheet gezeigt statt geparst — das genaue Ausgabeformat dieser Tools ist nicht gegen echte Hardware verifiziert, nach den Parser-Überraschungen bei M18 bewusst vorsichtig. Sicherheitsmaßnahme: IP/Hostname eines Geräts stammen letztlich aus dessen eigener (potenziell manipulierbarer) DHCP-Anfrage — vor dem Einsetzen in den RouterOS-Befehlsstring auf ein sicheres Zeichen-Set geprüft (NetworkToolsService.sanitized), da RouterOS' Konsole ";" als Befehlstrenner behandelt und ein böswillig gesetzter Hostname sonst einen zweiten Befehl einschleusen könnte. - Port-Scan (Nutzerwunsch: "Offene Ports rot, geschlossene grün,
nicht erreichbare grau") — läuft bewusst NICHT über den Router
(RouterOS hat kein eingebautes Portscan-Tool), sondern direkt von
diesem Mac per
Network.framework/NWConnectiongegen ~19 gängige TCP-Ports parallel. Live-Bug gefunden und gefixt: eine verweigerte Verbindung (Port zu, TCP RST) meldet sich beiNWConnectionüber den.waiting-Zustand, nicht über.failed— der ursprüngliche Code prüfte nur.failed, wartete bei einer Ablehnung also den vollen Timeout aus und meldete fälschlich "nicht erreichbar" statt "geschlossen". Test hat das direkt gegen einen echten (aber garantiert nicht belegten) Loopback-Port aufgedeckt, kein Rätselraten nötig. - Warnhinweis "Feste IP-Adresse zuweisen?" erweitert (Nutzerwunsch: "der Hinweis, wie die Adresse wieder in dynamisch gemacht werden kann") — nennt jetzt explizit den Rückweg (Rechtsklick → "Feste Zuweisung entfernen" im selben Tab) und dass das Gerät danach die Verbindung kurz trennen/neu aufbauen muss.
- "Neu scannen"-Button prominenter gemacht (Nutzerwunsch: "wird
übersehen") —
.buttonStyle(.borderedProminent)statt eines reinen Icon-Buttons. - DE/EN-Umschalter zeigt jetzt Landesflaggen (🇬🇧/🇩🇪) statt "EN"/"DE"-Text — zeigt die Flagge der Sprache, zu der ein Klick wechselt, exakt dieselbe "switch to"-Logik wie vorher.
- 92 Tests grün (6 neue
PortScannerTests, 6 neueNetworkToolsServiceTestsfür die Zeichen-Validierung). Alle Teile live bestätigt ("funktioniert" / "sieht gut aus und funktioniert"). - Nebenbei: großer
.alert/.confirmationDialog/.sheet-Modifier- Stapel inDevicesView.swiftlöste erneut Bug 19 aus ("compiler is unable to type-check this expression in reasonable time") — nach demselben, bereits etablierten Muster in zweiView-Extension- Funktionen aufgeteilt (withStaticAssignmentDialogs/withDeviceDetailSheets).
-
✅ M21:
L10n.t(...)-Zweisprachigkeit (Bug-19-Muster: M16 begann im Experte-Tab, siehe oben) auf die restlichen vier Tabs ausgerollt — Einrichten (alle sieben Wizard-Schritte: Mode/Wan/Lan/Vlan/Wifi/Firewall/Review, inkl.PortConflictWarningViewals eigener Sub-Struct mit durchgereichtemappLanguage), Übersicht (inkl.EdgeTooltipView/EdgeDetailView/NodeDetailView/LegendView), LAN-Scanner, Sicherungen.L10n.swiftum einen entsprechenden Übersetzungsblock erweitert; Model-Layer-Strings (z.B.WanConnectionMode.label,OverviewNode.Category.rawValue,OverviewEdgeKind.rawValue) werden wie schon beiRouterOSMenuCategoryam Verwendungsort gewrappt, nicht in der Model-Datei selbst geändert. Etablierte Konvention beibehalten: nur statische View-Strings werden übersetzt, ViewModel-generierte Laufzeitstrings (Fehlermeldungen,command.summary/cliLine,applyLog-Zeilen, Router-Live-Daten) bleiben unübersetzt — dieselbe Grenze, die schon der Experte-Tab zieht. Bug 19 (Modifier-Ketten-Typecheck-Timeout) trat dabei ein drittes Mal inDevicesView.swiftauf (zwei lange Confirmation-Dialog-Texte), Fix wie gehabt: in eigene Funktionen (staticAssignmentMessage/staticRemovalMessage) ausgelagert statt inline verkettet. Live bestätigt ("funktioniert").
Nutzerfrage, keine Umsetzung (2026-09-15): ob es Sinn macht, weitere
Sprachen "per Plugin" zu importieren. Antwort: für 1-2 weitere feste
Sprachen passt das bestehende L10n.swift-Dictionary-Muster gut; ein
echtes ladbares Plugin-System wäre für dieses Nischen-Tool vermutlich
Overkill ohne konkrete Nachfrage. Nutzer bleibt vorerst bei DE/EN, hat
aber erwähnt, dass Spanisch (und evtl. Hindi/indischer Markt, unklar)
später dazukommen könnte, da MikroTik-Router in ärmeren Ländern
(Brasilien, spanischsprachige Länder) stark verbreitet sind — siehe
Claude-Memory future-language-support.md für Details.
"Abbrechen"-Button im Einrichten-Tab ergänzt (2026-09-16, Nutzerwunsch:
"mir fehlt im einrichte-tab (einfach und experte) noch ein
"Abbrechen"-Button") — vorher gab es keinen Weg aus dem Wizard außer
sich Schritt für Schritt per "Zurück" bis zu Modus vorzuklicken. Zentral
in SetupView.swift als Toolbar-Button gelöst (nicht in jedem der sieben
Schritt-Views einzeln dupliziert), sichtbar auf jedem Schritt in beiden
Modi, mit .confirmationDialog ("Einrichten abbrechen?" /
"Weiter ausfüllen") gegen versehentlichen Datenverlust bereits
eingegebener Werte, deaktiviert während isApplying (ein laufendes
Apply soll nicht mitten im Ablauf abreißen). SetupViewModel.finish()
in eine private resetToInitialState() plus zwei benannte Wrapper
(finish()/neues cancel()) aufgeteilt — gleicher Reset, aber je nach
Aufrufkontext (erfolgreicher Apply vs. Nutzer bricht ab) unter dem
passenden Namen lesbar. Live bestätigt ("sieht gut aus").
M23: Experte-Tab-Sektionsüberschriften prominenter + einklappbar (2026-09-16, Nutzerwunsch: "die Sektionsüberschriften ... müssen prominenter sein und eingefärbt, bitte größer machen und passend farblich hinterlegen (darkmode beachten)", danach "die einzelnen Sektion einklappbar machen"). Zwei Schritte:
Section("...")s Standard-Header (macOS' winzige graue Kapitälchen- Beschriftung) durch eigenenTextersetzt: größer/fett (.title3.bold()), HintergrundColor.accentColor.opacity(0.18)statt eines festen RGB-Werts, damit Light/Dark Mode automatisch passend kontrastieren —.textCase(nil)nötig, sonst erzwingt macOS trotzdem wieder seine eigene Kapitälchen-Darstellung über dem eigenen View.- Einklappbar gemacht — erster Versuch mit SwiftUIs eingebautem
Section(isExpanded:)(native Disclosure-Dreieck), vom Nutzer sofort korrigiert: "der Pfeil sollte immer zu sehen sein und nicht erst beim Hovern" — das native macOS-Sidebar-Dreieck ist genau wie in Finder standardmäßig Hover-only, keine SwiftUI-Option dafür vorgesehen. Fix: eigenes Chevron-Icon (chevron.right, per.rotationEffectgedreht) direkt im selbstgebauten Header, ganzer Header alsButtonklickbar;Section(isExpanded:)-API komplett fallengelassen zugunsten einesif isExpanded { ForEach(...) }innerhalb eines schlichtenSection { }, gesteuert über eigenen@State/Binding (collapsedCategories: Set<RouterOSMenuCategory>für die neun Kategorien,isCustomPathExpanded: Boolfür "Eigener Menüpfad"). Auf Nutzerwunsch ("standardmäßig sollten beim Öffnen des Experte-Tab alle Sektion zugeklappt sein") startet alles zugeklappt..listStyle(.sidebar)an derListergänzt, für den nativen macOS-Sidebar-Look. Live bestätigt ("sehr gut, bin sehr zufrieden").
M24: LAN-Scanner — "Aktionen"-Button, Traffic-Monitor pro Port + Sparkline, ARP-Auflösungsbug gefixt (2026-09-16). Drei Nutzerwünsche in einer Runde:
- "Aktionen"-Button statt Rechtsklick ("der Rechtsklick ist nicht
eindeutig erkennbar oder intuitiv, verlager bitte alle Funktionen
des Rechtsklicks in den Button") —
.contextMenukomplett entfernt, derselbedeviceMenu(for:)-Inhalt (Feste IP zuweisen/entfernen, Netzwerk-Tools, Rohdaten anzeigen) läuft jetzt über einen sichtbarenMenu-Button ("•••", Label "Aktionen") hinter jeder Geräte-Zeile. - Traffic-Monitor pro Port + Sparkline ("ein Traffic-Monitor im
LAN-Scanner", danach "ein kleines Liniendiagramm der letzten 10
Sekunden ... pro Port") — pro-Gerät geht nicht ohne RouterOS-
seitige Queue-Trees pro MAC (Nutzer selbst dagegen entschieden:
"Traffic pro PORT statt pro Gerät"), daher denselben bereits
gebauten
InterfaceTrafficMonitor(Verbinden-Tab) wiederverwendet —DevicesViewModelbekam eigenestartTrafficPolling/stopTrafficPolling(3s-Takt, gleiches Muster wieConnectViewModel). Live-↓/↑-Zahl direkt neben jedem Port-Titel, plus neuerTrafficSample-Typ (Core/Models/InterfaceTraffic.swift) für eine nach echter Zeit (nicht fester Sample-Anzahl) auf 10 Sekunden getrimmte Historie, gerendert alsChart/LineMark(Swift Charts,TrafficSparklineinDevicesView.swift) ohne Achsen — reiner Trend-Blick, die genaue Zahl steht ja daneben. - Bug 34, dabei live gefunden: ein Laptop an
ether4(Traffic sichtbar aktiv) erschien trotzdem unter "Unbekannter Port". Ursache: RouterOS'/ip arp-Tabelle hielt für dieselbe MAC-Adresse zwei Zeilen gleichzeitig — eine korrekte (interface=ether4, status=reachable) und eine veraltete (interface=bridge, status=failed, Überbleibsel von vor einem Netzwerkwechsel des Geräts).DevicesViewModel.buildDevicesbautearpInterfaceByMACbisher per blindem Überschreiben der letzten gesehenen Zeile auf — welche Zeile "gewann" hing von der Router-internen Tabellen- Reihenfolge ab, nicht von Korrektheit. Fix:status=reachablegewinnt jetzt immer gegen jeden anderen Status, bei Gleichstand ein konkreter Port gegen "bridge". Zwei neue Regressionstests (testReachableArpRowWinsOverStaleFailedRowForSameMAC/...RegardlessOfOrder, letzterer prüft explizit, dass der Fix nicht nur für die live beobachtete Reihenfolge zufällig funktioniert).
Nebenbefund beim Debuggen von Punkt 2, kein Code-Bug, aber ein
Betriebshinweis: der Traffic-Monitor zeigte zunächst dauerhaft nichts
an — InterfaceTrafficMonitor.fetchTraffic scheiterte still (try?)
mit untrustedSSHHostKey. Ursache: der Router hatte seit dem letzten
Werksreset (2026-09-15) einen neuen SSH-Host-Key, aber da die Haupt-
verbindung dieser Session durchgehend über REST lief (M22-Fix), kam der
bereits vorhandene "Unbekannter SSH-Schlüssel"-Bestätigungsdialog nie
zum Zug — der lief bisher nur im SSH-Fallback-Pfad von
ConnectionService.connect. Dedizierte SSH-Verbindungen (Backup,
Netzwerk-Tools, Traffic-Monitor, Update, Werksreset) haben keinen
eigenen UI-Bestätigungspfad für einen neuen Host-Key, scheitern bei
einem Mismatch einfach still. Behoben durch www-ssl kurz deaktiviert
(erzwingt den SSH-Fallback-Pfad in der Haupt-Verbindung), Nutzer hat
den neuen Fingerprint einmalig über den bestehenden Dialog bestätigt
(SSHHostKeyTrustStore ist ein einziger, über UserDefaults.standard
geteilter Speicher — einmal dort vertraut gilt für jede dedizierte
SSH-Verbindung mit), danach www-ssl wieder aktiviert. Bleibt ein
echter, noch offener Schwachpunkt: falls der SSH-Host-Key sich künftig
nochmal ändert (z.B. nach einem weiteren Werksreset) UND REST zu diesem
Zeitpunkt bereits verbunden ist, würden dieselben dedizierten Dienste
wieder wortlos nichts tun, ohne Hinweis auf die Ursache — siehe
"Nächste Schritte".
M25: Einrichten-Wizard-Politur (2026-09-16, mehrere kleine Nutzerwünsche in einer Runde):
- WAN-Schritt: "Zurück"-Button ergänzt — fehlte seit jeher (nur "Weiter"), in beiden Modi, fiel im Experte-Modus nur eher auf, da dort mehr Schritte durchgeklickt werden. Live bestätigt.
- "Abbrechen" (Einrichten) + "Jetzt sichern" (Sicherungen) prominent
gemacht — dieselbe
.buttonStyle(.borderedProminent)-Behandlung wie zuvor schon "Neu scannen" (Bug: "leicht zu übersehen"), jetzt konsequent auf alle einzelnen, gleichrangigen Toolbar-Aktions- buttons der App angewendet. Übersicht-Tabs Toolbar-Gruppe (Zoom/ Zurücksetzen/Aktualisieren, mehrere gleichrangige Buttons nebeneinander) bewusst ausgenommen — dort würde durchgängiges Prominent-Styling überladen wirken, ein anderes UI-Muster als "eine einzelne leicht übersehene Hauptaktion". - LAN- und VLAN-Schritt: Adressfelder starten leer (Nutzerwunsch:
"die auszufüllenden Felder leer lassen, nur Beispiele zeigen ...
so sieht man das Schema dahinter") —
LanDhcpConfig/VlanEntryhatten bisher konkrete Default-Werte (z.B. "192.168.88.1/24" bzw. eine aus der VLAN-ID abgeleitete Adresse); da jedes TextField-Label ("Router-Adresse (z.B. 192.168.88.1/24)") ohnehin als Platzhalter dient, sobald das Feld leer ist, versteckte ein vorbelegter Wert dieses Beispiel bisher komplett. Betrifft beide Modi (LAN-Schritt wird von Einfach und Experte geteilt).interfaceName(Picker) undleaseTimeHours(Stepper) bleiben unverändert vorbelegt — beides Auswahl-UI ohne "Beispiel tippen"-Konzept. Notwendige Begleitänderung:VlanStepViews "Weiter" hatte bisher keine Validierungssperre (unnötig, solange die Defaults immer gültig waren) — jetzt analog zum längst vorhandenenLanStepView-Muster ergänzt, sonst hätte ein leer gelassenes VLAN-Feld einen Befehl mit leeren Argumenten zum Router geschickt. Zwei Tests entsprechend angepasst (testDefaultAddressesStartEmptyForExamplePlaceholderersetzt die alte Ableitungs-Prüfung;testLanDhcpCommandsCoverAddressPoolServerAndNetworksetzt die Adressfelder jetzt explizit statt sich auf Modell-Defaults zu verlassen). Live bestätigt ("gut soweit").
M26: "Bekannte Router" hinterlegt jetzt die Seriennummer
(2026-09-16). Nutzer-Szenario, live so passiert: mit einem zweiten
Router desselben Modells verbunden (Werks-Adresse+Zugangsdaten
identisch zum bisherigen Testrouter, 192.168.88.1/admin) — die App
hat daraufhin den bestehenden "Bekannte Router"-Eintrag des alten
Testrouters einfach für den neuen umbenannt (SavedRoutersStore. recordSuccessfulConnection matchte bisher nur auf Host+Benutzername,
kein Feld unterschied zwei physisch verschiedene Geräte an derselben
Werks-Adresse). Klick auf den alten Eintrag verband danach zwangsläufig
mit dem tatsächlich erreichbaren Gerät (dem neuen Router), nicht mit
dem gemeinten alten. Fix (Nutzervorschlag: "ist es nicht besser die
Seriennummer des Gerätes mit zu hinterlegen?"): SavedRouter bekam ein
neues optionales serialNumber-Feld (/system routerboards
"serial-number", bereits seit M14 als RouterBoardInfo.serialNumber
ausgelesen — "unbekannt" wird dabei wie nil behandelt, sonst würden
zwei Geräte ohne auslesbare Seriennummer fälschlich als "gleich"
gelten). recordSuccessfulConnection matcht jetzt gestuft: exakte
Seriennummer gewinnt zuerst; ein einzelner bestehender Eintrag ohne
gespeicherte Seriennummer (alte, vor diesem Feld gespeicherte Einträge,
oder Geräte ohne physisches Routerboard) wird beim nächsten Connect
einmalig nachträglich damit befüllt statt dupliziert; alles andere legt
einen neuen, getrennten Eintrag an. In SavedRouterRow als kleine
"SN: ..."-Zeile sichtbar gemacht, damit zwei gleich benannte/adressierte
Einträge auch optisch unterscheidbar sind. Drei neue Tests
(testDifferentSerialAtSameHostUsernameCreatesSeparateEntry,
testLegacyEntryWithoutSerialGetsUpgradedInPlaceNotDuplicated,
testSameSerialReconnectUpdatesRecencyNotDuplicated). Migrationshinweis
für den konkreten Fall: der bereits verschmolzene Bestandseintrag zeigt
aktuell die Seriennummer des neuen Routers — der nächste Connect zum
alten Testrouter legt automatisch einen neuen, eigenen Eintrag an (da
dessen Seriennummer nicht mehr passt), bei Bedarf danach umbenennen.
Live bestätigt ("passt, funktioniert") — allerdings dabei zwei weitere,
eng verwandte Bugs gefunden, siehe M27 direkt danach.
M27: Passwort-Anzeige-Button, Schlüsselbund-Trennung, terse-Fallback
verallgemeinert (2026-09-16, beim Live-Test von M26 gefunden):
- Passwort-Anzeige-Button (Nutzerwunsch: "setze hinter das Passwort
eine Auge-Symbol zum aufdecken des Passwortes, so kann ich prüfen, ob
die Daten übernommen wurden") — Augen-Icon neben dem Passwortfeld im
Verbinden-Tab, schaltet zwischen
SecureFieldundTextFieldum. War das Diagnose-Werkzeug, das die beiden folgenden Bugs erst aufdeckte. - Bug 35: Schlüsselbund teilte sich einen Eintrag zwischen zwei
Routern — beim Umschalten zwischen zwei "Bekannte Router"-Einträgen
(seit M26 korrekt als getrennte Einträge geführt) blieb das
angezeigte Passwort trotzdem identisch. Aufwendig eingegrenzt: ein
temporärer Debug-Print bestätigte zunächst, dass
ConnectViewModels@Published-Werte bei jedem Klick korrekt gesetzt wurden (auch die View selbst rendert nachweislich jedes Mal frisch, per zweitem Debug-Print inmainContentbestätigt) — zwei SwiftUI-.id()- Reparaturversuche (erst auf Section-, dann auf Feld-Ebene) liefen deshalb ins Leere, weil das eigentliche Problem gar keins der Anzeige war. Tatsächliche Ursache:KeychainServicespeichert Passwörter nur nach"username@host"— zwei physisch verschiedene Router mit identischem Host+Benutzername (Werks-Adresse) teilen sich dadurch denselben Schlüsselbund-Eintrag, unabhängig davon, dassSavedRoutersStoresie seit M26 bereits korrekt als getrennte Einträge führt. Fix:KeychainService.save/loadPassword/deletePasswordbekommen einen optionalenserialNumber-Parameter, der den Account-Key auf"username@host#seriennummer"erweitert;loadPasswordfällt auf den alten, unqualifizierten Key zurück, wenn kein qualifizierter Eintrag existiert (Rückwärtskompatibilität für vor diesem Fix gespeicherte Passwörter).ConnectViewModel. connect()s Passwort-Speicherung musste dafür hinter den erfolgreichen Verbindungsaufbau verschoben werden (die Seriennummer ist vorher noch nicht bekannt). Die beiden zuvor eingebauten.id()-Umwege wieder entfernt, da wirkungslos und irreführend (falsche Fehlerursache im Kommentar). - Bug 36, direkt danach beim selben Live-Test gefunden: neuer Router
zeigte gar keine Routerboard-Infos, keine Seriennummer — derselbe
Root Cause wie der vorherige "Jetzt prüfen"-Fehler (
/system package update, siehe oben):/system routerboard print ... terseschlägt auf diesem Router/dieser RouterOS-Version mit"expected end of command (line 1 column N)"fehl statt mit dem bisher einzig abgefangenen"bad parameter terse".fetchMenuItemss Singleton-Fallback (Bug 8/20) griff deshalb nicht,routerBoardInfobliebnil(best-effort, Fehler wurde still verschluckt) — betraf gleichzeitig die Seriennummer (M26) und die komplette Routerboard-Anzeige im Verbinden-Tab. Fix:SSHTransport.fetchMenuItemss Erkennung in eine neuerejectsTerse(_:)-Hilfsfunktion ausgelagert, die jetzt beide bekannten Formulierungen abfängt. Sicher eingegrenzt, da die geprüfte Fehlermeldung ausschließlich vom exakten Aufruf"<menuPath> print without-paging terse"stammt — jeder Parserfehler dort betrifft per Konstruktion das anhängende"terse". Behebt nebenbei auch den "Jetzt prüfen"-Fehler von vorhin (/system package update), da beide über denselben Code-Pfad laufen. Live bestätigt ("passt, funktioniert").
Nächste Schritte
-
M16: restliche,RouterOSSchemaCatalog.swift-Sektionen übersetzenVerbinden-Tab zweisprachigund— alles erledigt, live bestätigt ("funktioniert"). Damit sind alle fünf Tabs vollständig zweisprachig (DE/EN).L10n.t(...)-Umstellung auf die übrigen vier Tabs (Einrichten/Übersicht/LAN-Scanner/Sicherungen) -
WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
-
Rest von M7: REST-Schreibtest an einem Gerät mit aktivem— erledigt (2026-09-16), siehe "Bekannte Einschränkungen" oben (Bug 30–32).www-sslwww-sslbleibt auf dem hEX-Testrouter aktiv (eigene CAlocal-ca+rest-cert), damit die App weiterhin REST-zuerst verbindet statt SSH-Fallback. Fehlerzustände/Politur im REST-Pfad noch nicht gezielt geprüft (z.B. Verbindungsabbruch mitten im Apply) — optional für später. -
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.
-
M10: WLAN-Schemas (an Gerät mit WLAN-Chip), Bonding, PPPoE-Client (mit echten oder Test-ISP-Zugangsdaten) noch gegen Hardware verifizieren.
-
Dauer-Editor (— erledigt (2026-09-16): WireGuard-Peer "Keepalive" (Label-Zusatz "(Sekunden)" entfernt, Stepper zeigt Einheit selbst), Scheduler "Intervall", Netwatch "Prüf-Intervall" (Default vonRouterOSFieldSchema.Kind.duration) auch auf weitere Zeitwert-Felder anwenden"00:00:10"auf"10s"umgestellt —DurationFieldEditor.parseversteht nur das Suffix-Format<Zahl><d/h/m/s>bzw. eine nackte Sekundenzahl, keinHH:MM:SS; der alte Default wäre beim Öffnen stillschweigend als "leer" geparst worden). Vor der Umstellung live geprüft (Test-Einträge angelegt und sofort wieder entfernt): RouterOS akzeptiert das Suffix-Format bei allen drei Feldern (persistent-keepalive=25s, Scheduler-interval=30s, Netwatch-interval=30s, jeweils imprint tersebestätigt). Live in der App bestätigt ("passt, live bestätigt"). Geräte-Tab hat weiterhin keine Zeitfelder, daher nicht betroffen. -
Firewall-NAT-/Filter-— behoben (2026-09-16): im Gegensatz zu den bei Bug 22–24 gefixten Menüs (dort peradd-Befehle sind bei wiederholtem Wizard-Lauf weiterhin nicht idempotent.set-Retry auf einem eindeutigen Feld wiename/interfacegelöst) haben Firewall-Filter/NAT-Regeln kein eindeutiges Identitätsfeld — "dieselbe Regel" heißt hier "identisches Argument-Set".SetupViewModel.apply(credentials:)holt deshalb einmalig (nicht pro Befehl) eine Live-Momentaufnahme der bestehenden/ip firewall filter- und/ip firewall nat-Regeln (connectionService.fetchMenuItems), undisDuplicateFirewallRulevergleicht jeden geplanten.add-Befehl (ohne das rein schreibseitige"place-before"-Argument, das nie ein gespeichertes RouterOS-Feld ist) gegen diese Momentaufnahme — bei Treffer wird der Befehl übersprungen und im Ablauf-Log als "bereits vorhanden, übersprungen" vermerkt statt erneut ausgeführt. Einmalige Momentaufnahme statt Nachfrage pro Befehl, damit in derselben Wizard-Ausführung neu hinzugefügte Regeln (z.B. eine zweite Isolationsregel) nicht versehentlich gegen sich selbst als Duplikat erkannt werden, aber Regeln aus einem früheren Wizard-Lauf trotzdem erkannt werden. Live bestätigt: kompletter Wizard mit aktivierter Firewall-Sektion zweimal hintereinander gegen denselben Router angewendet, Filter-/ NAT-Regelanzahl blieb beim zweiten Durchlauf unverändert ("passt, anzahlen ändern sich nicht"). -
— am 2026-09-16 live an.id-Positions-Überlagerung (fetchMenuItems) auf weitere Menüs prüfen/ip address//ip route/erneut/ip dhcp-server leasegeprüft, keine erneute Fehlzuordnung reproduzierbar (siehe "Bekannte Einschränkungen" oben für Details + Timing-Race-Hypothese für dynamische Menüs). Damit fürs Erste ausreichend untersucht — echte Behebung (z.B.fetchMenuItemsauf Einzel-Lookup statt Bulk-find+Positions-Zip umstellen) bleibt offen, aber ohne aktuell reproduzierbaren Schadensfall nicht dringend. -
Router wurde zwischen M12 und M13 vom Nutzer komplett auf Werkseinstellungen zurückgesetzt ("damit wir sauber weitermachen können"), im Zuge des ersten M13-Testlaufs (Bug 18, Login-Sperre) nochmal per Hardware-Reset, und am 2026-09-15 erneut vom Nutzer manuell auf Werkseinstellungen zurückgesetzt ("ich räume den Router mal auf - werksreset", danach bestätigt: "ist durch, funktioniert"). Jeder Config-Stand aus früheren Sessions (VLANs, Test-Pools/-DHCP- Server, Interface-Umbenennungen wie
ether51, angelegte Firewall- Regeln/Routen, etc.) existiert dadurch nicht mehr — nicht mehr relevant, nicht danach suchen. Vor Annahmen über den aktuellen Stand immer neu per Geräte-/Übersicht-/Verbinden-Tab prüfen statt auf ältere Einträge hier zu vertrauen. -
— am 2026-09-16 live an 12 Singleton-Menüs geprüft (fetchMenuItemss Output-Text-Fallback (Bug 20) auf weitere Singleton-Menüs prüfen/ip dns,/system identity,/system clock,/system ntp client,/tool e-mail,/system routerboard,/system resource,/system note,/system watchdog,/ip settings,/ip cloud,/interface bridge settings): alle melden exakt denselben Text"bad parameter terse (line 1 column N)"(Spaltennummer variiert, Substring immer gleich) — der bestehende Checkoutput.contains("bad parameter terse")fängt sie alle ab, keine dritte, unbehandelte Fehlerform gefunden. Nebenbefund:/system healthist kein Singleton, sondern liefert überterseeine echte Liste (mehrere Sensor-Zeilen, z.B. Spannung/ Temperatur) — falls das je kuratiert wird, nicht alsisSingletonmarkieren. Damit fürs Erste ausreichend untersucht, kein Codefix nötig. -
Ausführlichere Tooltips im Experte-Tab— erledigt: alleRouterOSFieldSchema.help-Texte inRouterOSSchemaCatalog.swiftüberarbeitet, jedes Adress-/Netz-/Bereichs-Feld hat jetzt ein konkretes Beispiel (z.B. "192.168.88.1/24" oder "192.168.88.10-192.168.88.254"), vorher leerehelp: ""-Texte gefüllt (u.a. bei Mangle/Raw-Regeln, den meisten "Deaktiviert"- Feldern, WireGuard-Peer-Feldern, System-/Tool-Menüs). Dabei zwei weitere Nutzerwünsche ergänzt: eine fette Überschrift (Menüname + Kategorie) oben inExpertMenuDetailView, da vorher nicht erkennbar war, in welcher Sektion man sich befindet; und eine hellblaue Hervorhebung des gerade geöffneten Eintrags in der linken Liste (ExpertView) — dafür.listRowBackgroundauf macOS' Sidebar-Stil verworfen (wird vom System überschrieben/nicht sichtbar), stattdessen Hintergrund direkt am Zeileninhalt (TextinRoundedRectangle- Fill) gesetzt. Alles vom Nutzer live in Xcode bestätigt ("passt gut"). -
M15-Bearbeiten-Funktion deckt Interfaces bewusst nicht ab— erledigt: jedes Interface hat jetzt eineditTarget— VLAN- Interfaces über das kuratierte/interface vlan-Schema (mit der eigenen.idaus/interface vlan, nicht der generischen/interface-Liste), alle anderen Typen (Ethernet, Bridge, WLAN, WireGuard) generisch über/interface. Dabei live einen echten Stolperstein gefunden: der Nutzer wollteether5zuether51umbenennen, hat aber (da für/interfacebisher gar kein kuratiertes Schema existierte, alles landete in "Weitere Parameter") versehentlichdefault-namestattnamegeändert — "bad parameter default-name".default-nameist RouterOS' Werksname des Ports, nie änderbar;namedas eigentliche, umbenennbare Feld, beide standen unbeschriftet nebeneinander. Fix:/interfacejetzt mit kuratiertem Schema (name/comment/disabled), Tooltip aufnameweist explizit auf die Verwechslungsgefahr mitdefault-namehin. Live bestätigt ("funktioniert"). Adress-Listen-Knoten bleiben bewusst weiter nicht editierbar — fassen mehrere Einträge in einem Knoten zusammen, kein 1:1-Bezug zu einem Router-Item, bräuchte eine andere UI (Liste statt Formular). -
Übersicht-Tab: Interface-Klick hebt jetzt transitiv alle zusammenhängenden Linien hervor, nicht nur die direkt anliegenden— Nutzeridee: "wenn ich einen Interface-Button klicke, möchte ich ALLE Linien sehen, die mit dem Interface irgendwie zusammenhängen". Bisher galt für jeden Knotentyp dieselbe 1-Hop-Regel (nur Kanten, die den geklickten Knoten selbst berühren) — bei einem Interface reißt das z.B. die Kette "DHCP-Server → sein Pool" oder "IP-Adresse → DHCP-Netzwerk-Optionen" ab, da diese Kanten nicht am Interface selbst hängen. Fix:OverviewGraph.highlightedNodeIDs(startingAt:)— für jeden Knotentyp außer Interface unverändert nur der eine Knoten selbst, für ein Interface eine Breitensuche (BFS) über alle Kanten in beide Richtungen, die den kompletten zusammenhängenden Ast einsammelt (Interface → Adresse/DHCP-Server/Route/Firewall-Regel, und von dort weiter → Pool bzw. DHCP-Netzwerk). Ausdrücklich nur für Interfaces so gewollt — alle anderen Knotentypen bleiben bei der bisherigen 1-Hop- Regel. Da ich die laufende App-GUI nicht selbst anklicken kann, die BFS-Logik stattdessen isoliert inOverviewGraphTests.swiftgetestet (testHighlightedNodeIDsExpandsTransitivelyOnlyForInterfaces): baut einen Graphen mit Interface→Adresse→DHCP-Netzwerk und Interface→DHCP-Server→Pool auf, bestätigt zunächst per Sanity-Check, dass Pool und DHCP-Netzwerk tatsächlich zwei Hops entfernt sind (kein direkter Edge zum Interface), und prüft dann, dasshighlightedNodeIDs(startingAt:)vom Interface aus alle fünf Knoten liefert, aber vom Pool oder von der Adresse aus nur sich selbst. 61 Tests grün (60 + 1 neu). Visuelle Live-Prüfung im laufenden Diagramm (2026-09-16): Interface-Klick (bridge) hebt die komplette zusammenhängende Kette hervor (inkl. Pool und DHCP-Netzwerk-Optionen zwei Hops entfernt), Klick auf Pool/Adresse bleibt bei der 1-Hop-Regel — beides wie erwartet bestätigt ("passt, live bestätigt"). -
Dedizierte SSH-Dienste scheitern still bei SSH-Host-Key-Mismatch, solange REST verbindet (siehe M24-Nebenbefund oben) —
BackupService/NetworkToolsService/InterfaceTrafficMonitor/UpdateService/FactoryResetServicehaben keinen eigenen UI-Bestätigungspfad für einen neuen SSH-Host-Key, nurConnectionService.connects SSH-Fallback zeigt den Dialog — und der wird nie erreicht, solange REST erfolgreich verbindet. Mögliche Fixes: diese Dienste beiuntrustedSSHHostKeyeinen eigenen Bestätigungsdialog zeigen lassen, oderConnectionServicebeim Verbinden zusätzlich (nicht nur im Fallback-Fall) einmal den SSH-Host-Key prüfen/bestätigen lassen, unabhängig davon, ob REST erfolgreich war. Tatsächlich so eingetreten (2026-09-16): beim ersten Experte-Tab-Schreibversuch einer neuen Sitzung (ensureSessionBackup, läuft einmal pro Verbindung vor der ersten Änderung) schlugBackupServices eigene, dedizierte SSH-Verbindung mit "Unbekannter SSH-Schlüssel" fehl — sichtbar nur als roterapplyError-Text im Experte-Formular, ohne erkennbaren Zusammenhang zum eigentlichen SSH-Host-Key-Problem. Der erste Trust-Fix-Versuch von vorhin (M24-Nebenbefund) hatte sich entgegen der Live-Bestätigung des Nutzers ("vertraut, verbunden per SSH") nicht tatsächlich persistiert —defaults read com.focus72.RouterOSAssistant RouterOSAssistant.TrustedSSHHostKeyFingerprintszeigte einen komplett fehlenden Schlüssel, obwohl der strukturell identische, nachweislich funktionierende REST-Zertifikat-Trust (TrustedCertificateFingerprints) im selben Preferences-Bereich korrekt vorhanden war. Ursache dafür bleibt ungeklärt — der Code-Pfad (ConnectViewModel.trustSSHHostKeyAndRetry→ConnectionService.trustCurrentSSHHostKeyAndRetry→SSHHostKeyTrustStore.trust) ist exakt symmetrisch zum funktionierenden Zertifikat-Pfad, kein Unterschied im Code gefunden. Fix diesmal perwww-sslerneut kurz deaktiviert, Trust-Dialog erneut bestätigt, und diesmal perdefaults readdirekt verifiziert, dass der Fingerprint wirklich persistiert wurde (war er) — DHCP-Netzwerk-Anlage danach live erfolgreich. Falls dieses Symptom nochmal auftritt: zuerst perdefaults readprüfen, ob der Trust wirklich gespeichert wurde, nicht nur der UI-Bestätigung vertrauen. -
M26 (Seriennummer bei "Bekannte Router") noch live testen— erledigt, siehe M27 oben (dabei zwei weitere Bugs gefunden und gefixt: geteilter Schlüsselbund-Eintrag,/system routerboardsterse-Ablehnung mit anderem Fehlertext als bisher erkannt).
Gitea-Remote origin ist eingerichtet und wird laufend gepusht (siehe
oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15.
/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.