forked from kay/RouterOS
M25: Einrichten-Wizard-Politur + SSH-Trust-Persistenz endlich verifiziert
- WAN-Schritt: fehlenden "Zurück"-Button ergänzt (beide Modi) - "Abbrechen"/"Jetzt sichern" prominent gemacht (wie "Neu scannen") - LAN-/VLAN-Schritt: Adressfelder starten leer, nur Format-Beispiel im Feld sichtbar statt vorbelegter Werte. VlanStepView bekam dafür eine Validierungssperre auf "Weiter" (fehlte bisher, war ok solange Defaults immer gültig waren). Zwei Tests entsprechend angepasst. - SSH-Host-Key-Trust aus M24 hatte sich entgegen der Live-Bestätigung nie tatsächlich persistiert (defaults read zeigte leeren Schlüssel, Ursache ungeklärt) — betraf BackupServices dedizierte SSH-Verbindung beim ersten Experte-Tab-Schreibversuch pro Sitzung. Erneut über den bestehenden Trust-Dialog bestätigt, diesmal per defaults read verifiziert statt nur der UI-Bestätigung vertraut. Alles live bestätigt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+68
-9
@@ -1481,6 +1481,42 @@ Zeitpunkt bereits verbunden ist, würden dieselben dedizierten Dienste
|
||||
wieder wortlos nichts tun, ohne Hinweis auf die Ursache — siehe
|
||||
"Nächste Schritte".
|
||||
|
||||
**M25: Einrichten-Wizard-Politur** (2026-09-16, mehrere kleine
|
||||
Nutzerwünsche in einer Runde):
|
||||
|
||||
- **WAN-Schritt: "Zurück"-Button ergänzt** — fehlte seit jeher (nur
|
||||
"Weiter"), in beiden Modi, fiel im Experte-Modus nur eher auf, da
|
||||
dort mehr Schritte durchgeklickt werden. Live bestätigt.
|
||||
- **"Abbrechen" (Einrichten) + "Jetzt sichern" (Sicherungen) prominent
|
||||
gemacht** — dieselbe `.buttonStyle(.borderedProminent)`-Behandlung
|
||||
wie zuvor schon "Neu scannen" (Bug: "leicht zu übersehen"), jetzt
|
||||
konsequent auf alle **einzelnen, gleichrangigen** Toolbar-Aktions-
|
||||
buttons der App angewendet. Übersicht-Tabs Toolbar-Gruppe (Zoom/
|
||||
Zurücksetzen/Aktualisieren, mehrere gleichrangige Buttons
|
||||
nebeneinander) bewusst ausgenommen — dort würde durchgängiges
|
||||
Prominent-Styling überladen wirken, ein anderes UI-Muster als "eine
|
||||
einzelne leicht übersehene Hauptaktion".
|
||||
- **LAN- und VLAN-Schritt: Adressfelder starten leer** (Nutzerwunsch:
|
||||
"die auszufüllenden Felder leer lassen, nur Beispiele zeigen ...
|
||||
so sieht man das Schema dahinter") — `LanDhcpConfig`/`VlanEntry`
|
||||
hatten bisher konkrete Default-Werte (z.B. "192.168.88.1/24" bzw.
|
||||
eine aus der VLAN-ID abgeleitete Adresse); da jedes TextField-Label
|
||||
("Router-Adresse (z.B. 192.168.88.1/24)") ohnehin als Platzhalter
|
||||
dient, sobald das Feld leer ist, versteckte ein vorbelegter Wert
|
||||
dieses Beispiel bisher komplett. Betrifft beide Modi (LAN-Schritt
|
||||
wird von Einfach und Experte geteilt). `interfaceName` (Picker) und
|
||||
`leaseTimeHours` (Stepper) bleiben unverändert vorbelegt — beides
|
||||
Auswahl-UI ohne "Beispiel tippen"-Konzept. Notwendige Begleitänderung:
|
||||
`VlanStepView`s "Weiter" hatte bisher **keine** Validierungssperre
|
||||
(unnötig, solange die Defaults immer gültig waren) — jetzt analog zum
|
||||
längst vorhandenen `LanStepView`-Muster ergänzt, sonst hätte ein leer
|
||||
gelassenes VLAN-Feld einen Befehl mit leeren Argumenten zum Router
|
||||
geschickt. Zwei Tests entsprechend angepasst
|
||||
(`testDefaultAddressesStartEmptyForExamplePlaceholder` ersetzt die
|
||||
alte Ableitungs-Prüfung; `testLanDhcpCommandsCoverAddressPoolServerAndNetwork`
|
||||
setzt die Adressfelder jetzt explizit statt sich auf Modell-Defaults
|
||||
zu verlassen). Live bestätigt ("gut soweit").
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. ~~M16: restliche `RouterOSSchemaCatalog.swift`-Sektionen übersetzen~~,
|
||||
@@ -1645,15 +1681,38 @@ wieder wortlos nichts tun, ohne Hinweis auf die Ursache — siehe
|
||||
`UpdateService`/`FactoryResetService` haben keinen eigenen
|
||||
UI-Bestätigungspfad für einen neuen SSH-Host-Key, nur
|
||||
`ConnectionService.connect`s SSH-*Fallback* zeigt den Dialog — und
|
||||
der wird nie erreicht, solange REST erfolgreich verbindet. Ein
|
||||
künftiger Host-Key-Wechsel (z.B. nach einem weiteren Werksreset)
|
||||
würde also wieder zu wortlos leeren Ergebnissen führen (leere
|
||||
Traffic-Anzeige, fehlschlagende Backups, etc.), ohne erkennbaren
|
||||
Grund in der UI. Mögliche Fixes: diese Dienste bei
|
||||
`untrustedSSHHostKey` einen eigenen Bestätigungsdialog zeigen
|
||||
lassen, oder `ConnectionService` beim Verbinden zusätzlich (nicht
|
||||
nur im Fallback-Fall) einmal den SSH-Host-Key prüfen/bestätigen
|
||||
lassen, unabhängig davon, ob REST erfolgreich war.
|
||||
der wird nie erreicht, solange REST erfolgreich verbindet. Mögliche
|
||||
Fixes: diese Dienste bei `untrustedSSHHostKey` einen eigenen
|
||||
Bestätigungsdialog zeigen lassen, oder `ConnectionService` beim
|
||||
Verbinden zusätzlich (nicht nur im Fallback-Fall) einmal den
|
||||
SSH-Host-Key prüfen/bestätigen lassen, unabhängig davon, ob REST
|
||||
erfolgreich war. **Tatsächlich so eingetreten (2026-09-16):** beim
|
||||
ersten Experte-Tab-Schreibversuch einer neuen Sitzung (`ensureSessionBackup`,
|
||||
läuft einmal pro Verbindung vor der ersten Änderung) schlug
|
||||
`BackupService`s eigene, dedizierte SSH-Verbindung mit
|
||||
"Unbekannter SSH-Schlüssel" fehl — sichtbar nur als roter
|
||||
`applyError`-Text im Experte-Formular, ohne erkennbaren
|
||||
Zusammenhang zum eigentlichen SSH-Host-Key-Problem. Der erste
|
||||
Trust-Fix-Versuch von vorhin (M24-Nebenbefund) hatte sich entgegen
|
||||
der Live-Bestätigung des Nutzers ("vertraut, verbunden per SSH")
|
||||
**nicht** tatsächlich persistiert — `defaults read
|
||||
com.focus72.RouterOSAssistant
|
||||
RouterOSAssistant.TrustedSSHHostKeyFingerprints` zeigte einen
|
||||
komplett fehlenden Schlüssel, obwohl der strukturell identische,
|
||||
nachweislich funktionierende REST-Zertifikat-Trust
|
||||
(`TrustedCertificateFingerprints`) im selben Preferences-Bereich
|
||||
korrekt vorhanden war. Ursache dafür bleibt ungeklärt — der Code-Pfad
|
||||
(`ConnectViewModel.trustSSHHostKeyAndRetry` →
|
||||
`ConnectionService.trustCurrentSSHHostKeyAndRetry` →
|
||||
`SSHHostKeyTrustStore.trust`) ist exakt symmetrisch zum
|
||||
funktionierenden Zertifikat-Pfad, kein Unterschied im Code
|
||||
gefunden. Fix diesmal per `www-ssl` erneut kurz deaktiviert,
|
||||
Trust-Dialog erneut bestätigt, **und diesmal per `defaults read`
|
||||
direkt verifiziert, dass der Fingerprint wirklich persistiert wurde**
|
||||
(war er) — DHCP-Netzwerk-Anlage danach live erfolgreich. Falls
|
||||
dieses Symptom nochmal auftritt: zuerst per `defaults read` prüfen,
|
||||
ob der Trust wirklich gespeichert wurde, nicht nur der UI-Bestätigung
|
||||
vertrauen.
|
||||
|
||||
Gitea-Remote `origin` ist eingerichtet und wird laufend gepusht (siehe
|
||||
oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15.
|
||||
|
||||
@@ -89,7 +89,12 @@ Geführter Schritt-für-Schritt-Assistent für die Grundkonfiguration:
|
||||
getrennte Netzwerke einrichten, jeweils mit eigenem Adressbereich und
|
||||
optionaler **Netzwerk-Isolation** (Geräte in diesem Netz erreichen
|
||||
keine anderen konfigurierten Netze, Internetzugriff bleibt erhalten).
|
||||
Adressfelder starten leer — das Feld selbst zeigt ein Beispielformat
|
||||
(z.B. "Router-Adresse (z.B. 192.168.88.1/24)"), das beim Tippen
|
||||
verschwindet.
|
||||
- **VLAN**: zusätzliche virtuelle Netzwerke auf demselben Kabel.
|
||||
Adressfelder starten hier ebenfalls leer, mit Beispielformat im
|
||||
jeweiligen Feld.
|
||||
- **Firewall-Grundschutz**: Internetfreigabe (NAT) plus Schutz vor
|
||||
unaufgeforderten Zugriffen aus dem Internet — mit einem Klick
|
||||
aktivierbar, bestehende eigene Regeln bleiben erhalten.
|
||||
@@ -263,8 +268,13 @@ Guided step-by-step assistant for base configuration:
|
||||
Expert mode, multiple physical ports/interfaces can be set up as
|
||||
separate networks, each with its own address range and optional
|
||||
**network isolation** (devices on this network can't reach any other
|
||||
configured network, while internet access still works).
|
||||
- **VLAN**: additional virtual networks on the same cable.
|
||||
configured network, while internet access still works). Address
|
||||
fields start empty — the field itself shows a format example (e.g.
|
||||
"Router Address (e.g. 192.168.88.1/24)"), which disappears as you
|
||||
type.
|
||||
- **VLAN**: additional virtual networks on the same cable. Address
|
||||
fields here start empty too, with a format example shown in each
|
||||
field.
|
||||
- **Basic firewall**: internet sharing (NAT) plus protection against
|
||||
unsolicited access from the internet — one click to enable, existing
|
||||
custom rules are preserved.
|
||||
|
||||
@@ -157,6 +157,7 @@ nur die zugehörigen Passwörter liegen weiterhin im macOS-Schlüsselbund.
|
||||
| M22 | REST-Transport (M7) erstmals live gegen Hardware verifiziert, 4 Bugs gefunden+gefixt | ✅ live verifiziert |
|
||||
| M23 | Experte-Tab: Sektionsüberschriften prominenter+eingefärbt, einklappbar (Standard: zugeklappt) | ✅ live verifiziert |
|
||||
| M24 | LAN-Scanner: "Aktionen"-Button statt Rechtsklick, Traffic-Monitor+Sparkline pro Port, ARP-Bug gefixt | ✅ live verifiziert |
|
||||
| M25 | Einrichten-Wizard-Politur: WAN-Zurück-Button, prominente Aktionsbuttons, leere Platzhalter-Felder (LAN/VLAN) | ✅ live verifiziert |
|
||||
| — | LAN-Port-Konflikt-Prüfung + "Fertig"-Button (Einrichten) | 🔶 gebaut, Live-Test offen |
|
||||
|
||||
Ausführlicher Stand inkl. aller gefundenen Bugs, offener Punkte und
|
||||
|
||||
@@ -3,12 +3,18 @@ import Foundation
|
||||
struct LanDhcpConfig: Identifiable, Equatable {
|
||||
var id = UUID()
|
||||
var interfaceName: String = "bridge"
|
||||
var networkAddress: String = "192.168.88.0/24"
|
||||
var routerAddress: String = "192.168.88.1/24"
|
||||
var poolRangeStart: String = "192.168.88.10"
|
||||
var poolRangeEnd: String = "192.168.88.254"
|
||||
/// Empty on purpose (Nutzerwunsch: "die auszufüllenden Felder leer lassen, nur Beispiele
|
||||
/// zeigen ... so sieht man das Schema dahinter") — each TextField's label (e.g.
|
||||
/// "Router-Adresse (z.B. 192.168.88.1/24)") doubles as its placeholder while empty, so a
|
||||
/// pre-filled value would hide the format example instead of showing it. `interfaceName`
|
||||
/// above and `leaseTimeHours` below stay as real defaults — a Picker selection and a
|
||||
/// Stepper, neither has a "type an example" UI to hide.
|
||||
var networkAddress: String = ""
|
||||
var routerAddress: String = ""
|
||||
var poolRangeStart: String = ""
|
||||
var poolRangeEnd: String = ""
|
||||
var leaseTimeHours: Int = 24
|
||||
var dnsServers: String = "192.168.88.1"
|
||||
var dnsServers: String = ""
|
||||
/// Blocks forward traffic to/from every other configured LAN/VLAN network — see
|
||||
/// `FirewallConfig.NetworkSegment`. Internet access (WAN NAT) is unaffected.
|
||||
var isolated: Bool = false
|
||||
|
||||
@@ -21,16 +21,21 @@ struct VlanEntry: Identifiable, Equatable {
|
||||
/// `FirewallConfig.NetworkSegment`. Internet access (WAN NAT) is unaffected.
|
||||
var isolated: Bool = false
|
||||
|
||||
/// Fields start empty on purpose (Nutzerwunsch: "die auszufüllenden Felder leer lassen,
|
||||
/// nur Beispiele zeigen ... so sieht man das Schema dahinter") — each TextField's label
|
||||
/// (e.g. "Router-Adresse (z.B. 192.168.20.1/24)") doubles as its placeholder while empty,
|
||||
/// so leaving these blank shows the format example instead of hiding it behind an
|
||||
/// auto-derived value the user might not even notice, let alone check. `VlanStepView`'s
|
||||
/// "Weiter" button is disabled until every VLAN's fields are actually filled in.
|
||||
init(name: String = "Gäste", vlanID: Int, parentInterface: String) {
|
||||
self.name = name
|
||||
self.vlanID = vlanID
|
||||
self.parentInterface = parentInterface
|
||||
let octet = vlanID % 256
|
||||
self.networkAddress = "192.168.\(octet).0/24"
|
||||
self.routerAddress = "192.168.\(octet).1/24"
|
||||
self.poolRangeStart = "192.168.\(octet).10"
|
||||
self.poolRangeEnd = "192.168.\(octet).254"
|
||||
self.dnsServers = "192.168.\(octet).1"
|
||||
self.networkAddress = ""
|
||||
self.routerAddress = ""
|
||||
self.poolRangeStart = ""
|
||||
self.poolRangeEnd = ""
|
||||
self.dnsServers = ""
|
||||
}
|
||||
|
||||
var interfaceName: String { "vlan\(vlanID)" }
|
||||
|
||||
@@ -223,6 +223,9 @@ struct BackupListView: View {
|
||||
Label(L10n.t("Jetzt sichern", appLanguage), systemImage: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
// Same precedent as LAN-Scanner's "Neu scannen"/Einrichten's "Abbrechen" —
|
||||
// a plain text toolbar button is easy to miss, applied app-wide now.
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(connectionService.credentials == nil || viewModel.isCreatingBackup)
|
||||
.help(connectionService.credentials == nil ? L10n.t("Zuerst im Tab 'Verbinden' mit dem Router verbinden.", appLanguage) : L10n.t("Sicherung jetzt erstellen", appLanguage))
|
||||
}
|
||||
|
||||
@@ -45,6 +45,13 @@ struct SetupView: View {
|
||||
Button(L10n.t("Abbrechen", appLanguage)) {
|
||||
showCancelConfirmation = true
|
||||
}
|
||||
// Same reasoning/precedent as LAN-Scanner's "Neu scannen" button
|
||||
// (Bug/Nutzerwunsch: "wird übersehen") — a plain text toolbar button
|
||||
// blends into the window chrome; `.borderedProminent` gives it a
|
||||
// filled, colored background so it's actually noticeable. Applied
|
||||
// consistently to every standalone toolbar action button in the app
|
||||
// per explicit request ("für die gesamte App fixen").
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(viewModel.isApplying)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,24 @@ struct VlanStepView: View {
|
||||
Button(L10n.t("Zurück", appLanguage)) { viewModel.goBack() }
|
||||
Spacer()
|
||||
Button(L10n.t("Weiter", appLanguage)) { viewModel.goNext() }
|
||||
.disabled(!isStepValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("Zusätzliche Netzwerke (VLAN)", appLanguage)))
|
||||
}
|
||||
|
||||
/// Only gates on filled-in fields while the VLAN section is actually enabled — with it off,
|
||||
/// `viewModel.vlans` is empty (see `setVlanSectionEnabled`) and there's nothing to validate.
|
||||
/// Needed as of the fields-start-empty change: previously every VLAN's address fields had a
|
||||
/// valid auto-derived default, so "Weiter" was always safe to press; now an incompletely
|
||||
/// filled-in VLAN would otherwise reach `buildCommands()` with blank arguments.
|
||||
private var isStepValid: Bool {
|
||||
guard viewModel.vlanSectionEnabled else { return true }
|
||||
return viewModel.vlans.allSatisfy { vlan in
|
||||
!vlan.name.isEmpty && !vlan.networkAddress.isEmpty && !vlan.routerAddress.isEmpty
|
||||
&& !vlan.poolRangeStart.isEmpty && !vlan.poolRangeEnd.isEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ struct WanStepView: View {
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Button(L10n.t("Zurück", appLanguage)) { viewModel.goBack() }
|
||||
Spacer()
|
||||
Button(L10n.t("Weiter", appLanguage)) { viewModel.goNext() }
|
||||
.disabled(!isStepValid)
|
||||
|
||||
@@ -40,7 +40,15 @@ final class RouterOSCommandBuilderTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testLanDhcpCommandsCoverAddressPoolServerAndNetwork() {
|
||||
let config = LanDhcpConfig()
|
||||
// Address fields default to "" now (Nutzerwunsch, 2026-09-16: fields start empty,
|
||||
// showing only the format example) — set explicitly here so this test keeps verifying
|
||||
// the command-building logic itself, not incidentally the model's old default values.
|
||||
var config = LanDhcpConfig()
|
||||
config.networkAddress = "192.168.88.0/24"
|
||||
config.routerAddress = "192.168.88.1/24"
|
||||
config.poolRangeStart = "192.168.88.10"
|
||||
config.poolRangeEnd = "192.168.88.254"
|
||||
config.dnsServers = "192.168.88.1"
|
||||
let commands = config.buildCommands()
|
||||
|
||||
XCTAssertEqual(commands.count, 6)
|
||||
|
||||
@@ -23,12 +23,16 @@ final class VlanEntryTests: XCTestCase {
|
||||
XCTAssertEqual(commands[7].arguments["interface"], "vlan20")
|
||||
}
|
||||
|
||||
func testDefaultAddressesAreDerivedFromVlanID() {
|
||||
/// Address fields start empty by design (Nutzerwunsch, 2026-09-16: "die auszufüllenden
|
||||
/// Felder leer lassen, nur Beispiele zeigen ... so sieht man das Schema dahinter") — a
|
||||
/// previous version auto-derived these from the VLAN ID; replaced with this test.
|
||||
func testDefaultAddressesStartEmptyForExamplePlaceholder() {
|
||||
let vlan = VlanEntry(vlanID: 30, parentInterface: "bridge")
|
||||
|
||||
XCTAssertEqual(vlan.routerAddress, "192.168.30.1/24")
|
||||
XCTAssertEqual(vlan.networkAddress, "192.168.30.0/24")
|
||||
XCTAssertEqual(vlan.poolRangeStart, "192.168.30.10")
|
||||
XCTAssertEqual(vlan.poolRangeEnd, "192.168.30.254")
|
||||
XCTAssertEqual(vlan.routerAddress, "")
|
||||
XCTAssertEqual(vlan.networkAddress, "")
|
||||
XCTAssertEqual(vlan.poolRangeStart, "")
|
||||
XCTAssertEqual(vlan.poolRangeEnd, "")
|
||||
XCTAssertEqual(vlan.dnsServers, "")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user