Bug 27: unveränderter distance=0 einer dynamischen Route beim Set abgelehnt
Gleiche Ursache wie Bug 26, aber bei einem kuratierten statt einem
unkuratierten Feld: eine dynamische/verbundene Route hat distance=0,
RouterOS akzeptiert das nur system-intern, nicht als expliziten Eingabe-
wert für "set" — auch wenn der Wert unverändert zurückgesendet wird
("value of distance out of range (1...255)").
Fix generalisiert das "nur bei Änderung senden"-Prinzip aus Bug 26 von
unkuratierten auf ALLE Felder beim Bearbeiten eines bestehenden Items:
pendingCommand diffed jetzt gegen das ursprünglich geladene Item, statt
kuratierte Felder immer komplett neu zu senden. Deckt implizit auch das
Leeren eines Feldes ab (Bug 25), dessen Sonderfall dadurch überflüssig
wurde und entfernt ist.
Nebenbei: ExpertViewModelTests.swift lief bisher gar nicht mit, weil
nach dem Anlegen der Datei kein "xcodegen generate" lief (Xcodegen
erzeugt die Sources-Dateiliste einmalig beim Generieren). Nach erneutem
Generate 58 Unit-Tests grün.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
This commit is contained in:
+11
@@ -779,3 +779,14 @@ Test, WLAN/Bonding/PPPoE-Live-Tests).
|
||||
geändert). 54 Unit-Tests grün (neue `ExpertViewModelTests` +
|
||||
`RouterOSCommandBuilderTests`-Ergänzung), HANDOFF.md aktualisiert,
|
||||
Commit + Push.
|
||||
- "ein fehler beim kommentar schreiben in der Route... value of distance
|
||||
out of range" → Bug 27: dieselbe Ursache wie Bug 26, aber bei einem
|
||||
kuratierten Feld — eine dynamische Route hat `distance=0`, unverändert
|
||||
zurückgesendet lehnt RouterOS das ab. Fix generalisiert: "nur bei
|
||||
Änderung senden" gilt jetzt für kuratierte UND unkuratierte Felder,
|
||||
Bug-25-Sonderfall dadurch überflüssig geworden und entfernt. Dabei
|
||||
entdeckt: `ExpertViewModelTests.swift` lief die ganze Zeit gar nicht
|
||||
mit, weil nach dem Anlegen der Datei kein `xcodegen generate` lief —
|
||||
Merksatz für künftige neue Dateien notiert. Nach `xcodegen generate`
|
||||
58 Unit-Tests grün (inkl. `ExpertViewModelTests` jetzt tatsächlich
|
||||
ausgeführt). HANDOFF.md aktualisiert, Commit + Push.
|
||||
|
||||
+36
-13
@@ -547,7 +547,26 @@ Feld wie der Kommentar geändert wurde. Fix: ein unkuratiertes Feld wird
|
||||
nur noch mitgeschickt, wenn sein Wert sich gegenüber dem ursprünglich
|
||||
geladenen Item tatsächlich geändert hat (oder neu hinzugefügt wurde) —
|
||||
betrifft nicht nur Routen, sondern jedes Menü mit berechneten/nur
|
||||
lesbaren Feldern im generischen Lese-Pfad.
|
||||
lesbaren Feldern im generischen Lese-Pfad. **Direkt beim Nachtesten
|
||||
derselbe Effekt nochmal, diesmal bei einem kuratierten Feld** (Bug 27) —
|
||||
eine dynamische/verbundene Route (automatisch angelegt durch eine
|
||||
IP-Adresse auf dem Interface) hat `distance=0`; das *unveränderte*
|
||||
Zurücksenden dieses ganz normal kuratierten Feldes beim Ändern des
|
||||
Kommentars scheiterte mit "value of distance out of range (1...255)" —
|
||||
RouterOS akzeptiert `distance=0` nur für System-verwaltete Routen, nicht
|
||||
als Eingabewert für `set`. Zeigt: das "nur bei Änderung senden"-Prinzip
|
||||
aus Bug 26 galt bisher nur für unkuratierte Felder, kuratierte Felder
|
||||
wurden weiterhin immer komplett neu gesendet. Fix generalisiert:
|
||||
`pendingCommand` sendet jetzt für **jedes** Feld (kuratiert wie
|
||||
unkuratiert) beim Bearbeiten eines bestehenden Items nur noch den
|
||||
Diff gegen das ursprünglich geladene Item — deckt implizit auch Bug 25
|
||||
(Leeren eines Feldes) mit ab, der Spezialfall dafür wurde entfernt.
|
||||
Nebenbei: die neue `ExpertViewModelTests.swift` lief anfangs gar nicht
|
||||
mit, weil `xcodegen generate` nach dem Anlegen der Datei nicht erneut
|
||||
lief — Xcodegen erzeugt die `sources`-Dateiliste einmalig beim
|
||||
Generieren, eine neu angelegte Datei taucht im generierten `.xcodeproj`
|
||||
nicht automatisch auf. **Merksatz: nach jeder neu angelegten Datei
|
||||
`xcodegen generate` erneut laufen lassen, nicht nur nach Datei-Edits.**
|
||||
|
||||
## `xcodebuild test` hängt — Gatekeeper, kein Code-Bug
|
||||
|
||||
@@ -831,13 +850,15 @@ Bezug zu einem Router-Item).
|
||||
|
||||
**Live gegen Hardware verifiziert** — Nutzer bestätigte Kommentar-
|
||||
Änderung an einer Firewall-Regel über den neuen Bearbeiten-Weg
|
||||
("funktioniert"). Direkt danach beim Weitertesten zwei echte Bugs in der
|
||||
gemeinsamen Bearbeiten-Logik gefunden (Bug 25, 26 — siehe Bug-Liste
|
||||
oben): geleertes Textfeld blieb bestehen, Route-Bearbeiten scheiterte an
|
||||
einem nur-lesbaren Feld (`immediate-gw`). Beide betreffen `ExpertViewModel.
|
||||
("funktioniert"). Direkt danach beim Weitertesten drei echte Bugs in der
|
||||
gemeinsamen Bearbeiten-Logik gefunden (Bug 25, 26, 27 — siehe Bug-Liste
|
||||
oben): geleertes Textfeld blieb bestehen, Route-Bearbeiten scheiterte
|
||||
erst an einem nur-lesbaren Feld (`immediate-gw`), dann am unveränderten
|
||||
`distance=0` einer dynamischen Route. Alle drei betreffen `ExpertViewModel.
|
||||
pendingCommand` und damit **auch den Experte-Tab direkt**, nicht nur den
|
||||
neuen Übersicht-Weg — gefixt, alle 54 Unit-Tests grün (inkl. neuer
|
||||
`ExpertViewModelTests` und `OverviewGraphTests.
|
||||
neuen Übersicht-Weg — gefixt (vereinheitlicht: nur geänderte Felder
|
||||
werden beim Bearbeiten gesendet, kuratiert wie unkuratiert), alle 58
|
||||
Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und `OverviewGraphTests.
|
||||
testEditableNodesCarryTheirRouterOSMenuAndItemID`).
|
||||
|
||||
## Stand der Milestones
|
||||
@@ -939,12 +960,14 @@ testEditableNodesCarryTheirRouterOSMenuAndItemID`).
|
||||
verdrängt) plus neue Fähigkeit: Knoten (IP-Adresse, Pool, DHCP-Server/
|
||||
-Netzwerk/-Client, Route, Firewall-Filter-/NAT-Regel, WireGuard-Peer)
|
||||
direkt über dieselbe Sheet wie im Experte-Tab bearbeiten und
|
||||
zurückschreiben. Dabei zwei Bugs in der gemeinsamen Bearbeiten-Logik
|
||||
gefunden+gefixt (Bug 25, 26 — betreffen auch den Experte-Tab direkt):
|
||||
geleertes Feld blieb bestehen statt geleert zu werden, Route-Bearbeiten
|
||||
scheiterte an einem nur-lesbaren Feld (`immediate-gw`). **Live gegen
|
||||
Hardware verifiziert** — Nutzer bestätigte Kommentar-Änderung an einer
|
||||
Firewall-Regel über den neuen Weg ("funktioniert").
|
||||
zurückschreiben. Dabei drei Bugs in der gemeinsamen Bearbeiten-Logik
|
||||
gefunden+gefixt (Bug 25, 26, 27 — betreffen auch den Experte-Tab
|
||||
direkt): geleertes Feld blieb bestehen statt geleert zu werden,
|
||||
Route-Bearbeiten scheiterte erst an einem nur-lesbaren Feld
|
||||
(`immediate-gw`), dann am unveränderten `distance=0` einer dynamischen
|
||||
Route. **Live gegen Hardware verifiziert** — Nutzer bestätigte
|
||||
Kommentar-Änderung an einer Firewall-Regel über den neuen Weg
|
||||
("funktioniert").
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
|
||||
@@ -150,13 +150,22 @@ final class ExpertViewModel: ObservableObject {
|
||||
/// actually executes, consistent with the rest of the app never applying silently.
|
||||
var pendingCommand: RouterOSCommand? {
|
||||
guard let schema = selectedSchema, let editingItem else { return nil }
|
||||
// Keep a curated field even when now empty if it previously held a value — RouterOS'
|
||||
// `set` only touches parameters it's given, so simply omitting a cleared field (the
|
||||
// naive "drop empty values" rule) leaves the old value in place instead of clearing it.
|
||||
// Confirmed live (2026-09-15): deleting a comment's text and saving left the old comment
|
||||
// on the router. Still drop fields that were already empty/unset, same as before — no
|
||||
// point sending e.g. an untouched optional field as "" on every save.
|
||||
var arguments = formValues.filter { !$0.value.isEmpty || !(editingItem.fields[$0.key] ?? "").isEmpty }
|
||||
let isNew = editingItem.id.isEmpty
|
||||
|
||||
// When editing an existing item, only send a field if its value actually differs from
|
||||
// what the router originally reported — RouterOS' `set` only touches parameters it's
|
||||
// given, so an unchanged field doesn't need resending, and for some fields resending the
|
||||
// existing value verbatim is actively rejected: a dynamic/connected route's `distance=0`
|
||||
// ("value of distance out of range (1...255)") and a route's read-only `immediate-gw`
|
||||
// ("bad parameter immediate-gw") both failed this way when saving an edit to a completely
|
||||
// different field like the comment (confirmed live, 2026-09-15). Diffing against the
|
||||
// original also naturally covers *clearing* a field (new value "" differs from the old
|
||||
// non-empty one, so it's still sent — explicitly, as "" — unlike a field that was already
|
||||
// empty and stays empty, which is correctly left out). New items have no "original" to
|
||||
// diff against, so every non-empty curated value is sent as before.
|
||||
var arguments: [String: String] = isNew
|
||||
? formValues.filter { !$0.value.isEmpty }
|
||||
: formValues.filter { $0.value != (editingItem.fields[$0.key] ?? "") }
|
||||
// RouterOS' CLI parser rejects "true"/"false" for boolean parameters — it only accepts
|
||||
// "yes"/"no" (confirmed live: "disabled=false" on "/interface vlan add" produced
|
||||
// "syntax error (line 1 column 30)"; "disabled=no" succeeded). Schema defaults are
|
||||
@@ -167,20 +176,14 @@ final class ExpertViewModel: ObservableObject {
|
||||
if value == "true" { arguments[field.key] = "yes" }
|
||||
else if value == "false" { arguments[field.key] = "no" }
|
||||
}
|
||||
// Only resend an uncurated ("Weitere Parameter") field if the user actually changed it
|
||||
// (or added a brand-new one) — RouterOS' `print`/REST GET returns some fields that are
|
||||
// computed/read-only and rejected on `set` (confirmed live: a route's "immediate-gw",
|
||||
// "bad parameter immediate-gw"). Since `startEditing` pre-fills every uncurated field
|
||||
// from the live item for visibility, blindly resending all of them on every save — even
|
||||
// to change one unrelated curated field like a comment — resent that computed value
|
||||
// unchanged and broke the whole edit. Comparing against `editingItem.fields` (the
|
||||
// untouched original) tells "user touched this" apart from "just showing what's there".
|
||||
// Same "only if changed" rule for uncurated ("Weitere Parameter") fields — these are
|
||||
// pre-filled from the live item for visibility by `startEditing`, so without this every
|
||||
// save would resend all of them, including computed/read-only ones RouterOS refuses.
|
||||
for extra in extraFields where !extra.key.isEmpty {
|
||||
if extra.value != editingItem.fields[extra.key] {
|
||||
arguments[extra.key] = extra.value
|
||||
}
|
||||
}
|
||||
let isNew = editingItem.id.isEmpty
|
||||
let summary = "\(isNew ? "Neu anlegen" : "Ändern") unter \(schema.menuPath)"
|
||||
if isNew {
|
||||
return .add(menuPath: schema.menuPath, restPath: schema.restPath, arguments: arguments, summary: summary)
|
||||
|
||||
@@ -8,7 +8,8 @@ final class ExpertViewModelTests: XCTestCase {
|
||||
menuPath: "/ip route", restPath: "ip/route", category: .routing,
|
||||
displayName: "Test", summary: "", explanation: "",
|
||||
fields: [
|
||||
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "")
|
||||
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: ""),
|
||||
RouterOSFieldSchema(key: "distance", label: "Distanz", kind: .int, help: "")
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -40,4 +41,17 @@ final class ExpertViewModelTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(viewModel.pendingCommand?.arguments["note"], "new")
|
||||
}
|
||||
|
||||
/// A dynamic/connected route's distance=0 is only valid as-is — RouterOS rejects it if
|
||||
/// resent verbatim on a `set` ("value of distance out of range (1...255)"), even when the
|
||||
/// value itself hasn't changed. Editing only the comment must not resend it.
|
||||
func testUnchangedCuratedFieldIsNotResentEvenIfOutOfNormalRange() {
|
||||
let viewModel = ExpertViewModel(connectionService: ConnectionService())
|
||||
viewModel.selectedSchema = makeSchema()
|
||||
viewModel.startEditing(RouterOSMenuItem(id: "*1", fields: ["comment": "", "distance": "0"]))
|
||||
viewModel.formValues["comment"] = "bridge"
|
||||
|
||||
XCTAssertNil(viewModel.pendingCommand?.arguments["distance"])
|
||||
XCTAssertEqual(viewModel.pendingCommand?.arguments["comment"], "bridge")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user