M30: Experte-Tab Mode-Taste-Menü + SSH-Trust-/REST-Fixes
Neues Schema für /system routerboard mode-button (enabled/on-event/ hold-time), live gegen echte Hardware verifiziert. Dabei drei echte Bugs gefunden und gefixt: - SSH-Trust-Dead-End beim ersten Experte-Tab-Schreiben (Backup-vor- Schreiben-Pfad hatte keinen Weg, den Trust-Dialog auszulösen) — ConnectionService.noteUntrustedSSHHostKey - RouterOSFieldSchema.clearable: optionale Felder senden nie mehr einen expliziten Leer-Wert, wenn das RouterOS ablehnt - RouterOSMenuSchema.writesRequireSSH + ConnectionService.applyViaSSH: für Menüs ohne REST-Anbindung (bewiesen per direktem SSH-Test) wird zwingend eine dedizierte SSH-Verbindung genutzt statt REST Live Ende-zu-Ende bestätigt: Skript anlegen, Mode-Taste zuweisen, Tastendruck löst Skript korrekt aus. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+30
@@ -1040,3 +1040,33 @@ Test, WLAN/Bonding/PPPoE-Live-Tests).
|
||||
Farben" → found.md Punkt 9 ergänzt (freie Farbwahl pro Kategorie statt
|
||||
nur 2 Themes, für später). Milestone live bestätigt, Docs aktualisiert,
|
||||
Commit + Push.
|
||||
|
||||
## Session: Experte-Tab "Mode-Taste"-Menü (M30)
|
||||
|
||||
- "weiter in der found.md" (nächster offener Punkt: Mode-Button) →
|
||||
Recherche bei help.mikrotik.com (nicht geraten): echte Felder
|
||||
enabled/on-event/hold-time, als neues Singleton-Schema gebaut,
|
||||
inkl. Warnhinweis zur physischen Bestätigungspflicht ab RouterOS
|
||||
7.1rc4.
|
||||
- "schreib mir ein ungefährliches testscript zum testen der
|
||||
Mode-Taste" → reiner :log-info-Befehl vorgeschlagen (keine
|
||||
Konfigänderung).
|
||||
- Nutzer stieß beim Skript-Anlegen zuerst auf einen SSH-Trust-Dead-End
|
||||
(Bug 40, echte Ursache gefunden per Codelesen: Backup-vor-erstem-
|
||||
Schreiben lief über eigene SSH-Verbindung ohne Trust-Dialog-
|
||||
Anbindung) — gefixt (`ConnectionService.noteUntrustedSSHHostKey`).
|
||||
- Mode-Taste speichern → HTTP 500. Erste Spur (leeres `hold-time`)
|
||||
gefixt, Fehler blieb identisch — Nutzer testete denselben Befehl
|
||||
direkt per SSH-Terminal: lief dort sofort. Damit bewiesen: RouterOS-
|
||||
REST-Deckungslücke für dieses Menü, kein App-Bug. Fix:
|
||||
`writesRequireSSH`-Flag + dedizierte SSH-Verbindung fürs Schreiben
|
||||
(Bug 41).
|
||||
- Tastendruck löste Skript aus, aber "not enough permissions" im Log.
|
||||
Zwei Terminal-Fixversuche liefen ins Leere (Skriptname-Großschreibung
|
||||
+ falsche `set`-Syntax trafen den Eintrag nie — `print detail`
|
||||
bestätigte unveränderten Zustand). Mit korrektem Index-Befehl
|
||||
(`dont-require-permissions=yes`) sofort gelöst (Bug 42, reines
|
||||
RouterOS-Verhalten, kein App-Bug).
|
||||
- Live Ende-zu-Ende bestätigt: Skript anlegen → Mode-Taste über die
|
||||
App zuweisen+speichern → Tastendruck löst Skript korrekt aus, Log
|
||||
zeigt den Eintrag. Docs aktualisiert, Commit + Push.
|
||||
|
||||
+54
@@ -1897,3 +1897,57 @@ Infrastruktur, zuvor gab es nur den DE/EN-Toolbar-Schalter):
|
||||
|
||||
Build+alle 98 Unit-Tests grün nach jedem Schritt. Live bestätigt
|
||||
("passt, lassen wir so").
|
||||
|
||||
**M30: Experte-Tab — "Mode-Taste"-Menü (`/system routerboard mode-button`)**
|
||||
(2026-09-17, gegen echte Hardware live verifiziert):
|
||||
|
||||
1. Recherchiert (help.mikrotik.com, nicht geraten): drei echte Felder
|
||||
`enabled`/`on-event`/`hold-time` (Zeitspanne Min..Max, z.B.
|
||||
"3s..5s"). Als neuer Singleton-Schema-Eintrag in
|
||||
`RouterOSSchemaCatalog.systemFamily` gebaut, `on-event` als
|
||||
`.menuItemPick` auf `/system script` (wie beim bestehenden
|
||||
Zeitplaner). Warnhinweis ergänzt: ab RouterOS 7.1rc4 braucht jede
|
||||
Änderung zusätzlich eine physische Tastenbestätigung am Gerät.
|
||||
2. **Bug 41:** Speichern schlug mit `HTTP 500` fehl. Erste, falsche
|
||||
Spur: `hold-time=""` im gesendeten Befehl (leeres optionales Feld
|
||||
wurde trotzdem mitgeschickt) — dafür neuer
|
||||
`RouterOSFieldSchema.clearable`-Parameter (Default `true`, bricht
|
||||
bestehende Tests nicht), `hold-time` auf `clearable: false` gesetzt.
|
||||
Fix wirkte (Feld verschwand aus dem gesendeten Befehl), **derselbe
|
||||
HTTP-500-Fehler blieb aber** — Beweis, dass das nicht die
|
||||
eigentliche Ursache war. Nutzer führte denselben Befehl direkt per
|
||||
SSH-Terminal aus: lief dort sofort fehlerfrei, `print` bestätigte
|
||||
alle Felder korrekt gesetzt. Damit zweifelsfrei belegt: dieses
|
||||
RouterOS-Hardware-Menü hat schlicht keine REST-API-Anbindung auf
|
||||
diesem Router — eine echte RouterOS-Deckungslücke, kein App-Bug.
|
||||
Fix: neuer `RouterOSMenuSchema.writesRequireSSH`-Parameter +
|
||||
`ConnectionService.applyViaSSH(_:)` (dedizierte SSH-Verbindung,
|
||||
exakt das Muster von `BackupService`/`UpdateService`) —
|
||||
`ExpertViewModel.saveEditingItem()` nutzt sie für so markierte
|
||||
Schemas zwingend statt des Session-Transports. Mode-Taste
|
||||
entsprechend markiert. Live bestätigt: Speichern über die App läuft
|
||||
seitdem fehlerfrei.
|
||||
3. **Bug 42 (kein App-Bug, reines RouterOS-Verhalten, hier
|
||||
dokumentiert weil beim Live-Test dieser Funktion gefunden):**
|
||||
Tastendruck löste das Skript aus, aber
|
||||
`script,error executing script ... failed ... (not enough
|
||||
permissions)` im Log. Skripte, die vom Mode-Button (RouterOS-intern
|
||||
"sys2"-Kontext) ausgelöst werden, laufen mit eingeschränkten
|
||||
Rechten, nicht mit dem vollen Standard-Policy-Set eines manuell
|
||||
angelegten Skripts. Zwei erste Fix-Versuche (Policy einschränken,
|
||||
dann `dont-require-permissions=yes` setzen) zeigten scheinbar keine
|
||||
Wirkung — Ursache: Skriptname war tatsächlich "Button test" (großes
|
||||
B), die Terminal-Befehle zielten auf "button test"/nutzten
|
||||
ungültige `set`-Syntax ohne `[find ...]`/Index, trafen also nie den
|
||||
echten Eintrag (`print detail` bestätigte das: Werte unverändert).
|
||||
Mit korrektem Index-Befehl (`/system script set 0
|
||||
dont-require-permissions=yes`) griff es sofort. **Für jedes
|
||||
künftige Mode-Taste-Skript nötig:** `dont-require-permissions=yes`,
|
||||
sonst schlägt die Ausführung vom Button aus fehl, obwohl dasselbe
|
||||
Skript manuell/per Scheduler einwandfrei liefe. Die App hat dafür
|
||||
noch kein Feld im "Skripte"-Schema (`/system script` kennt nur
|
||||
Name+Inhalt) — für später vorgemerkt (`found.md` Punkt 6).
|
||||
|
||||
Build+alle 99 Unit-Tests grün. Live Ende-zu-Ende bestätigt: Skript
|
||||
anlegen → Mode-Taste zuweisen+speichern (über die App) → Tastendruck
|
||||
löst Skript korrekt aus, Log zeigt den Eintrag.
|
||||
|
||||
@@ -414,6 +414,21 @@ RouterOS-Menü: `/system clock` · REST-Pfad: `system/clock`
|
||||
|---|---|---|---|---|---|
|
||||
| Zeitzone | `time-zone-name` | Text | Nein | — | Z.B. Europe/Berlin. |
|
||||
|
||||
#### Mode-Taste *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
|
||||
RouterOS-Menü: `/system routerboard mode-button` · REST-Pfad: `system/routerboard/mode-button`
|
||||
|
||||
Steuert, welches Skript beim Drücken der physischen Mode-Taste am Router ausgeführt wird.
|
||||
|
||||
Manche RouterBOARD-Geräte (z.B. hEX, cAP, hAP ac², LtAP mini, einige CCR/CRS) haben eine physische Mode-Taste an der Seite. Hier lässt sich festlegen, ob und wie lange sie gedrückt werden muss, damit ein zuvor unter "Skripte" angelegtes Skript ausgeführt wird.
|
||||
|
||||
> ⚠️ **Achtung:** Ab RouterOS 7.1rc4 muss jede Aktivierung oder Änderung dieser Einstellung zusätzlich durch einen physischen Tastendruck (Reset- oder Mode-Taste) innerhalb von 60 Sekunden am Gerät selbst bestätigt werden — eine Änderung über diese App allein reicht nicht.
|
||||
|
||||
| Feld | RouterOS-Parameter | Typ | Pflicht | Standard | Hilfetext |
|
||||
|---|---|---|---|---|---|
|
||||
| Aktiviert | `enabled` | Ja/Nein | Nein | `no` | Mode-Taste ein-/ausschalten. |
|
||||
| Auszuführendes Skript | `on-event` | Verweis auf bestehenden Eintrag unter `/system script` | Nein | — | Name eines zuvor unter "Skripte" angelegten Skripts. |
|
||||
| Haltedauer (Min..Max) | `hold-time` | Text | Nein | — | Wie lange die Taste gedrückt gehalten werden muss, als Zeitspanne Min..Max, z.B. "3s..5s". Verfügbar ab RouterOS 6.47beta60. |
|
||||
|
||||
#### Zeitserver (NTP) *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
|
||||
RouterOS-Menü: `/system ntp client` · REST-Pfad: `system/ntp/client`
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -169,6 +169,7 @@ nur die zugehörigen Passwörter liegen weiterhin im macOS-Schlüsselbund.
|
||||
| — | LAN-Port-Konflikt-Prüfung + "Fertig"-Button (Einrichten) | 🔶 gebaut, Live-Test offen |
|
||||
| M28 | Übersicht-Tab: Fokus-Modus (Klick auf Knoten → Kette im schwebenden Popup, Rest abgedunkelt); Close-Button-Layout in allen vier Popups vereinheitlicht | ✅ live verifiziert |
|
||||
| M29 | Einstellungen-Fenster (⌘,): Sprache, Update-Auto-Check, Farbschema, Textgröße, Bedienelement-Größe, LAN-Scanner-Refreshraten | ✅ live verifiziert |
|
||||
| M30 | Experte-Tab: "Mode-Taste"-Menü (`/system routerboard mode-button`), inkl. SSH-Zwangsweg für REST-Deckungslücken | ✅ live verifiziert |
|
||||
|
||||
Ausführlicher Stand inkl. aller gefundenen Bugs, offener Punkte und
|
||||
Session-Verlauf: [`HANDOFF.md`](HANDOFF.md) / [`CHATLOG.md`](CHATLOG.md).
|
||||
|
||||
@@ -461,6 +461,17 @@ enum L10n {
|
||||
"Uhrzeit/Zeitzone": "Time/Time Zone",
|
||||
"Zeitzone": "Time Zone",
|
||||
"Z.B. Europe/Berlin.": "E.g. Europe/Berlin.",
|
||||
"Mode-Taste": "Mode Button",
|
||||
"Steuert, welches Skript beim Drücken der physischen Mode-Taste am Router ausgeführt wird.":
|
||||
"Controls which script runs when the router's physical Mode button is pressed.",
|
||||
"Manche RouterBOARD-Geräte (z.B. hEX, cAP, hAP ac², LtAP mini, einige CCR/CRS) haben eine physische Mode-Taste an der Seite. Hier lässt sich festlegen, ob und wie lange sie gedrückt werden muss, damit ein zuvor unter \"Skripte\" angelegtes Skript ausgeführt wird.":
|
||||
"Some RouterBOARD devices (e.g. hEX, cAP, hAP ac², LtAP mini, some CCR/CRS) have a physical Mode button on the side. This controls whether, and for how long, it must be held down to run a script previously created under \"Scripts\".",
|
||||
"Ab RouterOS 7.1rc4 muss jede Aktivierung oder Änderung dieser Einstellung zusätzlich durch einen physischen Tastendruck (Reset- oder Mode-Taste) innerhalb von 60 Sekunden am Gerät selbst bestätigt werden — eine Änderung über diese App allein reicht nicht.":
|
||||
"Starting with RouterOS 7.1rc4, enabling or changing this setting also requires a physical button press (Reset or Mode button) on the device itself within 60 seconds to confirm — a change through this app alone isn't enough.",
|
||||
"Mode-Taste ein-/ausschalten.": "Turn the Mode button on/off.",
|
||||
"Haltedauer (Min..Max)": "Hold Time (Min..Max)",
|
||||
"Wie lange die Taste gedrückt gehalten werden muss, als Zeitspanne Min..Max, z.B. \"3s..5s\". Verfügbar ab RouterOS 6.47beta60.":
|
||||
"How long the button must be held down, as a Min..Max time range, e.g. \"3s..5s\". Available from RouterOS 6.47beta60 onward.",
|
||||
"Zeitserver (NTP)": "Time Server (NTP)",
|
||||
"Falsche Systemzeit kann Zertifikatsprüfungen (HTTPS/REST) und Log-Zeitstempel durcheinanderbringen.":
|
||||
"Wrong system time can mess up certificate checks (HTTPS/REST) and log timestamps.",
|
||||
|
||||
@@ -33,14 +33,22 @@ struct RouterOSFieldSchema: Identifiable, Equatable {
|
||||
let defaultValue: String?
|
||||
/// Whether a value is required before the item can be added.
|
||||
let required: Bool
|
||||
/// Whether blanking this field and saving should send an explicit `field=""` ("clear this")
|
||||
/// to RouterOS. True by default — this is the normal, tested behavior for plain free-text
|
||||
/// fields like "comment". False for fields whose RouterOS type isn't really free text (e.g. a
|
||||
/// time-interval-range like mode-button's "hold-time") — confirmed live: RouterOS' REST API
|
||||
/// can 500 on an empty string for a non-text property, where a leftover/never-set blank value
|
||||
/// in the form should just mean "leave this alone", not "actively clear it".
|
||||
let clearable: Bool
|
||||
|
||||
init(key: String, label: String, kind: Kind, help: String, defaultValue: String? = nil, required: Bool = false) {
|
||||
init(key: String, label: String, kind: Kind, help: String, defaultValue: String? = nil, required: Bool = false, clearable: Bool = true) {
|
||||
self.key = key
|
||||
self.label = label
|
||||
self.kind = kind
|
||||
self.help = help
|
||||
self.defaultValue = defaultValue
|
||||
self.required = required
|
||||
self.clearable = clearable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +96,12 @@ struct RouterOSMenuSchema: Identifiable, Equatable {
|
||||
/// removed. The Expert tool skips the add/remove UI and the transports fetch/apply them
|
||||
/// differently for menus flagged this way.
|
||||
let isSingleton: Bool
|
||||
/// True for menus confirmed live not to work over RouterOS' REST API at all — the exact same
|
||||
/// write succeeds instantly over plain SSH, REST 500s regardless of which fields are sent (not
|
||||
/// a data-formatting issue this app can work around). Routes writes through a dedicated SSH
|
||||
/// connection instead (`ConnectionService.applyViaSSH`), bypassing whatever the session's
|
||||
/// active transport is. Confirmed for `/system routerboard mode-button` (2026-09-17).
|
||||
let writesRequireSSH: Bool
|
||||
|
||||
init(
|
||||
menuPath: String,
|
||||
@@ -99,7 +113,8 @@ struct RouterOSMenuSchema: Identifiable, Equatable {
|
||||
warning: String? = nil,
|
||||
fields: [RouterOSFieldSchema] = [],
|
||||
listColumns: [String] = [],
|
||||
isSingleton: Bool = false
|
||||
isSingleton: Bool = false,
|
||||
writesRequireSSH: Bool = false
|
||||
) {
|
||||
self.menuPath = menuPath
|
||||
self.restPath = restPath
|
||||
@@ -111,5 +126,6 @@ struct RouterOSMenuSchema: Identifiable, Equatable {
|
||||
self.fields = fields
|
||||
self.listColumns = listColumns
|
||||
self.isSingleton = isSingleton
|
||||
self.writesRequireSSH = writesRequireSSH
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +654,26 @@ enum RouterOSSchemaCatalog {
|
||||
listColumns: ["time-zone-name"],
|
||||
isSingleton: true
|
||||
),
|
||||
RouterOSMenuSchema(
|
||||
menuPath: "/system routerboard mode-button", restPath: "system/routerboard/mode-button", category: .system,
|
||||
displayName: "Mode-Taste",
|
||||
summary: "Steuert, welches Skript beim Drücken der physischen Mode-Taste am Router ausgeführt wird.",
|
||||
explanation: "Manche RouterBOARD-Geräte (z.B. hEX, cAP, hAP ac², LtAP mini, einige CCR/CRS) haben eine physische Mode-Taste an der Seite. Hier lässt sich festlegen, ob und wie lange sie gedrückt werden muss, damit ein zuvor unter \"Skripte\" angelegtes Skript ausgeführt wird.",
|
||||
warning: "Ab RouterOS 7.1rc4 muss jede Aktivierung oder Änderung dieser Einstellung zusätzlich durch einen physischen Tastendruck (Reset- oder Mode-Taste) innerhalb von 60 Sekunden am Gerät selbst bestätigt werden — eine Änderung über diese App allein reicht nicht.",
|
||||
fields: [
|
||||
RouterOSFieldSchema(key: "enabled", label: "Aktiviert", kind: .bool,
|
||||
help: "Mode-Taste ein-/ausschalten.", defaultValue: "no"),
|
||||
RouterOSFieldSchema(key: "on-event", label: "Auszuführendes Skript",
|
||||
kind: .menuItemPick(menuPath: "/system script", restPath: "system/script", valueField: "name"),
|
||||
help: "Name eines zuvor unter \"Skripte\" angelegten Skripts."),
|
||||
RouterOSFieldSchema(key: "hold-time", label: "Haltedauer (Min..Max)", kind: .text,
|
||||
help: "Wie lange die Taste gedrückt gehalten werden muss, als Zeitspanne Min..Max, z.B. \"3s..5s\". Verfügbar ab RouterOS 6.47beta60.",
|
||||
clearable: false)
|
||||
],
|
||||
listColumns: ["enabled", "on-event"],
|
||||
isSingleton: true,
|
||||
writesRequireSSH: true
|
||||
),
|
||||
RouterOSMenuSchema(
|
||||
menuPath: "/system ntp client", restPath: "system/ntp/client", category: .system,
|
||||
displayName: "Zeitserver (NTP)",
|
||||
|
||||
@@ -113,6 +113,18 @@ final class ConnectionService: ObservableObject {
|
||||
pendingSSHTrustFingerprint = nil
|
||||
}
|
||||
|
||||
/// Safety net for when the proactive `verifySSHTrust` check (above) didn't already cover a
|
||||
/// dedicated SSH service's host key — confirmed live: a session's first Expert-tab write (its
|
||||
/// pre-apply backup, over its own dedicated SSH connection) can still hit
|
||||
/// `untrustedSSHHostKey` even after a successful connect, dead-ending as plain error text with
|
||||
/// no way to resolve it (same root symptom as Bug 13/37, just at write-time instead of
|
||||
/// connect-time). Callers catch `RouterOSError.untrustedSSHHostKey` around their own write
|
||||
/// path and call this so the same Verbinden-tab trust dialog appears — trusting there unblocks
|
||||
/// a manual retry of whatever write just failed.
|
||||
func noteUntrustedSSHHostKey(_ fingerprint: String) {
|
||||
pendingSSHTrustFingerprint = fingerprint
|
||||
}
|
||||
|
||||
/// Best-effort SSH host-key probe run once right after a successful REST connect — see
|
||||
/// `pendingSSHTrustFingerprint`'s doc comment for why this is needed at all. A throwaway SSH
|
||||
/// connection is the only way to learn the router's host key (there's no way to ask for it
|
||||
@@ -139,6 +151,25 @@ final class ConnectionService: ObservableObject {
|
||||
try await activeTransport.apply(command)
|
||||
}
|
||||
|
||||
/// Applies a command over a dedicated, one-shot SSH connection instead of whatever
|
||||
/// `activeTransport` currently is — for the small number of menus confirmed live not to work
|
||||
/// over RouterOS' REST API at all (not a data/formatting issue on this app's side: the exact
|
||||
/// same write succeeds instantly over plain SSH/terminal, REST 500s regardless of which fields
|
||||
/// are sent). Confirmed for `/system routerboard mode-button` (2026-09-17); same dedicated-
|
||||
/// connection pattern as `BackupService`/`UpdateService`.
|
||||
func applyViaSSH(_ command: RouterOSCommand) async throws {
|
||||
guard let credentials else { throw RouterOSError.notConnected }
|
||||
let transport = SSHTransport(credentials: credentials)
|
||||
try await transport.connect()
|
||||
do {
|
||||
try await transport.apply(command)
|
||||
await transport.disconnect()
|
||||
} catch {
|
||||
await transport.disconnect()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func fetchFirewallRuleCounts() async throws -> FirewallRuleCounts {
|
||||
guard let activeTransport else { throw RouterOSError.notConnected }
|
||||
return try await activeTransport.fetchFirewallRuleCounts()
|
||||
|
||||
@@ -266,6 +266,9 @@ final class DevicesViewModel: ObservableObject {
|
||||
// conflict enough that the alert never actually becomes visible — a silent failure
|
||||
// that looked like nothing happened at all.
|
||||
pendingStaticAssignment = nil
|
||||
if case RouterOSError.untrustedSSHHostKey(let fingerprint) = error {
|
||||
connectionService.noteUntrustedSSHHostKey(fingerprint)
|
||||
}
|
||||
applyError = error.localizedDescription
|
||||
}
|
||||
isApplying = false
|
||||
@@ -298,6 +301,9 @@ final class DevicesViewModel: ObservableObject {
|
||||
await load()
|
||||
} catch {
|
||||
pendingStaticRemoval = nil
|
||||
if case RouterOSError.untrustedSSHHostKey(let fingerprint) = error {
|
||||
connectionService.noteUntrustedSSHHostKey(fingerprint)
|
||||
}
|
||||
applyError = error.localizedDescription
|
||||
}
|
||||
isApplying = false
|
||||
|
||||
@@ -166,6 +166,16 @@ final class ExpertViewModel: ObservableObject {
|
||||
var arguments: [String: String] = isNew
|
||||
? formValues.filter { !$0.value.isEmpty }
|
||||
: formValues.filter { $0.value != (editingItem.fields[$0.key] ?? "") }
|
||||
// A non-`clearable` field (see `RouterOSFieldSchema.clearable`'s doc comment) never sends
|
||||
// an explicit empty value, even if the diff above decided to "clear" it — RouterOS can
|
||||
// reject or even 500 an empty string for a property whose type isn't plain text (confirmed
|
||||
// live: `/system routerboard mode-button`'s "hold-time", a time-interval-range field,
|
||||
// 500'd on `hold-time=""`).
|
||||
for field in schema.fields where !field.clearable {
|
||||
if arguments[field.key]?.isEmpty == true {
|
||||
arguments.removeValue(forKey: field.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
|
||||
@@ -209,10 +219,17 @@ final class ExpertViewModel: ObservableObject {
|
||||
applyError = nil
|
||||
do {
|
||||
try await ensureSessionBackup()
|
||||
try await connectionService.apply(command)
|
||||
if selectedSchema?.writesRequireSSH == true {
|
||||
try await connectionService.applyViaSSH(command)
|
||||
} else {
|
||||
try await connectionService.apply(command)
|
||||
}
|
||||
cancelEditing()
|
||||
await reloadItems()
|
||||
} catch {
|
||||
if case RouterOSError.untrustedSSHHostKey(let fingerprint) = error {
|
||||
connectionService.noteUntrustedSSHHostKey(fingerprint)
|
||||
}
|
||||
applyError = error.localizedDescription
|
||||
}
|
||||
isApplying = false
|
||||
@@ -233,6 +250,9 @@ final class ExpertViewModel: ObservableObject {
|
||||
pendingRemoval = nil
|
||||
await reloadItems()
|
||||
} catch {
|
||||
if case RouterOSError.untrustedSSHHostKey(let fingerprint) = error {
|
||||
connectionService.noteUntrustedSSHHostKey(fingerprint)
|
||||
}
|
||||
applyError = error.localizedDescription
|
||||
}
|
||||
isApplying = false
|
||||
|
||||
@@ -14,6 +14,29 @@ final class ExpertViewModelTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testNonClearableFieldNeverSendsExplicitEmptyValue() {
|
||||
let schema = RouterOSMenuSchema(
|
||||
menuPath: "/system routerboard mode-button", restPath: "system/routerboard/mode-button", category: .system,
|
||||
displayName: "Test", summary: "", explanation: "",
|
||||
fields: [
|
||||
RouterOSFieldSchema(key: "enabled", label: "Aktiviert", kind: .bool, help: "", defaultValue: "no"),
|
||||
RouterOSFieldSchema(key: "hold-time", label: "Haltedauer", kind: .text, help: "", clearable: false)
|
||||
],
|
||||
isSingleton: true
|
||||
)
|
||||
let viewModel = ExpertViewModel(connectionService: ConnectionService())
|
||||
viewModel.selectedSchema = schema
|
||||
// A previously non-empty "hold-time" that the user's form now shows blank (whatever the
|
||||
// cause) must still never be sent as an explicit clear — only this field's `clearable:
|
||||
// false` should suppress it; "enabled" (clearable, unaffected) still sends normally.
|
||||
viewModel.startEditing(RouterOSMenuItem(id: "singleton", fields: ["enabled": "no", "hold-time": "3s..5s"]))
|
||||
viewModel.formValues["enabled"] = "yes"
|
||||
viewModel.formValues["hold-time"] = ""
|
||||
|
||||
XCTAssertNil(viewModel.pendingCommand?.arguments["hold-time"])
|
||||
XCTAssertEqual(viewModel.pendingCommand?.arguments["enabled"], "yes")
|
||||
}
|
||||
|
||||
func testClearingACuratedFieldSendsExplicitEmptyValue() {
|
||||
let viewModel = ExpertViewModel(connectionService: ConnectionService())
|
||||
viewModel.selectedSchema = makeSchema()
|
||||
|
||||
@@ -115,21 +115,52 @@ Umsetzung (komplett neuer Mechanismus, `AppTextSize.dynamicTypeSize` entfernt):
|
||||
- Alle ~110 `.font(...)`-Aufrufe in der App auf `.appFont(...)` umgestellt (automatisiert per Scratchpad-Skript, textbasierte 1:1-Ersetzung pro bekanntem Font-Ausdruck) — **außer** zwei Stellen mit fest-breiten Pixel-Layouts, die beim Skalieren brechen würden: `OverviewView.NodeCardView` (Diagramm-Knotenkarten, feste `nodeWidth`/`nodeHeight`) und `DevicesView.DeviceRow`/`DeviceColumnHeader` (LAN-Scanner-Tabellenspalten mit festen `DeviceColumn`-Breiten + `lineLimit(1)`). Diese bleiben bewusst fix, sonst Text-Clipping/Überlappung.
|
||||
- Mechanismus per `ImageRenderer`-Snapshot verifiziert (Basis-Text bei Skalierungsfaktor 0,85 vs. 1,4 rendert nachweislich unterschiedlich groß), bevor erneut um Live-Test gebeten wurde.
|
||||
|
||||
Build grün, alle 98 Unit-Tests grün.
|
||||
Build grün, alle 98 Unit-Tests grün. Bitte nochmal live testen — diesmal an Form-lastigen Stellen (z.B. Settings-Fenster selbst, LAN-Scanner-Überschriften/Fehlertexte, nicht die zwei bewusst fixen Ausnahmen).
|
||||
|
||||
Nachbesserung 3 (Live-Test: "textgröße funktioniert, die Farbschemas nicht"): Textgröße bestätigt. Farbschema noch kaputt — beim Fix in Nachbesserung 1 nur die Übersicht-Diagrammfarben (Node-Kategorien/Kanten) überarbeitet, die LAN-Scanner-eigenen Theme-Farben (`trafficActive`/`staticLease`/`dynamicLease`/`portOpen`/`portClosed`) dabei übersehen — die waren weiterhin zu ähnliche SwiftUI-Nachbarfarben (grün/mint, orange/gelb, rot/pink), an einem kleinen Punkt/Icon praktisch nicht unterscheidbar. User fragte, ob selbst am Router geprüft werden kann — nein (kein Zugang/Tool dafür), aber Bug sitzt ohnehin rein im App-Rendering, nicht am Router: per `ImageRenderer`-Snapshot der tatsächlichen Farbwerte selbst verifiziert (Swatch-Vergleich beider Themes, sichtbar unterschiedlich) statt nur zu behaupten, es sei jetzt richtig. Neue Werte: `trafficActive`/`dynamicLease`/`portClosed` grün→blau, `staticLease` orange→lila, `portOpen` rot→orange.
|
||||
### 6. Mode-Button Setup
|
||||
**Status:** fixed (live bestätigt)
|
||||
|
||||
Bitte nochmal live testen — diesmal auch LAN-Scanner-Statuspunkte (Fest/Dynamisch, Port-Scan) einschließen, nicht nur Übersicht-Diagramm.
|
||||
Wunsch: an der Seite des Router ist eine Taste "Mode", diese ist belegbar (Scripte, Deaktivierung, etc). Einlesen, was geht, und bauen.
|
||||
|
||||
Nachbesserung 4 (Live-Test: "Lan-scanner und Übersicht funktionieren" — Farbschema damit bestätigt; aber "Experte-Tab zeigt noch keine Änderung" bei Textgröße): Ursache gefunden — die komplette Sidebar-Liste (`Text(schema.displayName)` in `ExpertView.swift`) und jedes einzelne Parameter/Wert-`TextField` in `ExpertMenuDetailView.swift` hatten von Anfang an **gar kein** `.font(...)`. Die automatisierte `.appFont(...)`-Umstellung ersetzte nur vorhandene `.font(...)`-Aufrufe — unstyled Text/TextField (SwiftUI-Systemstandard, fix) blieb dadurch unangetastet. Fix: `.environment(\.font, .system(size: 13 * scale))` einmal an der `WindowGroup`-Wurzel als Fallback — wirkt nur dort, wo kein eigenes `.appFont(...)`/`.font(...)` bereits einen Font setzt (der gewinnt lokal weiterhin), deckt aber jede unstyled Stelle app-weit ab, nicht nur den Experte-Tab.
|
||||
Recherche (help.mikrotik.com, nicht geraten): `/system routerboard mode-button` ist ein Singleton-Einstellungsmenü (kein Listen-Menü) mit drei echten Feldern:
|
||||
- `enabled` (yes/no, Standard no)
|
||||
- `on-event` (Name eines Skripts aus `/system script`)
|
||||
- `hold-time` (Zeitspanne Min..Max, z.B. `3s..5s`, verfügbar ab RouterOS 6.47beta60)
|
||||
|
||||
Build grün, alle 98 Unit-Tests grün.
|
||||
Wichtiger Fund: ab RouterOS 7.1rc4 muss jede Aktivierung/Änderung dieser Einstellung zusätzlich per physischem Tastendruck (Reset- oder Mode-Taste) am Gerät selbst innerhalb von 60 Sekunden bestätigt werden — eine Änderung allein über die App reicht nicht. Als `warning` im Schema hinterlegt, damit das im Experte-Tab sichtbar ist.
|
||||
|
||||
Nachbesserung 5 (User-Feedback: "keine Änderung beim Farbschema unter Experte") — war kein Bug, sondern Scope: Farbschema war nie für den Experte-Tab verdrahtet, dort gab's schlicht keine kategorie-eingefärbten Elemente (Sidebar/Icons alle `.secondary`). Rückfrage: soll das ausgeweitet werden? User: ja. Umsetzung: `ColorTheme.color(for: RouterOSMenuCategory)` (neu, `AppPreferences.swift`) bucketet die 13 `RouterOSMenuCategory`-Fälle auf die bestehende 5-Farb-Palette (alle 5 Firewall-Untermenüs → `.firewall`, VPN/WLAN/Queues/System/Tools → `.service`) — dieselben Farben wie im Übersicht-Diagramm, keine zweite Palette. `ExpertView.sectionHeader(...)` bekommt neuen optionalen `categoryColor`-Parameter, zeigt bei Kategorie-Sektionen einen 8pt-Farbpunkt vor dem Titel (bei "Eigener Menüpfad" weiterhin `nil`, keine Farbe).
|
||||
Umsetzung: neuer Eintrag in `RouterOSSchemaCatalog.systemFamily` (`/system routerboard mode-button`, `category: .system`, `isSingleton: true`). `on-event` nutzt `.menuItemPick(menuPath: "/system script", ...)` (wie beim bestehenden Zeitplaner) statt Freitext — zeigt eine Auswahl der bereits angelegten Skripte. `hold-time` als `.text` (kein eigener Range-Editor gebaut, dafür wäre ein neuer `RouterOSFieldSchema.Kind` nötig — Skalierungsaufwand für ein einzelnes Feld aktuell nicht gerechtfertigt, Format steht im Hilfetext). DE/EN-Übersetzungen ergänzt. Erreichbar direkt im Experte-Tab unter "System".
|
||||
|
||||
Build grün, alle 98 Unit-Tests grün.
|
||||
Build grün, alle 98 Unit-Tests grün. Nicht live geprüft (kein Router mit Mode-Taste hier erreichbar) — bitte am echten Gerät: Skript unter "Skripte" anlegen, dann unter "Mode-Taste" auswählen + aktivieren, danach die physische Bestätigung am Gerät nicht vergessen (siehe Warnhinweis).
|
||||
|
||||
6. Mode-Button Setup: an der Seite des Router ist eine Taste "Mode" diese ist belegbar (Scripte, Deaktivierung, etc). einlesen, was geht und bauen.
|
||||
7. Manual direkt in die App integrieren.
|
||||
8. Abwechselnde ´Farbkombis bei Tabellenansichten - besser Lesbarkeit
|
||||
9. Settings → Darstellung: anpassbare Farben — freie Farbwahl pro Kategorie/Status statt nur der 2 vordefinierten Themes (Standard/Kontrastreich aus Punkt 5). Beim ursprünglichen Rückfrage bewusst auf 2 Themes beschränkt; falls später gewünscht, bräuchte es ColorPicker pro `OverviewNode.Category`/`OverviewEdgeKind`/LAN-Scanner-Status/`RouterOSMenuCategory` statt der festen `ColorTheme`-Switch-Statements in `AppPreferences.swift`.
|
||||
Nachbesserung 1 (Live-Test beim Skript-Anlegen, nicht Mode-Taste selbst — Bug 40): Skript-Anlegen im Experte-Tab hing dauerhaft an "Unbekannter SSH-Schlüssel ... Bitte bestätigen", auch nach mehrfachem "Anlegen"-Klick. Ursache gefunden (Code gelesen, nicht geraten): `ExpertViewModel.saveEditingItem()`/`confirmRemoval()` und `DevicesViewModel.confirmStaticAssignment()`/`confirmStaticRemoval()` rufen vor dem eigentlichen Schreiben `ensureSessionBackup()` — die erste Sicherung pro Sitzung läuft über `BackupService`s eigene, dedizierte SSH-Verbindung. `ConnectionService.verifySSHTrust` prüft den SSH-Host-Key zwar proaktiv direkt nach jedem erfolgreichen REST-Connect (Bug 13/37-Fix), aber wenn dieser proaktive Check aus irgendeinem Grund nicht (mehr) greift, hatte der Schreibpfad selbst keine Möglichkeit, `pendingSSHTrustFingerprint` zu setzen — der Fehler landete nur als toter Text in `applyError`, ohne Trust-Dialog, exakt derselbe Grundfehler wie Bug 13/37, nur diesmal beim Schreiben statt beim Verbinden.
|
||||
|
||||
Fix: neue `ConnectionService.noteUntrustedSSHHostKey(_:)` — alle vier oben genannten Catch-Blöcke rufen sie bei `RouterOSError.untrustedSSHHostKey` auf, wodurch derselbe Trust-Dialog im Verbinden-Tab erscheint, den `verifySSHTrust` auch nutzt. Bitte nach Rebuild: nochmal "Anlegen" versuchen, diesmal im Verbinden-Tab nach einer Trust-Bestätigung schauen, dort bestätigen, dann im Experte-Tab erneut "Anlegen".
|
||||
|
||||
Build grün, alle 98 Unit-Tests grün. Nicht live geprüft (kein Router hier erreichbar).
|
||||
|
||||
Nachbesserung 2 (Live-Test: Mode-Taste speichern → "Fehlermeldung HTTP 500", gesendeter Befehl laut Vorschau-Anzeige `hold-time=""`): mit echtem Diagnose-Unit-Test nachgestellt statt weiter spekuliert — bei realistischer Datenlage (Router-GET ohne "hold-time"-Feld) entsteht kein leerer Wert, also kein einfacher "Feld war nie gesetzt"-Fall. Tatsächliche Ursache bleibt ungeklärt (kein Router zum Nachstellen erreichbar), aber der Effekt ist reproduzierbar sichtbar: ein leeres `hold-time=""` geht raus und lässt RouterOS' REST-API mit HTTP 500 abbrechen (Time-Interval-Range-Typ akzeptiert wohl keinen leeren String, anders als ein reines Textfeld).
|
||||
|
||||
Fix: neuer `RouterOSFieldSchema.clearable`-Parameter (Default `true`, bestehendes Verhalten für alle anderen Felder unverändert) — ein Feld mit `clearable: false` schickt nie ein explizites Leeren (`feld=""`), selbst wenn die Diff-Logik das sonst täte. `hold-time` ist jetzt `clearable: false`. Neuer Test `testNonClearableFieldNeverSendsExplicitEmptyValue` deckt das ab, bestehender `testClearingACuratedFieldSendsExplicitEmptyValue` (Kommentar-Feld leeren) bleibt unverändert grün — betrifft also nur `hold-time`, nicht die generelle "Feld leeren"-Funktion.
|
||||
|
||||
Build grün, alle 99 Unit-Tests grün. Bitte nochmal live testen: Mode-Taste mit "Aktiviert" + Skript speichern, "Haltedauer" leer lassen.
|
||||
|
||||
Nachbesserung 3 (Live-Test: Fehler blieb identisch, obwohl `hold-time` laut Vorschau-Anzeige jetzt korrekt weggelassen wurde — bewies, dass Nachbesserung 2 zwar wirkt, aber nicht die Ursache war): User führte denselben Befehl direkt per SSH-Terminal am Router aus (`/system routerboard mode-button set enabled=yes on-event="Button test"`) — dort lief er sofort fehlerfrei, `print` bestätigte alle drei Felder korrekt gesetzt (inkl. Werks-Default `hold-time: 0s..1m`, den es demnach doch gibt — passt zur ursprünglichen Vermutung in Nachbesserung 2, war aber am Ende nicht die eigentliche Fehlerursache). Damit zweifelsfrei bewiesen: `/system routerboard mode-button` funktioniert über SSH einwandfrei, aber nicht über REST — eine echte Deckungslücke in RouterOS' REST-API für dieses Hardware-Menü, kein App-seitiger Datenfehler.
|
||||
|
||||
Fix: neuer `RouterOSMenuSchema.writesRequireSSH`-Parameter (Default `false`) + `ConnectionService.applyViaSSH(_:)` (dedizierte SSH-Verbindung, exakt dasselbe Muster wie `BackupService`/`UpdateService`). `ExpertViewModel.saveEditingItem()` nutzt für als `writesRequireSSH: true` markierte Schemas jetzt immer diese dedizierte SSH-Verbindung statt der Session-Transport (REST oder SSH, je nachdem wie verbunden wurde). Mode-Taste-Schema entsprechend markiert.
|
||||
|
||||
Build grün, alle 99 Unit-Tests grün. Live bestätigt: Mode-Taste-Speichern über die App lief jetzt fehlerfrei (kein HTTP 500 mehr).
|
||||
|
||||
Nachbesserung 4 (Log zeigte nach Tastendruck: `script,error executing script Button test from sys2 failed, please check it manually (not enough permissions)`) — separates, tieferes RouterOS-Problem, nicht mehr die App/REST-Frage: Skripte, die vom Mode-Button ("sys2"-Kontext) ausgelöst werden, laufen mit eingeschränkten Rechten, nicht mit den vollen Standard-Skript-Rechten. Zwei Versuche schlugen fehl, weil sie nie wirklich beim Router ankamen: der Skriptname war tatsächlich "Button test" (großes B), meine Terminal-Befehle zielten auf "button test" (klein) bzw. nutzten ungültige `set`-Syntax ohne `[find ...]`/Index — RouterOS matchte nichts, `print detail` zeigte danach unverändert `dont-require-permissions=no`. Mit korrektem Index-Befehl (`/system script set 0 dont-require-permissions=yes`) griff es sofort — Log zeigt seither saubere `script,info Mode-Taste gedrückt: ...`-Einträge bei jedem Tastendruck.
|
||||
|
||||
**Fazit für jedes künftige Mode-Taste-Skript**: `dont-require-permissions=yes` setzen, sonst schlägt die Ausführung vom Button aus mit "not enough permissions" fehl, obwohl dasselbe Skript manuell/per Scheduler einwandfrei liefe. Die App hat dafür aktuell noch kein Feld im "Skripte"-Schema (`/system script` kennt nur Name+Inhalt) — bei Bedarf als eigener Punkt ergänzbar (`policy`/`dont-require-permissions`-Felder).
|
||||
|
||||
Mode-Taste (Fund #6) ist damit vollständig live verifiziert: Anlegen über App → Skript korrekt zugewiesen → Speichern ohne Fehler → Tastendruck löst Skript korrekt aus.
|
||||
|
||||
### 7. Manual direkt in die App integrieren
|
||||
**Status:** offen
|
||||
|
||||
### 8. Abwechselnde Farbkombis bei Tabellenansichten
|
||||
**Status:** offen
|
||||
|
||||
Bessere Lesbarkeit durch alternierende Zeilenfarben in Tabellenansichten (LAN-Scanner, Experte-Listen, etc.).
|
||||
|
||||
Reference in New Issue
Block a user