From 2e507aa9efcd6b6826c143e041bd468a28905508 Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 15 Sep 2026 15:10:55 +0200 Subject: [PATCH] =?UTF-8?q?M15=20erweitert:=20Interfaces=20direkt=20editie?= =?UTF-8?q?rbar=20(=C3=9Cbersicht=20+=20Experte-Tab)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jedes Interface bekommt jetzt ein editTarget — VLAN-Interfaces über das kuratierte /interface vlan-Schema (mit der eigenen .id aus /interface vlan, nicht der generischen /interface-Liste), alle anderen Typen (Ethernet, Bridge, WLAN, WireGuard) generisch über /interface. Dabei live einen echten Stolperstein gefunden: für /interface gab es bisher kein kuratiertes Schema, alle Felder landeten unbeschriftet in "Weitere Parameter" — beim Versuch, ether5 zu ether51 umzubenennen, wurde versehentlich default-name (RouterOS' Werksname, nie änderbar) statt name geändert ("bad parameter default-name"). Fix: /interface jetzt mit kuratiertem Schema (Name/Kommentar/Deaktiviert), Tooltip auf "Name" warnt explizit vor der Verwechslung mit default-name. Adress-Listen-Knoten bleiben bewusst weiter nicht editierbar (fassen mehrere Einträge zusammen, bräuchten eine andere UI-Form). Live bestätigt ("funktioniert"). 60 Unit-Tests grün. Manual.md/ HANDOFF.md aktualisiert. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG --- CHATLOG.md | 12 ++++++++ HANDOFF.md | 29 ++++++++++++------- Manual.md | 22 +++++++------- .../Core/Models/RouterOSSchemaCatalog.swift | 16 ++++++++++ .../Features/Overview/OverviewViewModel.swift | 20 +++++++++++-- .../OverviewGraphTests.swift | 23 +++++++++++++++ 6 files changed, 99 insertions(+), 23 deletions(-) diff --git a/CHATLOG.md b/CHATLOG.md index faaa1bf..787b471 100644 --- a/CHATLOG.md +++ b/CHATLOG.md @@ -802,3 +802,15 @@ Test, WLAN/Bonding/PPPoE-Live-Tests). Tab-Inhalte auf macOS beim Wechsel aber nicht. Fix: `.onAppear` statt `.task`, feuert bei jedem Tab-Besuch neu. 59 Unit-Tests grün, HANDOFF.md aktualisiert, Commit + Push. +- "was sind die nächsten Milestones?" → offene Punkte aus HANDOFF.md + zusammengefasst, Nutzer wählte "6" (M15 auf Interfaces/Adress-Listen + erweitern). Interfaces umgesetzt: VLAN-Interfaces über kuratiertes + `/interface vlan`, alle anderen Typen generisch über `/interface`. + Live gefunden: Nutzer wollte `ether5`→`ether51` umbenennen, hat aber + `default-name` (Werksname, nie änderbar) statt `name` erwischt, da + beide unbeschriftet in "Weitere Parameter" nebeneinander standen — + "bad parameter default-name". Fix: `/interface` jetzt kuratiert + (Name/Kommentar/Deaktiviert), Tooltip warnt explizit vor der + Verwechslung. Live bestätigt ("funktioniert"). Adress-Listen bewusst + weiter zurückgestellt (andere UI-Form nötig). 60 Unit-Tests grün, + Manual.md/HANDOFF.md aktualisiert, Commit + Push. diff --git a/HANDOFF.md b/HANDOFF.md index 231ab08..13c2d45 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1070,16 +1070,25 @@ Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und Hintergrund direkt am Zeileninhalt (`Text` in `RoundedRectangle`- Fill) gesetzt. Alles vom Nutzer live in Xcode bestätigt ("passt gut"). -11. **M15-Bearbeiten-Funktion deckt Interfaces und Adress-Listen bewusst - nicht ab** — physische/VLAN/Bridge-Interfaces verteilen ihre Felder - über mehrere typ-spezifische RouterOS-Menüs (`/interface ethernet`, - `/interface bridge`, `/interface vlan`, ...), ein sauberes - `OverviewNode.EditTarget` bräuchte pro Interface-Typ eine eigene - Zuordnung statt der generischen `/interface`. Adress-Listen-Knoten - fassen mehrere Einträge in einem Knoten zusammen, kein 1:1-Bezug zu - einem Router-Item — bräuchte eine andere UI (Liste statt Formular). - Beides bei Bedarf nachrüstbar, für diese Session bewusst - zurückgestellt. +11. ~~M15-Bearbeiten-Funktion deckt Interfaces bewusst nicht ab~~ — + erledigt: jedes Interface hat jetzt ein `editTarget` — VLAN- + Interfaces über das kuratierte `/interface vlan`-Schema (mit der + eigenen `.id` aus `/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 wollte `ether5` zu `ether51` + umbenennen, hat aber (da für `/interface` bisher gar kein + kuratiertes Schema existierte, alles landete in "Weitere Parameter") + versehentlich `default-name` statt `name` geändert — + "bad parameter default-name". `default-name` ist RouterOS' Werksname + des Ports, nie änderbar; `name` das eigentliche, umbenennbare Feld, + beide standen unbeschriftet nebeneinander. Fix: `/interface` jetzt + mit kuratiertem Schema (`name`/`comment`/`disabled`), Tooltip auf + `name` weist explizit auf die Verwechslungsgefahr mit `default-name` + hin. 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). Gitea-Remote `origin` ist eingerichtet und wird laufend gepusht (siehe oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15. diff --git a/Manual.md b/Manual.md index ee7a397..5e615fc 100644 --- a/Manual.md +++ b/Manual.md @@ -95,12 +95,13 @@ Grafisches Diagramm des kompletten aktuellen Router-Zustands: Knotens ist ebenfalls klickbar (springt direkt zum verbundenen Element) und zeigt beim Hovern eine kurze Erklärung als Tooltip. - **Direktes Bearbeiten**: IP-Adressen, Pools, DHCP-Server/-Netzwerke/ - -Clients, Routen, Firewall-Filter-/NAT-Regeln und WireGuard-Peers - lassen sich anklicken und über "Bearbeiten" direkt ändern — schreibt - sofort an den Router zurück. (Physische/VLAN/Bridge-Interfaces und - Adress-Listen-Knoten sind hier noch nicht direkt editierbar, nur über - den Experte-Tab.) Dynamische/automatisch angelegte Routen (z.B. die - Verbindungsroute eines Netzwerks) zeigen bewusst keinen + -Clients, Routen, Firewall-Filter-/NAT-Regeln, WireGuard-Peers und + Interfaces (Ethernet, Bridge, WLAN, VLAN, WireGuard) lassen sich + anklicken und über "Bearbeiten" direkt ändern — schreibt sofort an den + Router zurück. (Adress-Listen-Knoten sind hier noch nicht direkt + editierbar, nur über den Experte-Tab, da ein Knoten dort mehrere + Einträge zusammenfasst.) Dynamische/automatisch angelegte Routen + (z.B. die Verbindungsroute eines Netzwerks) zeigen bewusst keinen Bearbeiten-Button, da RouterOS solche Einträge selbst verwaltet. - Aktualisiert sich automatisch, sobald du in den Tab wechselst. - Zoom-Buttons (−/100%/+) für die Diagrammgröße. @@ -219,10 +220,11 @@ Graphical diagram of the router's complete current state: clickable (jumps straight to the connected element) and shows a short explanation as a tooltip on hover. - **Direct editing**: IP addresses, pools, DHCP servers/networks/ - clients, routes, firewall filter/NAT rules and WireGuard peers can be - clicked and changed via "Edit" — writes back to the router - immediately. (Physical/VLAN/bridge interfaces and address-list nodes - aren't directly editable here yet, only through the Expert tab.) + clients, routes, firewall filter/NAT rules, WireGuard peers and + interfaces (Ethernet, bridge, WLAN, VLAN, WireGuard) can be clicked + and changed via "Edit" — writes back to the router immediately. + (Address-list nodes aren't directly editable here yet, only through + the Expert tab, since one node there folds several entries together.) Dynamic/auto-created routes (e.g. a network's own connected route) deliberately show no Edit button, since RouterOS manages those itself. - Refreshes automatically whenever you switch to this tab. diff --git a/RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift b/RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift index 99522b5..8325364 100644 --- a/RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift +++ b/RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift @@ -199,6 +199,22 @@ enum RouterOSSchemaCatalog { // MARK: - Interfaces (generic, browsing + free-form fields) private static let interfaceFamily: [RouterOSMenuSchema] = [ + RouterOSMenuSchema( + menuPath: "/interface", restPath: "interface", category: .interfaces, + displayName: "Alle Interfaces (generisch)", + summary: "Gemeinsame Felder, die für jedes Interface gelten, unabhängig vom Typ (Ethernet, Bridge, WLAN, WireGuard, VLAN, ...).", + explanation: "RouterOS listet hier alle Interfaces zusammen. Typ-spezifische Felder (z.B. die VLAN-ID eines VLAN-Interfaces) liegen in den jeweils eigenen Menüs (z.B. \"VLAN-Interfaces\") — hier nur das, was für jeden Interface-Typ gleich funktioniert.", + warning: "RouterOS liefert hier zusätzlich einen \"default-name\" (Werksname des Ports) mit — der erscheint unten bei \"Weitere Parameter\", ist aber nicht änderbar (\"bad parameter default-name\", live bestätigt). Zum Umbenennen nur das Feld \"Name\" oben verwenden.", + fields: [ + RouterOSFieldSchema(key: "name", label: "Name", kind: .text, + help: "Der aktuelle Name dieses Interfaces, z.B. ether5 oder vlan20 — nicht zu verwechseln mit \"default-name\" (Werksname, weiter unten bei \"Weitere Parameter\", nicht änderbar).", required: true), + RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, + help: "Nur zur eigenen Wiedererkennung, ohne technische Wirkung."), + RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, + help: "Interface inaktiv schalten, ohne es zu löschen.", defaultValue: "no") + ], + listColumns: ["name"] + ), RouterOSMenuSchema( menuPath: "/interface bridge", restPath: "interface/bridge", category: .interfaces, displayName: "Bridge", diff --git a/RouterOSAssistant/Features/Overview/OverviewViewModel.swift b/RouterOSAssistant/Features/Overview/OverviewViewModel.swift index 631763f..b192333 100644 --- a/RouterOSAssistant/Features/Overview/OverviewViewModel.swift +++ b/RouterOSAssistant/Features/Overview/OverviewViewModel.swift @@ -81,10 +81,10 @@ final class OverviewViewModel: ObservableObject { // Interfaces: every physical/virtual interface RouterOS reports (ethernet, vlan, bridge, // wireless, wireguard, ...). "vlan-id"/parent info is filled in below from "/interface // vlan", since the generic "/interface" listing alone doesn't carry it. - var vlanInfoByName: [String: (vlanID: String, parent: String)] = [:] + var vlanInfoByName: [String: (vlanID: String, parent: String, itemID: String)] = [:] for item in vlans { guard let name = item.fields["name"] else { continue } - vlanInfoByName[name] = (item.fields["vlan-id"] ?? "?", item.fields["interface"] ?? "") + vlanInfoByName[name] = (item.fields["vlan-id"] ?? "?", item.fields["interface"] ?? "", item.id) } for item in interfaces { @@ -98,10 +98,24 @@ final class OverviewViewModel: ObservableObject { if item.fields["disabled"] == "true" || item.fields["disabled"] == "yes" { subtitle += " · deaktiviert" } + // VLAN interfaces edit through their own curated "/interface vlan" schema (name/ + // vlan-id/interface/disabled) using that menu's own item id — every other interface + // type (ethernet, bridge, wireless, wireguard, ...) has no curated schema of its own + // here, so it falls back to the generic "/interface" menu, which does support the + // common fields (name, comment, disabled) via a plain "set", just without curated + // field labels/tooltips (same free-form editor the Experte-Tab uses for any + // uncurated menu). + let editTarget: OverviewNode.EditTarget + if let vlanInfo = vlanInfoByName[name] { + editTarget = .init(menuPath: "/interface vlan", restPath: "interface/vlan", itemID: vlanInfo.itemID) + } else { + editTarget = .init(menuPath: "/interface", restPath: "interface", itemID: item.id) + } nodes.append(OverviewNode( id: nodeID(.interface, name), category: .interface, kind: type, title: name, subtitle: subtitle, - detail: sortedDetail(item.fields) + detail: sortedDetail(item.fields), + editTarget: editTarget )) } diff --git a/RouterOSAssistantTests/OverviewGraphTests.swift b/RouterOSAssistantTests/OverviewGraphTests.swift index f985b6d..e082a28 100644 --- a/RouterOSAssistantTests/OverviewGraphTests.swift +++ b/RouterOSAssistantTests/OverviewGraphTests.swift @@ -37,6 +37,29 @@ final class OverviewGraphTests: XCTestCase { XCTAssertEqual(vlanNode?.subtitle, "VLAN 20") } + /// A plain interface (no curated schema for bare "/interface" here) edits generically via + /// "/interface" using its own item id; a VLAN interface edits through the curated + /// "/interface vlan" schema using the VLAN's own item id from the "/interface vlan" listing, + /// not the generic "/interface" listing's id for the same object. + func testInterfaceEditTargetsUseGenericOrCuratedVlanSchema() { + let graph = OverviewViewModel.buildGraph( + interfaces: [ + item(["name": "ether5", "type": "ether"], id: "*1"), + item(["name": "vlan20", "type": "vlan"], id: "*2") + ], + bridgePorts: [], + vlans: [item(["name": "vlan20", "vlan-id": "20", "interface": "ether5"], id: "*A1")], + wireguardPeers: [], addresses: [], pools: [], dhcpServers: [], dhcpNetworks: [], + dhcpClients: [], routes: [], filterRules: [], natRules: [], addressLists: [] + ) + + let ether5 = graph.nodes.first { $0.id == "Interfaces:ether5" } + XCTAssertEqual(ether5?.editTarget, OverviewNode.EditTarget(menuPath: "/interface", restPath: "interface", itemID: "*1")) + + let vlan20 = graph.nodes.first { $0.id == "Interfaces:vlan20" } + XCTAssertEqual(vlan20?.editTarget, OverviewNode.EditTarget(menuPath: "/interface vlan", restPath: "interface/vlan", itemID: "*A1")) + } + func testDhcpServerEdgesToInterfaceAndPool() { let graph = OverviewViewModel.buildGraph( interfaces: [item(["name": "ether2", "type": "ether"])],