Fix 3 nicht funktionierende Alert-Dismiss-Buttons (M34)

README-Milestone-Nachcheck (Port-Konflikt-Prüfung/"Fertig"-Button)
deckte drei eigenständige Bugs derselben Klasse auf: Cancel/OK-Buttons
bei ReviewApplyViews Apply-Fehler-Alert, ConnectViews
Zertifikat-Alert und ConnectViews SSH-Hostkey-Alert taten nichts oder
zu wenig - der jeweilige Verbindungs-/Fehlerzustand blieb hängen,
der Dialog konnte nicht sauber verlassen werden.

Neue ConnectionService.cancelPendingTrustConfirmation() und
SetupViewModel.dismissApplyError(), alle drei Alerts korrekt
verdrahtet (Button-Action + Bindings-Setter fuer Tap-Outside/Esc).
Totes dismissPendingSSHTrust() entfernt. 1 neuer Regressionstest,
alle 102 Unit-Tests gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kay
2026-09-17 21:04:01 +02:00
co-authored by Claude Sonnet 5
parent 510e750b3c
commit 4810b7df63
9 changed files with 139 additions and 11 deletions
@@ -123,6 +123,27 @@ final class ConnectionService: ObservableObject {
await connect(with: credentials)
}
/// Backs out of a blocked connect attempt (`.needsCertificateConfirmation`/
/// `.needsSSHHostKeyConfirmation`) without trusting anything back to `.idle` so the
/// Verbinden-tab form is usable again for a fresh attempt (e.g. different credentials, wrong
/// host typo). Without this, `ConnectView`'s "Abbrechen" buttons on both trust alerts had no
/// way to actually change `state`, so the alert's `isPresented` binding (derived from
/// `state`/`pendingSSHTrustFingerprint`) stayed true and the dialog could never really be
/// dismissed found during the README milestone re-check, 2026-09-17 (same class of bug as
/// `SetupViewModel.dismissApplyError()`, fixed moments earlier this session). Also clears
/// `pendingSSHTrustFingerprint`, covering the background-trust-probe trigger path too, so one
/// method correctly backs out of either of the two ways `ConnectView`'s SSH-host-key alert
/// can be showing (see `pendingSSHTrustFingerprint`'s own doc comment for that distinction).
func cancelPendingTrustConfirmation() {
switch state {
case .needsCertificateConfirmation, .needsSSHHostKeyConfirmation:
state = .idle
default:
break
}
pendingSSHTrustFingerprint = nil
}
func trustCurrentSSHHostKeyAndRetry(fingerprint: String) async {
guard let credentials else { return }
sshHostKeyTrust.trust(host: credentials.host, fingerprint: fingerprint)
@@ -138,10 +159,6 @@ final class ConnectionService: ObservableObject {
pendingSSHTrustFingerprint = nil
}
func dismissPendingSSHTrust() {
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
@@ -134,7 +134,9 @@ struct ConnectView: View {
Button(L10n.t("Vertrauen und verbinden", appLanguage)) {
viewModel.trustAndRetry(fingerprint: fingerprint)
}
Button(L10n.t("Abbrechen", appLanguage), role: .cancel) {}
Button(L10n.t("Abbrechen", appLanguage), role: .cancel) {
viewModel.cancelCertificatePrompt()
}
} message: { fingerprint in
Text(L10n.t("Der Router hat sich mit einem unbekannten Zertifikat gemeldet.", appLanguage)
+ "\n" + L10n.t("Fingerabdruck:", appLanguage) + " \(fingerprint)\n\n"
@@ -189,7 +191,7 @@ struct ConnectView: View {
private var certificateAlertBinding: Binding<Bool> {
Binding(
get: { certificateFingerprint != nil },
set: { _ in }
set: { isPresented in if !isPresented { viewModel.cancelCertificatePrompt() } }
)
}
@@ -203,7 +205,7 @@ struct ConnectView: View {
private var sshHostKeyAlertBinding: Binding<Bool> {
Binding(
get: { sshHostKeyFingerprint != nil },
set: { _ in }
set: { isPresented in if !isPresented { viewModel.dismissSSHHostKeyPrompt() } }
)
}
@@ -145,8 +145,21 @@ final class ConnectViewModel: ObservableObject {
}
}
/// Backs out of whichever of the two trust-confirmation states is currently showing the
/// SSH-host-key alert (a blocked initial connect, or the background trust probe after a
/// successful REST connect see `ConnectionService.pendingSSHTrustFingerprint`'s doc
/// comment). Previously only cleared `pendingSSHTrustFingerprint`, which left the alert stuck
/// showing whenever it was triggered by the *other* path (`state ==
/// .needsSSHHostKeyConfirmation`) found during the README milestone re-check, 2026-09-17.
func dismissSSHHostKeyPrompt() {
connectionService.dismissPendingSSHTrust()
connectionService.cancelPendingTrustConfirmation()
}
/// Same "Abbrechen" fix as `dismissSSHHostKeyPrompt()`, for the unknown-TLS-certificate alert
/// its "Abbrechen" button previously had an empty action and could never actually clear
/// `connectionService.state` back out of `.needsCertificateConfirmation`.
func cancelCertificatePrompt() {
connectionService.cancelPendingTrustConfirmation()
}
func checkForUpdates(for credentials: RouterOSCredentials) {
@@ -79,10 +79,10 @@ struct ReviewApplyView: View {
L10n.t("Anwenden fehlgeschlagen", appLanguage),
isPresented: Binding(
get: { viewModel.applyError != nil },
set: { _ in }
set: { isPresented in if !isPresented { viewModel.dismissApplyError() } }
)
) {
Button(L10n.t("OK", appLanguage)) {}
Button(L10n.t("OK", appLanguage)) { viewModel.dismissApplyError() }
} message: {
Text(viewModel.applyError ?? "")
}
@@ -264,6 +264,17 @@ final class SetupViewModel: ObservableObject {
setFirewallSectionEnabled(true)
}
/// Dismisses the "Anwenden fehlgeschlagen" alert `ReviewApplyView`'s `.alert(isPresented:)`
/// binding derives its presented state from `applyError != nil`, so without this the alert's
/// own OK button (whose action previously did nothing) could never actually clear the error:
/// SwiftUI's next re-render would immediately re-evaluate the getter as still true and the
/// alert would reappear or fail to dismiss. Found during the README milestone re-check,
/// 2026-09-17 (bugs.md-style fix see `SetupViewModel`'s port-conflict entry in bugs.md for
/// the same-feature-area race-condition fix earlier this session).
func dismissApplyError() {
applyError = nil
}
/// Ends the wizard after a successful apply without this, the completed review screen just
/// sits there with a disabled "Jetzt anwenden" and no way forward except "Zurück" (which would
/// re-walk now-stale steps against the router state this apply just changed). Resets to a