Skip to content

Show real progress and a completion notice for manual backups - #582

Merged
bradleymackey merged 4 commits into
mainfrom
backup-progress-bar
Sep 20, 2026
Merged

bradleymackey merged 4 commits into
mainfrom
backup-progress-bar

Conversation

@bradleymackey

Copy link
Copy Markdown
Member

Summary

  • Backup Now row (AutoBackupView): was an AsyncButton that disabled, flashed an indeterminate spinner after 150 ms, then re-enabled with no feedback. It now follows the service status: a phase label ("Exporting vault…" / "Encrypting…" / "Rendering QR codes…" / "Saving…") with a determinate ProgressView, a "Cleaning up old backups…" row, then a green "Backup Complete" row for ~2 s before the button returns. Auto-triggered backups show the same row.
  • Real progress source: PDFDocumentRenderer.render gains a progress handler called after each QR code is drawn (PDFDataBlockDocumentRenderer); the two-pass VaultExportPDFDocumentRenderer maps each pass onto half the range; VaultBackupPDFGenerator.makePDF exposes it with a no-op default so BackupCreatePDFViewModel is untouched. Rendering QR codes (fresh CIContext per code, rendered twice) is ~90 % of a backup, so this is where the bar actually moves.
  • AutoBackupStatus.backingUp(AutoBackupProgress): new AutoBackupProgress { phase, phaseFraction, fractionCompleted } with fixed per-phase weights so the bar advances at a steady pace.
  • Off the main actor: the whole pipeline ran synchronously on @MainActor, so nothing could repaint mid-backup. AutoBackupServiceImpl now exports on main, then encrypts + renders in a nonisolated child task (same pattern as BackupCreatePDFViewModel.makeExportPayload). Progress comes back through an AsyncStream consumed with async let, so every update is applied in order before the status moves on — a late .backingUp can never overwrite .completed.
  • Backups run one at a time: performBackup suspends several times with no in-flight guard, so a debounced auto-backup (or toggling the feature on) during a manual backup ran concurrently: two files, two progress sequences fighting over status. Now queued FIFO; triggerBackupIfNeeded re-checks the payload hash when it is its turn so a change made during an in-flight backup is still picked up. Separate commit for bisectability.
  • Completion notice is view-model owned and keyed off the transition out of a real backup, so the sticky .completed(lastBackupDate) shown on screen open, or the .cleaningUp → .completed that follows a retention change, does not show it.

Manifesto: progress text is phase names only — no item content, no counts by lock/killphrase state, no new logging.

Follow-ups not in scope: QRCodeImageRenderer creates a CIContext per code (reusable) and the document is rendered twice with fresh random shard group IDs in release, so the first pass can't be cached — both are the real fix for backup duration. BackupCreatePDFViewModel still renders on main; the same offload applies.

Test plan

  • make format / make lint
  • PDFDataBlockDocumentRendererUnitTests: progress per image, across multiple data blocks, empty document reports 1, no progress on failure
  • VaultExportPDFDocumentRendererTests: first pass → 0…0.5, second → 0.5…1
  • New AutoBackupProgressTests (range mapping, continuity across phases, clamping)
  • AutoBackupServiceImplTests: ordered progress ending in .completed; overlapping forceBackups run sequentially; triggerBackupIfNeeded during an in-flight backup re-checks the hash (both verified to fail without the queue)
  • AutoBackupViewModelTests: backupProgress, completion notice show/hide/timeout, not shown without a backup or after an error
  • New AutoBackupViewSnapshotTests baselines: backingUp, cleaningUp, backupComplete; existing baselines unchanged
  • Full VaultExportTests, VaultBackupTests, VaultFeedTests, VaultiOSTests green on iPhone 18 Pro Max / iOS 27.0
  • Manual on simulator (-screenshot-scene backups): tapped Backup Now, captured "Rendering QR codes…" with the bar ~60 % mid-backup, then "Backup Complete", then the button back with footer "Last backup: …". No freeze, no spinner flash.
  • CI green

🤖 Generated with Claude Code

bradleymackey and others added 4 commits September 20, 2026 17:32
PDFDocumentRenderer.render now takes a progress handler, called after
each image is drawn with the fraction of images processed. The two-pass
vault renderer maps each pass onto half of the range, and
VaultBackupPDFGenerator.makePDF exposes it with a no-op default so
existing callers are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.backingUp now carries an AutoBackupProgress (phase + fraction). The
service exports on the main actor, then encrypts and renders in a
nonisolated child task so the UI can repaint; progress flows back
through an AsyncStream so every update is applied, in order, before
the status moves on to saving/completed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
performBackup suspends several times and had no in-flight guard, so a
debounced auto-backup (or toggling the feature on) during a manual
backup ran concurrently: two files written and two progress sequences
fighting over the status. Backups now queue behind the previous one.
triggerBackupIfNeeded re-checks the payload hash once it is its turn,
so a change made during an in-flight backup is still picked up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Backup Now row now follows the service status: a phase label and
determinate bar while backing up, a cleanup row, then a brief
"Backup Complete" confirmation before the button returns. The notice
is owned by the view model and keyed off the transition out of a real
backup, so the sticky completed status shown on screen open, or the
cleanup that follows a retention change, does not trigger it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bradleymackey
bradleymackey merged commit 42bf253 into main Sep 20, 2026
2 of 5 checks passed
@bradleymackey
bradleymackey deleted the backup-progress-bar branch September 20, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant