From 8e99ab8e410bb6d4eb76eccb1f9555f671e09d87 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 14 Sep 2026 14:11:34 +0200 Subject: [PATCH] =?UTF-8?q?Ger=C3=A4te-Tab:=20"Feste=20Zuweisung=20entfern?= =?UTF-8?q?en"=20(R=C3=BCckweg=20zu=20dynamisch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RouterOS kennt kein make-dynamic als Umkehrung von make-static (per Recherche-Agent gegen die offizielle DHCP-Doku bestätigt: nur check-status/make-static/send-reconfigure existieren) — der offizielle Weg ist, die statische Lease zu entfernen; das Gerät bekommt beim naechsten Verbindungsaufbau automatisch wieder eine dynamische Adresse, moeglicherweise eine andere IP als zuvor. Neuer Kontextmenü-Eintrag bei fest zugewiesenen Geräten im Geräte-Tab, mit Bestätigungsdialog (erklärt den Ablauf) und Nachkontrolle, dass der Lease-Eintrag wirklich entfernt wurde, bevor Erfolg gemeldet wird - gleiche Vorsicht wie beim bestehenden "Feste IP zuweisen". Vom Nutzer live bestätigt (fest zuweisen -> entfernen -> Kabel/WLAN neu verbinden -> wieder dynamisch), kein neuer Bug diesmal. HANDOFF.md/CHATLOG.md aktualisiert: "Zurück auf dynamisch" aus den offenen Punkten entfernt, M12-Beschreibung ergänzt. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ --- CHATLOG.md | 38 +++++++++++-- HANDOFF.md | 54 +++++++++---------- .../Features/Devices/DevicesView.swift | 35 ++++++++++-- .../Features/Devices/DevicesViewModel.swift | 54 ++++++++++++++++++- 4 files changed, 145 insertions(+), 36 deletions(-) diff --git a/CHATLOG.md b/CHATLOG.md index afd8714..692c5d3 100644 --- a/CHATLOG.md +++ b/CHATLOG.md @@ -523,7 +523,37 @@ falsche Negative. Alle vier zusammen ergaben eine ungewöhnlich lange, mehrstufige Fehlersuche mit wiederholten Rohdaten-Abgleichen und direkten Router-Gegenchecks (SSH-CLI + Winbox) durch den Nutzer. Commit für M11 separat (`10f30a8`), M12 + HANDOFF/CHATLOG-Update in diesem Commit. -Offen: "Zurück auf dynamisch"-Aktion fehlt noch im Geräte-Tab; ob dieselbe -`.id`-Überlagerung auch im Experte-Tab bei anderen Menüs falsch zuordnen -kann, ist ungeprüft; alle bereits vorher offenen Punkte unverändert -(siehe HANDOFF.md Nächste Schritte). +Offen (Stand direkt nach M12): "Zurück auf dynamisch"-Aktion fehlt noch +im Geräte-Tab; ob dieselbe `.id`-Überlagerung auch im Experte-Tab bei +anderen Menüs falsch zuordnen kann, ist ungeprüft; alle bereits vorher +offenen Punkte unverändert (siehe HANDOFF.md Nächste Schritte). + +## Nachtrag: "Feste Zuweisung entfernen" + +- "jetzt Statisch IP zurück in dynamische IP. das scheint nicht so + einfach zu funktionieren. Meine Recherchen ergeben: man muss erst die + statische IP löschen, dann den client disconnecten und dann wieder + connecten. ... kannst du das bestätigen? ... gib mir ein + Lösungsvorschlag" → per Recherche-Agent gegen die offizielle + RouterOS-DHCP-Doku geprüft: bestätigt, kein `make-dynamic`-Befehl + existiert (nur `check-status`/`make-static`/`send-reconfigure`), der + offizielle Weg ist `remove` der Lease, Client muss selbst neu anfragen + (Kabel/WLAN trennen+verbinden, Neustart, oder eigener Renewal-Timer), + keine Garantie auf dieselbe IP danach. Lösungsvorschlag vorgelegt: neuer + Kontextmenü-Eintrag "Feste Zuweisung entfernen" bei festen Geräten + (`.remove` über MAC-Adresse, Bestätigungsdialog mit diesen Hinweisen, + Nachkontrolle dass der Eintrag wirklich weg ist). +- "ja, bau das so" → `DevicesViewModel.pendingStaticRemoval`/ + `pendingRemovalCommand`/`confirmStaticRemoval`/`cancelStaticRemoval`, + zweiter `.confirmationDialog` in `DevicesView` (gleiches + Entkopplungs-Muster wie beim Zuweisen-Dialog, siehe Bug 16). Build+Tests + grün, Release neu gebaut+deployt. +- "das hat sofort funktioniert, habe die feste ip gelöscht und den client + danach disconnectet und wieder connected, alles ok. ip ist wieder + dynamisch. lassen wir so" — kein neuer Bug, erster Versuch erfolgreich, + kompletter Kreislauf (fest zuweisen → entfernen → neu verbinden → + dynamisch) vom Nutzer selbst bestätigt. +- "ja, committen und beides aktualisieren" → dieser Eintrag, HANDOFF.md + (M12-Beschreibung/Nächste-Schritte/Bekannte-Einschränkungen aktualisiert, + "Zurück auf dynamisch" als erledigt markiert) aktualisiert, Commit + erstellt. diff --git a/HANDOFF.md b/HANDOFF.md index 82a58b2..033e7da 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -21,13 +21,16 @@ farblich nach Verbindungsart getrennt, Hover/Klick hebt Zusammenhänge hervor). **M12 "Geräte"** ist ein LAN-Scanner (DHCP-Leases + ARP + Bridge-Host-Tabelle, gruppiert als eine Tabelle pro physischem Port) mit Rechtsklick-Aktion "Feste IP zuweisen" (RouterOS' "Make Static", per -`/ip dhcp-server lease make-static`). Beide Tabs live gegen den -hEX-Testrouter verifiziert — M12 dabei besonders gründlich, weil die -Fest/Dynamisch-Erkennung und die Zuweisung selbst sich als überraschend -zäh erwiesen: **vier** echte Bugs gefunden und gefixt (Bug 14–17 unten), -am Ende vom Nutzer selbst bestätigt ("das funktioniert jetzt super"). -Release-Build unter `/Applications/RouterOS Assistant.app` ist auf -aktuellem Stand. +`/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). Beide Tabs live gegen den hEX-Testrouter verifiziert — M12 +dabei besonders gründlich, weil die Fest/Dynamisch-Erkennung und die +Zuweisung selbst sich als überraschend zäh erwiesen: **vier** echte Bugs +gefunden und gefixt (Bug 14–17 unten), am Ende vom Nutzer selbst +bestätigt ("das funktioniert jetzt super", und für den Rückweg "das hat +sofort funktioniert"). Release-Build unter +`/Applications/RouterOS Assistant.app` ist auf aktuellem Stand. ## Ziel @@ -103,7 +106,7 @@ RouterOSAssistant/ 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) + 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) @@ -377,9 +380,6 @@ wiederholen. (M12/Bug 14) ist zusätzlich auf einer nirgends offiziell dokumentierten Annahme aufgebaut (Query-String-Filter `?feld=wert` auf einem GET) — komplett unverifiziert. -- **"Zurück auf dynamisch" fehlt im Geräte-Tab (M12)** — es gibt nur - "Feste IP zuweisen" (Winbox' "Make Static"), keine Umkehrung. Kein Bug, - einfach noch nicht gebaut; Nutzer hat danach gefragt. - **`fetchMenuItems`s `.id`-Positions-Überlagerung: Verlässlichkeit für andere Menüs ungeprüft** (siehe Bug 15) — bei `/ip dhcp-server lease` live als falsch bestätigt (`.id` landete auf der falschen Zeile), für @@ -600,13 +600,18 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server System/Werkzeuge/Mangle/Raw (siehe `OverviewGraph.unmappedAreas`), weiterhin nur über den Experte-Tab erreichbar. - ✅ M12: Geräte-Tab (LAN-Scanner: DHCP-Leases+ARP+Bridge-Host, eine - Tabelle pro physischem Port, "Feste IP zuweisen" für dynamische Leases) - — **live gegen Hardware verifiziert und vom Nutzer bestätigt - funktionierend** ("das funktioniert jetzt super"), nach vier gefundenen - und gefixten Bugs (Bug 14–17, siehe oben) — die aufwendigste + 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). - Fehlt noch: "Zurück auf dynamisch" (siehe Bekannte Einschränkungen). + Rückweg ("Feste Zuweisung entfernen") lief dagegen beim ersten Versuch + fehlerfrei — RouterOS-Doku vorher per Recherche-Agent geprüft (kein + `make-dynamic`, nur `remove` + Client muss neu verbinden), nicht + geraten. Nutzer bestätigte den vollen Kreislauf (fest zuweisen → + entfernen → Kabel/WLAN trennen+verbinden → wieder dynamisch). ## Nächste Schritte @@ -649,25 +654,20 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server "Wiederherstellen"-Button pro Eintrag im Sicherungen-Tab (`BackupListView`) mit Bestätigungsdialog (ähnlich der bestehenden Werksreset-Gefahrenzone). -11. **Geräte-Tab: "Zurück auf dynamisch"-Aktion bauen** — Gegenstück zu - "Feste IP zuweisen". RouterOS-Weg dafür noch nicht recherchiert/live - geprüft (vermutlich `remove` der Lease und Neuanlage, oder ein - eigenes Menü-Kommando wie bei `make-static` — vor dem Bauen klären, - nicht raten, siehe Bug 17 als Warnung). -12. **`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs +11. **`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs prüfen** — live als falsch bestätigt für `/ip dhcp-server lease` (Bug 15). Betrifft potenziell jedes `.set`/`.remove` im Experte-Tab. Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig vorhandenen Einträgen `:put [ find]` und ` print terse` unabhängig ausführen und die Reihenfolgen von Hand vergleichen. -13. Optional: den neuen Dauer-Editor auch für den Geräte-Tab nutzen, falls +12. Optional: den neuen Dauer-Editor auch für den Geräte-Tab nutzen, falls dort künftig Zeitfelder (z.B. eine Lease-Zeit-Anzeige) dazukommen. -14. `Kay-Uwes-iMac` (ether2, `defconf`) und der Test-Laptop `DEDELLB2M6GK3` +13. `Kay-Uwes-iMac` (ether2, `defconf`) und der Test-Laptop `DEDELLB2M6GK3` (ether3, `dhcp5`/`defconf` je nach Testrunde) sind **echte Geräte des Nutzers**, mit denen M12 live durchgetestet wurde — keine - Test-Artefakte, nicht aufräumen. Der Laptop hatte am Ende der Session - testweise eine feste IP zugewiesen bekommen; ob das der gewünschte - Dauerzustand ist oder nur zum Testen war, mit dem Nutzer klären. + Test-Artefakte, nicht aufräumen. Beide Geräte am Ende der Session + wieder dynamisch (kompletter Kreislauf fest→entfernt→dynamisch vom + Nutzer selbst am Laptop bestätigt). Kein Gitea-Remote vorhanden — falls der Nutzer später eine Gitea-Instanz aufsetzt (z.B. selbst gehostet auf der vorhandenen OMV-NAS, siehe diff --git a/RouterOSAssistant/Features/Devices/DevicesView.swift b/RouterOSAssistant/Features/Devices/DevicesView.swift index aeeb37f..088a204 100644 --- a/RouterOSAssistant/Features/Devices/DevicesView.swift +++ b/RouterOSAssistant/Features/Devices/DevicesView.swift @@ -1,8 +1,8 @@ import SwiftUI /// "Geräte" tab: LAN scanner — one table per physical Ethernet/WLAN port, each listing the -/// devices resolved onto it (name/IP/MAC/status), plus a right-click action to give a device a -/// permanent static IP the same way Winbox's "Make Static" does. +/// devices resolved onto it (name/IP/MAC/status), plus right-click actions to give a device a +/// permanent static IP (Winbox's "Make Static") or remove that reservation again. struct DevicesView: View { @ObservedObject var connectionService: ConnectionService @StateObject private var viewModel: DevicesViewModel @@ -16,6 +16,8 @@ struct DevicesView: View { /// dialogs elsewhere (BackupListView's factory-reset dialog): the dialog's own visibility and /// its payload are two separate pieces of state. @State private var showStaticConfirmation = false + /// Same reasoning as `showStaticConfirmation` — independent of `pendingStaticRemoval`. + @State private var showRemovalConfirmation = false init(connectionService: ConnectionService) { self.connectionService = connectionService @@ -105,6 +107,28 @@ struct DevicesView: View { ) } } + .confirmationDialog( + "Feste Zuweisung entfernen?", + isPresented: $showRemovalConfirmation, + titleVisibility: .visible + ) { + Button("Entfernen", role: .destructive) { + Task { await viewModel.confirmStaticRemoval() } + } + Button("Abbrechen", role: .cancel) { viewModel.cancelStaticRemoval() } + } message: { + if let device = viewModel.pendingStaticRemoval { + Text( + "Die feste Zuweisung für \(device.macAddress)" + + (device.hostName.map { " (\($0))" } ?? "") + + " wird entfernt. RouterOS kennt keine direkte Umkehrung von \"Make Static\" —" + + " das Gerät bekommt erst beim nächsten Verbindungsaufbau (Kabel/WLAN neu" + + " verbinden, Neustart) automatisch wieder eine Adresse per DHCP, eventuell" + + " eine andere IP als bisher." + + "\n\nBefehl: \(viewModel.pendingRemovalCommand?.cliLine ?? "")" + ) + } + } .alert( "Fehler", isPresented: Binding( @@ -127,7 +151,12 @@ struct DevicesView: View { private func deviceMenu(for device: LanDevice) -> some View { if device.hasLease { if device.isStatic { - Text("Bereits fest zugewiesen") + Button { + viewModel.pendingStaticRemoval = device + showRemovalConfirmation = true + } label: { + Label("Feste Zuweisung entfernen", systemImage: "pin.slash") + } } else { Button { viewModel.pendingStaticAssignment = device diff --git a/RouterOSAssistant/Features/Devices/DevicesViewModel.swift b/RouterOSAssistant/Features/Devices/DevicesViewModel.swift index 8c9177e..11e0f1f 100644 --- a/RouterOSAssistant/Features/Devices/DevicesViewModel.swift +++ b/RouterOSAssistant/Features/Devices/DevicesViewModel.swift @@ -2,8 +2,9 @@ import Foundation /// Builds the "Geräte" tab's LAN device list from a live router — DHCP leases enriched with the /// physical port each device was learned on (ARP + bridge host table) — grouped into one table -/// per physical port, and drives the one write action this tab offers: converting a dynamic -/// lease to a permanent static one. +/// per physical port, and drives the two write actions this tab offers: converting a dynamic +/// lease to a permanent static one, and removing a static lease again (RouterOS has no reverse +/// of "make-static", so "back to dynamic" is just removal — see `pendingStaticRemoval`). @MainActor final class DevicesViewModel: ObservableObject { @Published private(set) var devices: [LanDevice] = [] @@ -12,6 +13,12 @@ final class DevicesViewModel: ObservableObject { @Published private(set) var loadError: String? @Published var pendingStaticAssignment: LanDevice? + /// The device whose static reservation the user is about to remove — RouterOS has no + /// "make-dynamic" (confirmed against the official DHCP docs: only check-status, make-static, + /// send-reconfigure exist for this menu), so "back to dynamic" is genuinely just removing the + /// static lease; the device gets a fresh dynamic lease on its own next DHCP request, not + /// immediately or necessarily on the same IP. + @Published var pendingStaticRemoval: LanDevice? @Published private(set) var isApplying = false @Published private(set) var applyError: String? @@ -76,6 +83,17 @@ final class DevicesViewModel: ObservableObject { ) } + /// The command "Feste Zuweisung entfernen" would run — matched by MAC, same reasoning as + /// `pendingCommand`. + var pendingRemovalCommand: RouterOSCommand? { + guard let device = pendingStaticRemoval, device.hasLease else { return nil } + return .remove( + menuPath: "/ip dhcp-server lease", restPath: "ip/dhcp-server/lease", + matchField: "mac-address", matchValue: device.macAddress, + summary: "Feste Zuweisung entfernen für \(device.macAddress)" + ) + } + /// Backs up once per connection before this tab's first write — same shared flag the Expert /// tool uses (`ConnectionService.hasExpertToolBackedUpThisSession`), so a session that already /// backed up via one "power tool" doesn't back up again via the other. @@ -125,6 +143,38 @@ final class DevicesViewModel: ObservableObject { pendingStaticAssignment = nil } + func confirmStaticRemoval() async { + guard let command = pendingRemovalCommand, let device = pendingStaticRemoval else { return } + isApplying = true + applyError = nil + do { + try await ensureSessionBackup() + try await connectionService.apply(command) + // Same lesson as `confirmStaticAssignment` (Bug 10/17 in HANDOFF.md): empty output + // isn't proof it worked. Verify the lease for this MAC is actually gone — a lingering + // "no error" success is worth less than an honest check. + let remainingLeases = try await connectionService.fetchMenuItems( + menuPath: "/ip dhcp-server lease", restPath: "ip/dhcp-server/lease" + ) + let stillPresent = remainingLeases.contains { $0.fields["mac-address"]?.lowercased() == device.macAddress.lowercased() } + guard !stillPresent else { + throw RouterOSError.invalidResponse( + "Befehl lief ohne Fehlermeldung, aber der Lease-Eintrag ist weiterhin vorhanden. Bitte manuell mit \"/ip dhcp-server lease print\" kontrollieren." + ) + } + pendingStaticRemoval = nil + await load() + } catch { + pendingStaticRemoval = nil + applyError = error.localizedDescription + } + isApplying = false + } + + func cancelStaticRemoval() { + pendingStaticRemoval = nil + } + func dismissApplyError() { applyError = nil }