Finish backup spec: button state, invariant assertion, NAS error surfacing
- BackupView: action button shows "Checking status…" with spinner and blocks taps while coordinator.phase == .checking — prevents starting a backup before reconciliation has determined what actually needs uploading - BackupView: NAS status row shows statusService.refreshError (actual SMB error reason: auth failure, timeout, host unreachable) instead of the generic "Unreachable" string — surfaces the real diagnosis to the user - BackupStatusService: invariant assertion after every full reconcile — logs .error if alreadySafe + needBackup ≠ phoneTotal so filter mismatches and off-by-ones surface immediately in Console.app - BackupStatusService.buildManifestIndex: full diagnostic logging — remotePath, manifest file path, download byte count, decoded entry count, bootstrap fallback reason — all visible in Console.app filtered by "BackupStatus" Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -646,9 +646,11 @@ struct BackupView: View {
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: nasStatusLabel)
|
||||
}
|
||||
if lanMonitor.nasReachable == false {
|
||||
Text("Unreachable")
|
||||
Text(statusService.refreshError ?? "Unreachable")
|
||||
.font(.system(size: 9, weight: .regular))
|
||||
.foregroundStyle(AppTheme.destructive.opacity(0.7))
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.trailing)
|
||||
} else if lanMonitor.nasReachable == true && !engine.job.status.isActive {
|
||||
Text("Connected")
|
||||
.font(.system(size: 9, weight: .regular))
|
||||
@@ -691,43 +693,56 @@ struct BackupView: View {
|
||||
|
||||
private var actionButton: some View {
|
||||
Group {
|
||||
switch resolvedStatus {
|
||||
case .idle, .completed, .failed, .cancelled:
|
||||
Button(action: startBackup) {
|
||||
Text(resolvedStatus == .idle ? "Start backup" : "Back up again")
|
||||
}
|
||||
.buttonStyle(PrimaryButtonStyle(
|
||||
color: ctaFlashGreen ? AppTheme.positive : AppTheme.ink
|
||||
))
|
||||
.accessibilityHint("Starts backing up your photos to the NAS")
|
||||
|
||||
case .running:
|
||||
Button { engine.pause() } label: { Text("Pause") }
|
||||
.buttonStyle(GhostButtonStyle())
|
||||
.accessibilityLabel("Pause backup")
|
||||
|
||||
case .paused:
|
||||
HStack(spacing: 10) {
|
||||
Button { engine.resume() } label: { Text("Resume") }
|
||||
.buttonStyle(PrimaryButtonStyle())
|
||||
.accessibilityLabel("Resume backup")
|
||||
Button { engine.cancel() } label: { Text("Cancel") }
|
||||
.buttonStyle(GhostButtonStyle())
|
||||
.frame(width: 90)
|
||||
.accessibilityLabel("Cancel backup")
|
||||
}
|
||||
|
||||
case .preparing:
|
||||
// Coordinator is running a status check — block the start button so
|
||||
// a backup cannot be triggered before reconciliation completes.
|
||||
if coordinator.phase == .checking {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView().scaleEffect(0.8).tint(AppTheme.inkSecondary)
|
||||
Text("Preparing…")
|
||||
Text("Checking status…")
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
}
|
||||
.frame(height: 50)
|
||||
} else {
|
||||
switch resolvedStatus {
|
||||
case .idle, .completed, .failed, .cancelled:
|
||||
Button(action: startBackup) {
|
||||
Text(resolvedStatus == .idle ? "Start backup" : "Back up again")
|
||||
}
|
||||
.buttonStyle(PrimaryButtonStyle(
|
||||
color: ctaFlashGreen ? AppTheme.positive : AppTheme.ink
|
||||
))
|
||||
.accessibilityHint("Starts backing up your photos to the NAS")
|
||||
|
||||
case .running:
|
||||
Button { engine.pause() } label: { Text("Pause") }
|
||||
.buttonStyle(GhostButtonStyle())
|
||||
.accessibilityLabel("Pause backup")
|
||||
|
||||
case .paused:
|
||||
HStack(spacing: 10) {
|
||||
Button { engine.resume() } label: { Text("Resume") }
|
||||
.buttonStyle(PrimaryButtonStyle())
|
||||
.accessibilityLabel("Resume backup")
|
||||
Button { engine.cancel() } label: { Text("Cancel") }
|
||||
.buttonStyle(GhostButtonStyle())
|
||||
.frame(width: 90)
|
||||
.accessibilityLabel("Cancel backup")
|
||||
}
|
||||
|
||||
case .preparing:
|
||||
HStack(spacing: 8) {
|
||||
ProgressView().scaleEffect(0.8).tint(AppTheme.inkSecondary)
|
||||
Text("Preparing…")
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
}
|
||||
.frame(height: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.82), value: engine.job.status == .running)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.82), value: coordinator.phase == .checking)
|
||||
}
|
||||
|
||||
private func startBackup() {
|
||||
|
||||
Reference in New Issue
Block a user