forked from kay/RouterOS
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:
+12
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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"])],
|
||||||
|
|||||||
Reference in New Issue
Block a user