From 0e8d9103525ad7cb1f4e81af15d76521dfdd90dc Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 14 Sep 2026 20:43:56 +0200 Subject: [PATCH] Wizard: idempotenter WAN-Apply fuer DHCP-Client/PPPoE (Bug 21) Wizard erneut auf einem Interface durchlaufen, das schon einen DHCP-Client hat (hier: aus einer zurueckgespielten Sicherung), scheiterte live mit "failure: dhcp-client on that interface already exists" - WanConfig.buildCommands() erzeugt fuer DHCP-Client/PPPoE immer .add, ohne vorher zu pruefen, ob auf dem Interface schon einer existiert. Fix: SetupViewModel.applyIdempotently faengt einen .add-Fehlschlag auf /ip dhcp-client bzw. /interface pppoe-client ab und wiederholt ihn als .set (nach "interface" gematcht) - bewusst nur fuer diese zwei Menues mit "maximal ein Eintrag pro Interface"-Semantik, nicht generell fuer jedes .add (z.B. /ip address erlaubt legitim mehrere Adressen pro Interface). Gefunden beim Versuch, M8 (Netzwerk-Isolation) live durchzutesten - dieser Test selbst ist noch nicht abgeschlossen, naechste Session dort weitermachen (siehe HANDOFF.md Naechste Schritte Punkt 1). HANDOFF.md/CHATLOG.md aktualisiert: Bug 21, Sessionende-Stand. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ --- CHATLOG.md | 20 ++++++++++ HANDOFF.md | 39 ++++++++++++++++--- .../Wizard/Steps/Setup/SetupViewModel.swift | 34 +++++++++++++++- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/CHATLOG.md b/CHATLOG.md index 0933225..3f44cdc 100644 --- a/CHATLOG.md +++ b/CHATLOG.md @@ -676,3 +676,23 @@ Test, WLAN/Bonding/PPPoE-Live-Tests). - "ja, committen und beides aktualisieren" → dieser Eintrag, HANDOFF.md (M14, Bug 20, Design-Durchgang, Dark-Mode-Bestätigung) aktualisiert, Commit erstellt. + +## Session: M8-Hardware-Test begonnen (nicht abgeschlossen) + +- "was steht als nächstes auf dem Plan?" → HANDOFF-Liste zusammengefasst. + "fangen wir bei 4 an" (M8 Netzwerk-Isolation) → per `AskUserQuestion` + geklärt: voller Wizard-Durchlauf statt nur gezieltem Firewall-Test, + kombiniert damit Punkt 1 (kompletter Wizard-Durchlauf) mit. +- Unterwegs zwei praktische RouterOS-Fragen beantwortet: ether5 aus der + Bridge entfernen (`/interface bridge port remove [find + interface=ether5]` bzw. Experte-Tab), dann "Unerwartete Antwort vom + Router: failure: dhcp-client on that interface already exists" beim + WAN-Schritt — Ursache: Wizard nutzt immer `.add`, aber auf dem + Interface existierte aus der zurückgespielten Sicherung schon ein + DHCP-Client. "fixe es gleich" → Bug 21, `SetupViewModel. + applyIdempotently` (Fallback auf `.set` bei `.add`-Fehlschlag auf + DHCP-Client/PPPoE), deployt. +- "ok, alles speichern, genug für heute" → dieser Eintrag, HANDOFF.md + (Bug 21, Nächste-Schritte Punkt 1 als "in Arbeit" markiert — der + eigentliche M8-Isolationstest ist noch nicht zu Ende geführt, nächste + Session dort fortsetzen) aktualisiert, Commit erstellt. Sitzung beendet. diff --git a/HANDOFF.md b/HANDOFF.md index d7d39d1..5333861 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -56,6 +56,17 @@ Farbwerte, alles adaptive System-Farben) — vom Nutzer live bestätigt. Release-Build unter `/Applications/RouterOS Assistant.app` ist auf aktuellem Stand. +**Zum Sessionende:** M8-Hardware-Test (Netzwerk-Isolation) begonnen — +voller Wizard-Durchlauf über den Experte-Modus, WAN neu einrichten. +Dabei Bug 21 gefunden: der Wizard-Apply nutzte für WAN-DHCP-Client/PPPoE +immer `.add`, was auf einem bereits konfigurierten Interface (hier: aus +der zurückgespielten Sicherung) mit "failure: dhcp-client on that +interface already exists" scheiterte. Fix: `SetupViewModel. +applyIdempotently` versucht bei `.add` auf `/ip dhcp-client` oder +`/interface pppoe-client` nach einem Fehlschlag automatisch `.set` +(nach "interface" gematcht) — deployt, **aber der eigentliche +M8-Isolationstest selbst ist noch nicht zu Ende geführt/verifiziert**, +das ist der erste Schritt für die nächste Session. ## Ziel Native macOS-App (SwiftUI), die Laien per geführtem Interview-Wizard durch @@ -378,6 +389,19 @@ erreichen (siehe Bug 1 unten). Singleton-Menü, nicht nur `/system routerboard` — der generische Lese-Pfad wird von Übersicht-, Geräte- und Experte-Tab gemeinsam genutzt. +21. **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.applyIdempotently` fängt einen `.add`-Fehlschlag auf + `/ip dhcp-client`/`/interface pppoe-client` ab 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 address` erlaubt 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 @@ -741,11 +765,16 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server ## Nächste Schritte -1. Den kompletten Wizard einmal neu durchlaufen (WAN/LAN/VLAN/WLAN/ - Firewall) auf dem werksresetteten hEX und dabei das Firewall-Ergebnis - mit korrektem WAN-Port per `/ip firewall filter print` / - `/ip firewall nat print` kontrollieren (bisher nur die Interface- - Auswahl nach dem Parser-Fix bestätigt, kein erneuter Apply). +1. **In Arbeit, hier weitermachen:** kompletter Wizard-Durchlauf + (WAN/LAN mit zwei Interfaces ether2+ether3, eins isoliert/Firewall) + über den Experte-Modus — kombiniert M8-Hardware-Test (Netzwerk- + Isolation) mit diesem Punkt. Wurde in dieser Session begonnen, brach + beim WAN-Schritt mit Bug 21 ab (jetzt gefixt, deployt, aber + **ungetestet**). Nach erneutem Durchlauf: Firewall-Ergebnis mit + korrektem WAN-Port UND die Isolations-Regeln per + `/ip firewall filter print` kontrollieren, plus praktisch testen + (iMac ↔ Laptop sollten sich nicht erreichen, Internet für beide + schon). 2. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen. 3. Rest von M7: Fehlerzustände/Politur, REST-Schreibtest an einem Gerät mit aktivem `www-ssl`. diff --git a/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupViewModel.swift b/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupViewModel.swift index 68b18d4..703e04e 100644 --- a/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupViewModel.swift +++ b/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupViewModel.swift @@ -213,7 +213,7 @@ final class SetupViewModel: ObservableObject { for command in plannedCommands { applyLog.append(command.summary) - try await connectionService.apply(command) + try await applyIdempotently(command) } didApplySuccessfully = true @@ -223,4 +223,36 @@ final class SetupViewModel: ObservableObject { isApplying = false } } + + /// RouterOS allows at most one item per interface on some menus (a DHCP client, a PPPoE + /// client) — re-running the wizard's WAN step on an interface that already has one makes the + /// wizard's own `.add` fail live-confirmed: "failure: dhcp-client on that interface already + /// exists". Rather than hard-fail the whole apply here, retry once as a `.set` matched by + /// "interface" instead — reconfigures the existing entry in place, which is what re-running + /// the wizard on the same interface should mean anyway. Scoped to exactly these two + /// known-unique-per-interface menus, not applied generally to every `.add` — other menus + /// (e.g. "/ip address") legitimately allow multiple items per interface, where silently + /// converting a would-be-duplicate `.add` into a `.set` would be wrong, not helpful. + private static let interfaceUniqueMenuPaths: Set = ["/ip dhcp-client", "/interface pppoe-client"] + + private func applyIdempotently(_ command: RouterOSCommand) async throws { + do { + try await connectionService.apply(command) + } catch { + guard case .add = command.operation, + Self.interfaceUniqueMenuPaths.contains(command.menuPath), + let interfaceName = command.arguments["interface"] else { + throw error + } + let retryCommand = RouterOSCommand.set( + menuPath: command.menuPath, + restPath: command.restPath, + matchField: "interface", + matchValue: interfaceName, + arguments: command.arguments, + summary: command.summary + ) + try await connectionService.apply(retryCommand) + } + } }