Fix 4 Bugs aus systematischem Tester-Durchgang (bugs.md)
Race Condition bei LAN-Port-Konflikt-Prüfung (Generation-Zähler), Firewall-Titel widersprach sich im Einfach-Modus, 2 fehlende EN-Übersetzungen, Health-Check- Herzschlag ignorierte laufende Wizard-/Experte-Schreibvorgänge. Außerdem M7/M9 im README/HANDOFF auf fertig aktualisiert (Modusschalter+Experte-Zweig liefen bereits über die Multi-LAN/WLAN-Live-Tests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+15
-9
@@ -996,24 +996,29 @@ Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und
|
|||||||
wiederherstellen" (Gefahrenzone im Sicherungen-Tab,
|
wiederherstellen" (Gefahrenzone im Sicherungen-Tab,
|
||||||
`/system reset-configuration no-defaults=no`), eigenes App-Icon
|
`/system reset-configuration no-defaults=no`), eigenes App-Icon
|
||||||
("Signal Router"-Motiv).
|
("Signal Router"-Motiv).
|
||||||
- 🔶 M7: Härtung — SSH-Hostkey-TOFU **fertig, gegen echte Hardware
|
- ✅ M7: Härtung — SSH-Hostkey-TOFU **fertig, gegen echte Hardware
|
||||||
bestätigt** (inkl. neuem "Trennen"-Button im Verbinden-Tab, der dafür
|
bestätigt** (inkl. neuem "Trennen"-Button im Verbinden-Tab, der dafür
|
||||||
nötig wurde). REST-Schreibpfad gegen `www-ssl` seit 2026-09-16 verifiziert
|
nötig wurde). REST-Schreibpfad gegen `www-ssl` seit 2026-09-16 verifiziert
|
||||||
(Bug 30–32). Dabei zusätzlich Bug 37 gefixt: REST-Erstverbindungen
|
(Bug 30–32). Dabei zusätzlich Bug 37 gefixt: REST-Erstverbindungen
|
||||||
etablieren jetzt automatisch auch SSH-Trust im Hintergrund, statt dass
|
etablieren jetzt automatisch auch SSH-Trust im Hintergrund, statt dass
|
||||||
dedizierte SSH-Dienste (Backup u.a.) beim ersten Zugriff mit einem
|
dedizierte SSH-Dienste (Backup u.a.) beim ersten Zugriff mit einem
|
||||||
unbestätigbaren Hostkey-Fehler dead-enden (live bestätigt, "passt").
|
unbestätigbaren Hostkey-Fehler dead-enden (live bestätigt, "passt").
|
||||||
Verbleibend: REST-Fehlerzustände bei Verbindungsabbruch mitten im Apply
|
Verbindungsabbruch mitten im Apply per Code-Review bestätigt (sauberer
|
||||||
noch nicht gezielt geprüft.
|
`do/catch`-Abbruch, `applyLog` zeigt Fortschritt, Backup davor als Netz)
|
||||||
|
— bewusst nicht live erzwungen.
|
||||||
- ✅ M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
|
- ✅ M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
|
||||||
(eigene Firewall-Regeln pro LAN/VLAN) — **live gegen Hardware
|
(eigene Firewall-Regeln pro LAN/VLAN) — **live gegen Hardware
|
||||||
verifiziert**, sowohl manuell (SSH) als auch über den App-Wizard
|
verifiziert**, sowohl manuell (SSH) als auch über den App-Wizard
|
||||||
selbst (`ether4`), dabei drei App-Bugs gefunden+gefixt (Bug 22–24,
|
selbst (`ether4`), dabei drei App-Bugs gefunden+gefixt (Bug 22–24,
|
||||||
siehe oben). Isolation, DNS und Internet vom Nutzer am echten Gerät
|
siehe oben). Isolation, DNS und Internet vom Nutzer am echten Gerät
|
||||||
bestätigt.
|
bestätigt.
|
||||||
- 🔶 M9: Einfach/Experte-Modus im Einrichten-Wizard — gebaut, Compile/
|
- ✅ M9: Einfach/Experte-Modus im Einrichten-Wizard — Modusschalter +
|
||||||
Unit-Test-verifiziert. UI (Modusumschalter selbst) noch nicht manuell
|
Experte-Zweig **live gegen Hardware bestätigt**: der Multi-LAN/
|
||||||
durchgeklickt — nur M10s Experte-Tab wurde das (siehe M10).
|
Isolation-Test (2026-09-15, `ether4`) und der WLAN-Test (2026-09-17,
|
||||||
|
hAP lite) liefen beide über den App-Wizard im Experte-Modus (Multi-LAN,
|
||||||
|
VLAN-Schritt, Isolation-Toggle — alles nur im Experte-Zweig vorhanden).
|
||||||
|
Einfach-Zweig selbst (vereinfachter Pfad ohne VLAN/Isolation) nicht
|
||||||
|
separat live geklickt.
|
||||||
- ✅ M10: Experte-Tab (generischer RouterOS-Zugriff + kuratierte Firewall-
|
- ✅ M10: Experte-Tab (generischer RouterOS-Zugriff + kuratierte Firewall-
|
||||||
und weitere Schemas) — Kern-Logik **live gegen Hardware verifiziert**,
|
und weitere Schemas) — Kern-Logik **live gegen Hardware verifiziert**,
|
||||||
UI (VLAN anlegen/löschen, DHCP-Server mit Adress-Pool zuweisen) **vom
|
UI (VLAN anlegen/löschen, DHCP-Server mit Adress-Pool zuweisen) **vom
|
||||||
@@ -1632,9 +1637,10 @@ verallgemeinert** (2026-09-16, beim Live-Test von M26 gefunden):
|
|||||||
REST-zuerst verbindet statt SSH-Fallback. Fehlerzustände/Politur im
|
REST-zuerst verbindet statt SSH-Fallback. Fehlerzustände/Politur im
|
||||||
REST-Pfad noch nicht gezielt geprüft (z.B. Verbindungsabbruch
|
REST-Pfad noch nicht gezielt geprüft (z.B. Verbindungsabbruch
|
||||||
mitten im Apply) — optional für später.
|
mitten im Apply) — optional für später.
|
||||||
3. M9 UI (Einfach/Experte-Modusumschalter im Einrichten-Tab selbst) noch
|
3. ~~M9 UI (Einfach/Experte-Modusumschalter im Einrichten-Tab selbst)
|
||||||
manuell durchklicken — M10s Experte-Tab wurde bereits vom Nutzer
|
durchklicken~~ — der Experte-Zweig lief bereits über den App-Wizard bei
|
||||||
bestätigt (siehe oben), der Moduswechsel im Wizard noch nicht.
|
den Multi-LAN/Isolation- (2026-09-15) und WLAN-Tests (2026-09-17),
|
||||||
|
siehe M9-Zeile oben. Einfach-Zweig weiterhin nicht separat geklickt.
|
||||||
4. M10: ~~WLAN-Schemas (an Gerät mit WLAN-Chip)~~ — erledigt
|
4. M10: ~~WLAN-Schemas (an Gerät mit WLAN-Chip)~~ — erledigt
|
||||||
(2026-09-17, hAP lite). ~~Bonding~~ — erledigt (2026-09-17, hAP
|
(2026-09-17, hAP lite). ~~Bonding~~ — erledigt (2026-09-17, hAP
|
||||||
lite, `ether3`+`ether4`, `mode=active-backup`): `/interface bonding
|
lite, `ether3`+`ether4`, `mode=active-backup`): `/interface bonding
|
||||||
|
|||||||
@@ -149,9 +149,9 @@ nur die zugehörigen Passwörter liegen weiterhin im macOS-Schlüsselbund.
|
|||||||
| M1–M4 | Projektgerüst, Connect, Backup, WAN/LAN/DHCP, VLAN | ✅ live verifiziert |
|
| M1–M4 | Projektgerüst, Connect, Backup, WAN/LAN/DHCP, VLAN | ✅ live verifiziert |
|
||||||
| M5 | WLAN-Schritt (Legacy-Treiber, `/interface wireless`) | ✅ live verifiziert (hAP lite, Smartphone verbunden) |
|
| M5 | WLAN-Schritt (Legacy-Treiber, `/interface wireless`) | ✅ live verifiziert (hAP lite, Smartphone verbunden) |
|
||||||
| M6 | Firewall-Grundschutz | ✅ live verifiziert |
|
| M6 | Firewall-Grundschutz | ✅ live verifiziert |
|
||||||
| M7 | Härtung (SSH-Hostkey-TOFU) | 🔶 TOFU + REST-Schreibpfad live verifiziert; Verbindungsabbruch-mitten-im-Apply per Code-Review bestätigt (sauberer `do/catch`-Abbruch, kein Absturz/Hänger, `applyLog` zeigt Fortschritt, Backup davor als Netz) — bewusst nicht live erzwungen (Risiko eines halb-konfigurierten Routers unverhältnismäßig zum Erkenntnisgewinn) |
|
| M7 | Härtung (SSH-Hostkey-TOFU) | ✅ TOFU + REST-Schreibpfad live verifiziert; Verbindungsabbruch-mitten-im-Apply per Code-Review bestätigt (sauberer `do/catch`-Abbruch, kein Absturz/Hänger, `applyLog` zeigt Fortschritt, Backup davor als Netz) — bewusst nicht live erzwungen (Risiko eines halb-konfigurierten Routers unverhältnismäßig zum Erkenntnisgewinn) |
|
||||||
| M8 | Mehrere LAN-Interfaces + Netzwerk-Isolation | ✅ live verifiziert |
|
| M8 | Mehrere LAN-Interfaces + Netzwerk-Isolation | ✅ live verifiziert |
|
||||||
| M9 | Einfach/Experte-Modusschalter im Wizard | 🔶 gebaut, UI-Test offen |
|
| M9 | Einfach/Experte-Modusschalter im Wizard | ✅ Modusschalter + Experte-Zweig live verifiziert (Multi-LAN/Isolation-Test 2026-09-15, WLAN-Test 2026-09-17 liefen beide über den App-Wizard im Experte-Modus); Einfach-Zweig selbst nicht separat live geklickt |
|
||||||
| M10 | Experte-Tab (generischer RouterOS-Zugriff) | ✅ live verifiziert |
|
| M10 | Experte-Tab (generischer RouterOS-Zugriff) | ✅ live verifiziert |
|
||||||
| M11 | Übersicht-Tab (IST-Zustand-Diagramm) | ✅ live verifiziert |
|
| M11 | Übersicht-Tab (IST-Zustand-Diagramm) | ✅ live verifiziert |
|
||||||
| M12 | Geräte-Tab (LAN-Scanner + Static-IP) | ✅ live verifiziert |
|
| M12 | Geräte-Tab (LAN-Scanner + Static-IP) | ✅ live verifiziert |
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ enum L10n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static let translations: [String: String] = [
|
private static let translations: [String: String] = [
|
||||||
|
"Netzwerk-Test fehlgeschlagen": "Network test failed",
|
||||||
|
"Fokus-Ansicht schließen": "Close focus view",
|
||||||
|
"Firewall": "Firewall",
|
||||||
"Verbinden": "Connect",
|
"Verbinden": "Connect",
|
||||||
"Verbindung unterbrochen — versuche automatisch, erneut zu verbinden…":
|
"Verbindung unterbrochen — versuche automatisch, erneut zu verbinden…":
|
||||||
"Connection lost — trying to reconnect automatically…",
|
"Connection lost — trying to reconnect automatically…",
|
||||||
|
|||||||
@@ -42,6 +42,22 @@ final class ConnectionService: ObservableObject {
|
|||||||
private var healthMonitorTask: Task<Void, Never>?
|
private var healthMonitorTask: Task<Void, Never>?
|
||||||
private static let healthCheckInterval: Duration = .seconds(10)
|
private static let healthCheckInterval: Duration = .seconds(10)
|
||||||
private static let reconnectRetryInterval: Duration = .seconds(5)
|
private static let reconnectRetryInterval: Duration = .seconds(5)
|
||||||
|
/// How many callers currently consider themselves "mid-write" (Setup-Wizard apply, Experte-Tab
|
||||||
|
/// save/remove) — while non-zero, the health-check heartbeat below skips its round so a slow
|
||||||
|
/// but otherwise-succeeding apply on weak hardware doesn't get mistaken for a dropped
|
||||||
|
/// connection and trigger a spurious "Wiederverbinden…" banner (bugs.md #4, 2026-09-17).
|
||||||
|
private var activeWriteCount = 0
|
||||||
|
|
||||||
|
/// Call before a multi-step write (Setup-Wizard apply, Experte-Tab save/remove) starts, paired
|
||||||
|
/// with `endWrite()` once it finishes (success or failure) — see `activeWriteCount`'s doc
|
||||||
|
/// comment.
|
||||||
|
func beginWrite() {
|
||||||
|
activeWriteCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func endWrite() {
|
||||||
|
activeWriteCount = max(0, activeWriteCount - 1)
|
||||||
|
}
|
||||||
|
|
||||||
private let certificateTrust: CertificateTrustStore
|
private let certificateTrust: CertificateTrustStore
|
||||||
private let sshHostKeyTrust: SSHHostKeyTrustStore
|
private let sshHostKeyTrust: SSHHostKeyTrustStore
|
||||||
@@ -271,7 +287,7 @@ final class ConnectionService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func checkConnectionHealthAndReconnectIfNeeded() async {
|
private func checkConnectionHealthAndReconnectIfNeeded() async {
|
||||||
guard case .connected = state, !isReconnecting,
|
guard case .connected = state, !isReconnecting, activeWriteCount == 0,
|
||||||
let activeTransport, let credentials else { return }
|
let activeTransport, let credentials else { return }
|
||||||
do {
|
do {
|
||||||
_ = try await activeTransport.fetchMenuItems(menuPath: "/system identity", restPath: "system/identity")
|
_ = try await activeTransport.fetchMenuItems(menuPath: "/system identity", restPath: "system/identity")
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ final class ExpertViewModel: ObservableObject {
|
|||||||
guard let command = pendingCommand else { return }
|
guard let command = pendingCommand else { return }
|
||||||
isApplying = true
|
isApplying = true
|
||||||
applyError = nil
|
applyError = nil
|
||||||
|
connectionService.beginWrite()
|
||||||
|
defer { connectionService.endWrite() }
|
||||||
do {
|
do {
|
||||||
try await ensureSessionBackup()
|
try await ensureSessionBackup()
|
||||||
if selectedSchema?.writesRequireSSH == true {
|
if selectedSchema?.writesRequireSSH == true {
|
||||||
@@ -239,6 +241,8 @@ final class ExpertViewModel: ObservableObject {
|
|||||||
guard let schema = selectedSchema else { return }
|
guard let schema = selectedSchema else { return }
|
||||||
isApplying = true
|
isApplying = true
|
||||||
applyError = nil
|
applyError = nil
|
||||||
|
connectionService.beginWrite()
|
||||||
|
defer { connectionService.endWrite() }
|
||||||
do {
|
do {
|
||||||
let command = RouterOSCommand.remove(
|
let command = RouterOSCommand.remove(
|
||||||
menuPath: schema.menuPath, restPath: schema.restPath,
|
menuPath: schema.menuPath, restPath: schema.restPath,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ struct FirewallStepView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
.navigationTitle(LocalizedStringKey(L10n.t("Firewall (optional)", appLanguage)))
|
.navigationTitle(LocalizedStringKey(L10n.t(viewModel.mode == .expert ? "Firewall (optional)" : "Firewall", appLanguage)))
|
||||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepFirewall) } }
|
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepFirewall) } }
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if viewModel.firewallSectionEnabled {
|
if viewModel.firewallSectionEnabled {
|
||||||
|
|||||||
@@ -134,8 +134,15 @@ final class SetupViewModel: ObservableObject {
|
|||||||
lanPortConflicts[id] = nil
|
lanPortConflicts[id] = nil
|
||||||
acknowledgedPortConflicts.remove(id)
|
acknowledgedPortConflicts.remove(id)
|
||||||
isCheckingPortConflict.remove(id)
|
isCheckingPortConflict.remove(id)
|
||||||
|
portConflictRequestGeneration[id] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bumped on every `checkPortConflict(for:)` call for a given config, so a slower, older
|
||||||
|
/// in-flight check can recognize it's been superseded and discard its own result instead of
|
||||||
|
/// racing the newer one — see that function's doc comment for the concrete failure this
|
||||||
|
/// prevents (bugs.md #1, 2026-09-17).
|
||||||
|
private var portConflictRequestGeneration: [LanDhcpConfig.ID: Int] = [:]
|
||||||
|
|
||||||
/// Live-checks whether the port currently picked for this LAN config already carries other
|
/// Live-checks whether the port currently picked for this LAN config already carries other
|
||||||
/// configuration (bridge membership, an existing address, WAN dial-up) — called when the LAN
|
/// configuration (bridge membership, an existing address, WAN dial-up) — called when the LAN
|
||||||
/// step appears and whenever its interface Picker selection changes. Any prior acknowledgement
|
/// step appears and whenever its interface Picker selection changes. Any prior acknowledgement
|
||||||
@@ -143,14 +150,24 @@ final class SetupViewModel: ObservableObject {
|
|||||||
/// port says nothing about the newly picked one. Best-effort — a failed check (e.g. transient
|
/// port says nothing about the newly picked one. Best-effort — a failed check (e.g. transient
|
||||||
/// connection hiccup) must not block the wizard; the user simply doesn't get the extra warning
|
/// connection hiccup) must not block the wizard; the user simply doesn't get the extra warning
|
||||||
/// for that attempt, same as before this feature existed.
|
/// for that attempt, same as before this feature existed.
|
||||||
|
///
|
||||||
|
/// Two call sites (`.onAppear` and `.onChange(of: interfaceName)`) can fire in quick
|
||||||
|
/// succession for the same `configID` while a previous check is still in flight — without the
|
||||||
|
/// generation guard below, a slower-but-older response could land after a faster-but-newer one
|
||||||
|
/// and overwrite it with a stale port's result (bugs.md #1).
|
||||||
func checkPortConflict(for configID: LanDhcpConfig.ID) {
|
func checkPortConflict(for configID: LanDhcpConfig.ID) {
|
||||||
guard let config = lanConfigs.first(where: { $0.id == configID }) else { return }
|
guard let config = lanConfigs.first(where: { $0.id == configID }) else { return }
|
||||||
|
let interfaceName = config.interfaceName
|
||||||
acknowledgedPortConflicts.remove(configID)
|
acknowledgedPortConflicts.remove(configID)
|
||||||
lanPortConflicts[configID] = nil
|
lanPortConflicts[configID] = nil
|
||||||
isCheckingPortConflict.insert(configID)
|
isCheckingPortConflict.insert(configID)
|
||||||
|
let generation = (portConflictRequestGeneration[configID] ?? 0) + 1
|
||||||
|
portConflictRequestGeneration[configID] = generation
|
||||||
Task {
|
Task {
|
||||||
defer { isCheckingPortConflict.remove(configID) }
|
let result = try? await connectionService.checkPortConflict(interfaceName: interfaceName)
|
||||||
lanPortConflicts[configID] = try? await connectionService.checkPortConflict(interfaceName: config.interfaceName)
|
guard portConflictRequestGeneration[configID] == generation else { return }
|
||||||
|
lanPortConflicts[configID] = result
|
||||||
|
isCheckingPortConflict.remove(configID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +299,7 @@ final class SetupViewModel: ObservableObject {
|
|||||||
lanPortConflicts = [:]
|
lanPortConflicts = [:]
|
||||||
isCheckingPortConflict = []
|
isCheckingPortConflict = []
|
||||||
acknowledgedPortConflicts = []
|
acknowledgedPortConflicts = []
|
||||||
|
portConflictRequestGeneration = [:]
|
||||||
applyLog = []
|
applyLog = []
|
||||||
applyError = nil
|
applyError = nil
|
||||||
didApplySuccessfully = false
|
didApplySuccessfully = false
|
||||||
@@ -316,6 +334,8 @@ final class SetupViewModel: ObservableObject {
|
|||||||
didApplySuccessfully = false
|
didApplySuccessfully = false
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
connectionService.beginWrite()
|
||||||
|
defer { connectionService.endWrite() }
|
||||||
do {
|
do {
|
||||||
applyLog.append("Sichere aktuelle Konfiguration…")
|
applyLog.append("Sichere aktuelle Konfiguration…")
|
||||||
_ = try await backupService.createBackup(for: credentials)
|
_ = try await backupService.createBackup(for: credentials)
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# bugs.md
|
||||||
|
|
||||||
|
Ergebnis eines systematischen Tester-/Senior-Dev-Durchgangs (2026-09-17) gegen den echten
|
||||||
|
hAP lite Testrouter (192.168.88.1, RouterOS 7.24.4). Hinweis zur Methode: die App-UI selbst
|
||||||
|
konnte nicht geklickt werden (keine macOS-UI-Automatisierung verfügbar) — stattdessen
|
||||||
|
Code-Audit jedes Feature-Bereichs, Router-Ground-Truth per SSH gegenprüft (`/export terse`,
|
||||||
|
Live-Konfiguration), und wo möglich per Skript verifiziert statt geraten (siehe je Eintrag).
|
||||||
|
Router-Zustand zum Testzeitpunkt: nahezu Werkszustand, nur die WLAN-Testkonfiguration aus
|
||||||
|
einer früheren Session vorhanden.
|
||||||
|
|
||||||
|
**Status:** `offen` | `postponed` | `fixed`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-09-17
|
||||||
|
|
||||||
|
### 1. Race Condition bei Port-Konflikt-Prüfung im LAN-Schritt
|
||||||
|
**Status:** fixed (Build grün, noch nicht live gegenreproduziert)
|
||||||
|
**Confidence:** hoch (Logikfehler direkt im Code nachvollzogen, nicht live reproduziert)
|
||||||
|
|
||||||
|
`SetupViewModel.checkPortConflict(for:)` (`SetupViewModel.swift:146`) liest `config.interfaceName`
|
||||||
|
synchron beim Aufruf und startet dann einen `Task`, der das Ergebnis nach dem `await` unbedingt in
|
||||||
|
`lanPortConflicts[configID]` schreibt — ohne Generation-Counter oder Abbruch des vorherigen `Task`.
|
||||||
|
|
||||||
|
Der Aufruf passiert an zwei Stellen für dieselbe `configID`: `.onAppear` (bei jedem Erscheinen des
|
||||||
|
LAN-Schritts) und `.onChange(of: config.interfaceName)` (bei jeder Port-Auswahl). Wechselt der
|
||||||
|
Nutzer den Port zügig zweimal hintereinander (oder wechselt ihn, bevor die `onAppear`-Prüfung des
|
||||||
|
vorherigen Ports fertig ist), können zwei `Task`s parallel laufen. Der zuerst gestartete, aber
|
||||||
|
zuletzt fertige Task überschreibt das Ergebnis des neueren mit dem Stand des alten Ports — "last
|
||||||
|
response wins" statt "last request wins".
|
||||||
|
|
||||||
|
**Konkretes Fehlerbild:** Nutzer wählt Port A (hat Konflikt, z.B. Bridge-Mitgliedschaft), App prüft
|
||||||
|
noch, Nutzer wechselt schnell zu Port B (frei). Kommt Port As Prüfergebnis später zurück als Port
|
||||||
|
Bs, zeigt die App fälschlich eine Konflikt-Warnung für den inzwischen ausgewählten, tatsächlich
|
||||||
|
freien Port B — oder, im umgekehrten Fall, verschluckt eine echte Warnung für einen Port, der
|
||||||
|
tatsächlich bereits belegt ist, sodass "Weiter" freigeschaltet wird, obwohl der gewählte Port beim
|
||||||
|
Anwenden unbemerkt vorhandene Konfiguration überschreibt.
|
||||||
|
|
||||||
|
**Fix:** Generation-Zähler `portConflictRequestGeneration: [LanDhcpConfig.ID: Int]` ergänzt, bei
|
||||||
|
jedem `checkPortConflict(for:)`-Aufruf hochgezählt; ein abgeschlossener `Task` schreibt sein
|
||||||
|
Ergebnis nur, wenn seine Generation beim Abschluss noch die aktuellste ist — ein überholter Task
|
||||||
|
verwirft sein Ergebnis stillschweigend statt es zu übernehmen.
|
||||||
|
|
||||||
|
### 2. "Firewall (optional)"-Titel widerspricht sich selbst im Einfach-Modus
|
||||||
|
**Status:** fixed (Build grün, noch nicht live gegenreproduziert)
|
||||||
|
**Confidence:** hoch (direkt im Code sichtbar)
|
||||||
|
|
||||||
|
`FirewallStepView.swift:63`: `.navigationTitle(... "Firewall (optional)" ...)` ist fest, unabhängig
|
||||||
|
vom Modus. Im Einfach-Modus zeigt derselbe Screen aber den Text "Firewall-Grundschutz ist im
|
||||||
|
einfachen Modus immer aktiv." (Zeile 20) — der Titel behauptet "optional", der Inhalt sagt "immer
|
||||||
|
an". Für Experte-Modus stimmt der Titel (dort gibt es den Toggle). Kleiner, aber sofort sichtbarer
|
||||||
|
Text-Widerspruch für jeden, der im Einfach-Modus durch den Wizard geht.
|
||||||
|
|
||||||
|
**Fix:** Titel modusabhängig gemacht — `viewModel.mode == .expert ? "Firewall (optional)" : "Firewall"`, neuer Key `"Firewall"` in `L10n.swift` ergänzt.
|
||||||
|
|
||||||
|
### 3. Fehlende Englisch-Übersetzungen (sichtbar im EN-UI)
|
||||||
|
**Status:** fixed (skriptgeprüft: beide Keys jetzt in `L10n.swift`, verbleibende zwei fehlende
|
||||||
|
Keys "OK"/"Revision" bewusst nicht ergänzt — identisch in beiden Sprachen, kein sichtbarer Effekt)
|
||||||
|
**Confidence:** hoch (skriptgeprüft: alle `L10n.t(...)`-Aufrufstellen gegen `L10n.swift`s
|
||||||
|
Übersetzungs-Dictionary abgeglichen — `L10n.t` fällt bei fehlendem Key auf den deutschen
|
||||||
|
Originaltext zurück, siehe `L10n.swift:12-15`)
|
||||||
|
|
||||||
|
Zwei Stellen zeigen im Sprachmodus Englisch weiterhin deutschen Text:
|
||||||
|
- `Features/Devices/DevicesView.swift:335` — `"Netzwerk-Test fehlgeschlagen"` (Fehlermeldungstitel)
|
||||||
|
- `Features/Overview/OverviewView.swift:350` — `"Fokus-Ansicht schließen"` (Tooltip auf dem
|
||||||
|
Schließen-Button der Fokus-Ansicht)
|
||||||
|
|
||||||
|
(Ein dritter fehlender Key, `"Revision"`, ist praktisch nicht sichtbar, da das Wort in beiden
|
||||||
|
Sprachen identisch ist — nicht extra gelistet.)
|
||||||
|
|
||||||
|
**Fix-Ansatz:** Beide Strings in `L10n.swift`s `translations`-Dictionary ergänzen (`"Netzwerk-Test
|
||||||
|
fehlgeschlagen": "Network test failed"`, `"Fokus-Ansicht schließen": "Close focus view"`).
|
||||||
|
|
||||||
|
### 4. Health-Check-Heartbeat ignoriert laufende Wizard-/Experte-Schreibvorgänge
|
||||||
|
**Status:** fixed (Build grün, noch nicht live gegenreproduziert)
|
||||||
|
|
||||||
|
Fix: `ConnectionService.beginWrite()`/`endWrite()` (Zähler `activeWriteCount`) ergänzt,
|
||||||
|
`checkConnectionHealthAndReconnectIfNeeded()` prüft jetzt zusätzlich `activeWriteCount == 0`.
|
||||||
|
`SetupViewModel.apply()` und `ExpertViewModel.saveEditingItem()`/`confirmRemoval()` klammern ihren
|
||||||
|
Task-Body jetzt mit `beginWrite()`/`defer { endWrite() }`.
|
||||||
|
**Confidence:** mittel (Logiklücke im Code nachvollzogen; nicht live reproduziert — REST läuft über
|
||||||
|
unabhängige HTTP-Requests und dürfte robust sein, SSH-Exec-Kanäle sind laut SSH-Protokoll
|
||||||
|
grundsätzlich nebenläufig nutzbar, siehe `SSHTransport.swift`/`RestTransport.swift` — insofern kein
|
||||||
|
Daten-Korruptionsrisiko, aber eine echte Lücke in der Ablaufsteuerung)
|
||||||
|
|
||||||
|
`ConnectionService.checkConnectionHealthAndReconnectIfNeeded()` (`ConnectionService.swift:273`)
|
||||||
|
läuft alle 10 Sekunden (`healthCheckInterval`) und prüft nur `!isReconnecting` — nicht, ob gerade
|
||||||
|
ein `SetupViewModel.apply()` oder `ExpertViewModel.saveEditingItem()` in Arbeit ist. Der hAP-lite-
|
||||||
|
Testrouter ist sehr schwach (MIPS 24Kc, 1 Kern, 650MHz, im Test bereits 54% CPU-Last im Leerlauf
|
||||||
|
gemessen). Ein mehrere Sekunden dauernder Apply (z.B. Bonding, mehrere Firewall-Regeln) kann
|
||||||
|
zeitlich mit dem Heartbeat kollidieren; schlägt der Heartbeat unter Last mit Timeout fehl, während
|
||||||
|
der eigentliche Apply eigentlich noch normal durchläuft, zeigt die App fälschlich den
|
||||||
|
"Wiederverbinden…"-Banner an, obwohl gar keine echte Verbindungsunterbrechung vorliegt — und
|
||||||
|
`reconnectLoop` könnte im ungünstigsten Fall sogar `finishConnecting` mit einer neuen
|
||||||
|
Transport-Instanz auslösen, während der ursprüngliche Apply noch auf der alten weiterläuft.
|
||||||
|
|
||||||
|
**Fix-Ansatz:** Einfachster Schutz: `SetupViewModel.isApplying` / `ExpertViewModel.isApplying` (oder
|
||||||
|
ein neuer gemeinsamer "isWriting"-Zähler auf `ConnectionService`) zusätzlich zur Guard-Bedingung in
|
||||||
|
`checkConnectionHealthAndReconnectIfNeeded()` prüfen, damit der Heartbeat während eines aktiven
|
||||||
|
Schreibvorgangs aussetzt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Noch nicht geprüft / außerhalb dieses Durchgangs
|
||||||
|
|
||||||
|
- UI-Interaktion selbst (Klickpfade, Darstellung) — nicht automatisierbar, siehe Hinweis oben.
|
||||||
|
- PPPoE-Client — laut Nutzer am Testrouter nicht testbar (hängt hinter einem weiteren
|
||||||
|
konfigurierten Router, kein direkter ISP-Uplink). Kein neuer Fund, nur zur Vollständigkeit.
|
||||||
|
- Restliche Tabs/Bereiche (Übersicht-Diagramm-Interaktion, Sicherungen, Mode-Taste, Settings) wurden
|
||||||
|
im Code überflogen, ohne konkreten neuen Fund über die bereits in `found.md`/`HANDOFF.md`
|
||||||
|
dokumentierten Punkte hinaus.
|
||||||
@@ -228,3 +228,25 @@ Build grün, alle 99 Unit-Tests grün. Live bestätigt ("funktion").
|
|||||||
Nachbesserung 1 (User-Wunsch: "ein countdown, während wiederverbindens noch mit einbauen und wieviel versuche bereits gelaufen sind"): zwei neue `@Published`-Werte in `ConnectionService` — `reconnectAttemptCount` (hochgezählt pro REST+SSH-Runde) und `secondsUntilNextReconnectAttempt` (sekündlich runtergezählt zwischen den Versuchen, `nil` während ein Versuch tatsächlich läuft). Banner im Verbinden-Tab zeigt jetzt eine zweite, kleinere Zeile: "Versuch 3 · nächster in 4s" (bzw. "Versuch 3 …" während der Verbindungsversuch selbst läuft).
|
Nachbesserung 1 (User-Wunsch: "ein countdown, während wiederverbindens noch mit einbauen und wieviel versuche bereits gelaufen sind"): zwei neue `@Published`-Werte in `ConnectionService` — `reconnectAttemptCount` (hochgezählt pro REST+SSH-Runde) und `secondsUntilNextReconnectAttempt` (sekündlich runtergezählt zwischen den Versuchen, `nil` während ein Versuch tatsächlich läuft). Banner im Verbinden-Tab zeigt jetzt eine zweite, kleinere Zeile: "Versuch 3 · nächster in 4s" (bzw. "Versuch 3 …" während der Verbindungsversuch selbst läuft).
|
||||||
|
|
||||||
Build grün, alle 99 Unit-Tests grün. Bitte nochmal live testen.
|
Build grün, alle 99 Unit-Tests grün. Bitte nochmal live testen.
|
||||||
|
|
||||||
|
### 11. Systematischer Tester-Durchgang (bugs.md, 2026-09-17)
|
||||||
|
**Status:** fixed (Build + Tests grün, noch nicht live gegenreproduziert)
|
||||||
|
|
||||||
|
Auf Nutzerwunsch ("teste alle Funktionalitäten, suche bugs") Code-Audit gegen den echten
|
||||||
|
hAP-lite-Testrouter durchgeführt (UI selbst nicht klickbar, keine macOS-UI-Automatisierung
|
||||||
|
verfügbar — stattdessen Code-Review + Router-Ground-Truth per SSH). Vier Funde in `bugs.md`
|
||||||
|
dokumentiert und direkt gefixt:
|
||||||
|
|
||||||
|
1. Race Condition bei der Port-Konflikt-Prüfung im LAN-Schritt (`SetupViewModel.checkPortConflict`)
|
||||||
|
— schneller Portwechsel konnte ein überholtes Prüfergebnis über ein aktuelleres schreiben.
|
||||||
|
Fix: Generation-Zähler pro LAN-Config, überholte Antworten werden verworfen.
|
||||||
|
2. `FirewallStepView`-Titel behauptete "Firewall (optional)" auch im Einfach-Modus, wo der
|
||||||
|
Grundschutz laut Text direkt darunter fest aktiv ist. Fix: Titel modusabhängig.
|
||||||
|
3. Zwei fehlende Englisch-Übersetzungen (skriptgeprüft gegen alle `L10n.t(...)`-Aufrufstellen):
|
||||||
|
"Netzwerk-Test fehlgeschlagen", "Fokus-Ansicht schließen". Ergänzt in `L10n.swift`.
|
||||||
|
4. Health-Check-Herzschlag (`ConnectionService`, alle 10s) prüfte laufende Setup-Wizard-/
|
||||||
|
Experte-Schreibvorgänge nicht mit — auf schwacher Hardware (hAP lite: 1 Kern, 650MHz) konnte
|
||||||
|
ein langsamer, aber erfolgreicher Apply fälschlich als Verbindungsverlust gewertet werden.
|
||||||
|
Fix: neuer `beginWrite()`/`endWrite()`-Zähler, Herzschlag pausiert währenddessen.
|
||||||
|
|
||||||
|
Build grün, alle 99 Unit-Tests grün. Details je Fund in `bugs.md`.
|
||||||
|
|||||||
Reference in New Issue
Block a user