M15 erweitert: Interfaces direkt editierbar (Übersicht + Experte-Tab)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
This commit is contained in:
Kay
2026-09-15 15:10:55 +02:00
co-authored by Claude Sonnet 5
parent bd610b0a8c
commit 2e507aa9ef
6 changed files with 99 additions and 23 deletions
+12
View File
@@ -802,3 +802,15 @@ Test, WLAN/Bonding/PPPoE-Live-Tests).
Tab-Inhalte auf macOS beim Wechsel aber nicht. Fix: `.onAppear` statt 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 `.task`, feuert bei jedem Tab-Besuch neu. 59 Unit-Tests grün, HANDOFF.md
aktualisiert, Commit + Push. 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.
+19 -10
View File
@@ -1070,16 +1070,25 @@ Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und
Hintergrund direkt am Zeileninhalt (`Text` in `RoundedRectangle`- Hintergrund direkt am Zeileninhalt (`Text` in `RoundedRectangle`-
Fill) gesetzt. Alles vom Nutzer live in Xcode bestätigt ("passt Fill) gesetzt. Alles vom Nutzer live in Xcode bestätigt ("passt
gut"). gut").
11. **M15-Bearbeiten-Funktion deckt Interfaces und Adress-Listen bewusst 11. ~~M15-Bearbeiten-Funktion deckt Interfaces bewusst nicht ab~~ —
nicht ab** — physische/VLAN/Bridge-Interfaces verteilen ihre Felder erledigt: jedes Interface hat jetzt ein `editTarget` — VLAN-
über mehrere typ-spezifische RouterOS-Menüs (`/interface ethernet`, Interfaces über das kuratierte `/interface vlan`-Schema (mit der
`/interface bridge`, `/interface vlan`, ...), ein sauberes eigenen `.id` aus `/interface vlan`, nicht der generischen
`OverviewNode.EditTarget` bräuchte pro Interface-Typ eine eigene `/interface`-Liste), alle anderen Typen (Ethernet, Bridge, WLAN,
Zuordnung statt der generischen `/interface`. Adress-Listen-Knoten WireGuard) generisch über `/interface`. Dabei live einen echten
fassen mehrere Einträge in einem Knoten zusammen, kein 1:1-Bezug zu Stolperstein gefunden: der Nutzer wollte `ether5` zu `ether51`
einem Router-Item — bräuchte eine andere UI (Liste statt Formular). umbenennen, hat aber (da für `/interface` bisher gar kein
Beides bei Bedarf nachrüstbar, für diese Session bewusst kuratiertes Schema existierte, alles landete in "Weitere Parameter")
zurückgestellt. 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 Gitea-Remote `origin` ist eingerichtet und wird laufend gepusht (siehe
oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15. oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15.
+12 -10
View File
@@ -95,12 +95,13 @@ Grafisches Diagramm des kompletten aktuellen Router-Zustands:
Knotens ist ebenfalls klickbar (springt direkt zum verbundenen Knotens ist ebenfalls klickbar (springt direkt zum verbundenen
Element) und zeigt beim Hovern eine kurze Erklärung als Tooltip. Element) und zeigt beim Hovern eine kurze Erklärung als Tooltip.
- **Direktes Bearbeiten**: IP-Adressen, Pools, DHCP-Server/-Netzwerke/ - **Direktes Bearbeiten**: IP-Adressen, Pools, DHCP-Server/-Netzwerke/
-Clients, Routen, Firewall-Filter-/NAT-Regeln und WireGuard-Peers -Clients, Routen, Firewall-Filter-/NAT-Regeln, WireGuard-Peers und
lassen sich anklicken und über "Bearbeiten" direkt ändern — schreibt Interfaces (Ethernet, Bridge, WLAN, VLAN, WireGuard) lassen sich
sofort an den Router zurück. (Physische/VLAN/Bridge-Interfaces und anklicken und über "Bearbeiten" direkt ändern — schreibt sofort an den
Adress-Listen-Knoten sind hier noch nicht direkt editierbar, nur über Router zurück. (Adress-Listen-Knoten sind hier noch nicht direkt
den Experte-Tab.) Dynamische/automatisch angelegte Routen (z.B. die editierbar, nur über den Experte-Tab, da ein Knoten dort mehrere
Verbindungsroute eines Netzwerks) zeigen bewusst keinen 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. Bearbeiten-Button, da RouterOS solche Einträge selbst verwaltet.
- Aktualisiert sich automatisch, sobald du in den Tab wechselst. - Aktualisiert sich automatisch, sobald du in den Tab wechselst.
- Zoom-Buttons (/100%/+) für die Diagrammgröße. - 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 clickable (jumps straight to the connected element) and shows a short
explanation as a tooltip on hover. explanation as a tooltip on hover.
- **Direct editing**: IP addresses, pools, DHCP servers/networks/ - **Direct editing**: IP addresses, pools, DHCP servers/networks/
clients, routes, firewall filter/NAT rules and WireGuard peers can be clients, routes, firewall filter/NAT rules, WireGuard peers and
clicked and changed via "Edit" — writes back to the router interfaces (Ethernet, bridge, WLAN, VLAN, WireGuard) can be clicked
immediately. (Physical/VLAN/bridge interfaces and address-list nodes and changed via "Edit" — writes back to the router immediately.
aren't directly editable here yet, only through the Expert tab.) (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) Dynamic/auto-created routes (e.g. a network's own connected route)
deliberately show no Edit button, since RouterOS manages those itself. deliberately show no Edit button, since RouterOS manages those itself.
- Refreshes automatically whenever you switch to this tab. - Refreshes automatically whenever you switch to this tab.
@@ -199,6 +199,22 @@ enum RouterOSSchemaCatalog {
// MARK: - Interfaces (generic, browsing + free-form fields) // MARK: - Interfaces (generic, browsing + free-form fields)
private static let interfaceFamily: [RouterOSMenuSchema] = [ 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( RouterOSMenuSchema(
menuPath: "/interface bridge", restPath: "interface/bridge", category: .interfaces, menuPath: "/interface bridge", restPath: "interface/bridge", category: .interfaces,
displayName: "Bridge", displayName: "Bridge",
@@ -81,10 +81,10 @@ final class OverviewViewModel: ObservableObject {
// Interfaces: every physical/virtual interface RouterOS reports (ethernet, vlan, bridge, // Interfaces: every physical/virtual interface RouterOS reports (ethernet, vlan, bridge,
// wireless, wireguard, ...). "vlan-id"/parent info is filled in below from "/interface // wireless, wireguard, ...). "vlan-id"/parent info is filled in below from "/interface
// vlan", since the generic "/interface" listing alone doesn't carry it. // 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 { for item in vlans {
guard let name = item.fields["name"] else { continue } 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 { for item in interfaces {
@@ -98,10 +98,24 @@ final class OverviewViewModel: ObservableObject {
if item.fields["disabled"] == "true" || item.fields["disabled"] == "yes" { if item.fields["disabled"] == "true" || item.fields["disabled"] == "yes" {
subtitle += " · deaktiviert" 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( nodes.append(OverviewNode(
id: nodeID(.interface, name), category: .interface, kind: type, id: nodeID(.interface, name), category: .interface, kind: type,
title: name, subtitle: subtitle, title: name, subtitle: subtitle,
detail: sortedDetail(item.fields) detail: sortedDetail(item.fields),
editTarget: editTarget
)) ))
} }
@@ -37,6 +37,29 @@ final class OverviewGraphTests: XCTestCase {
XCTAssertEqual(vlanNode?.subtitle, "VLAN 20") 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() { func testDhcpServerEdgesToInterfaceAndPool() {
let graph = OverviewViewModel.buildGraph( let graph = OverviewViewModel.buildGraph(
interfaces: [item(["name": "ether2", "type": "ether"])], interfaces: [item(["name": "ether2", "type": "ether"])],