feat(030): restore from backup during first-run setup (#340) - #380
Merged
Conversation
New spec for restoring from a backup during first-run setup (GitHub #340). Clarify session 2026-09-07 recorded and integrated: - FR-020: backup file stays unencrypted, no password on backup or restore; file-level encryption is out of scope / future enhancement. - FR-019: reject any backup recorded as newer than the running app — a newer same-major build included — with an "update the app" message. - FR-015: the pre-restore recovery copy is taken for every restore, first-run included (copy of the freshly-seeded default database). - Large-dataset edge case: no wall-clock target; bar is a non-blocking UI with continuous progress; round-trip test uses a representative multi-year mid-size dataset, no timing assertion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plan for restoring from a backup during first-run setup, extending the existing .sfbak pipeline: - new pre-wizard /first-run-restore screen (FR-001 checkbox) + terminal /restart-required screen (FR-007); App.razor.cs routes first-run through /first-run-restore, never /setup directly - backup completeness (FR-011/FR-012): add JournalEntry, BankReconciliation, ReconciliationLine + 12 Settings fields + Fee.TaxCode / Transaction.TaxCode / Transaction.JournalEntryId via append-only protobuf members; schema 1.1.0 -> 1.2.0; older .sfbak files stay restorable - post-write read-back verification (FR-021-024) -> BackupVerificationException - full semver version comparison: reject any newer file incl. a newer same-major build (FR-019) - native Save dialog via CommunityToolkit.Maui FileSaver behind IBackupDestinationPicker; default filename "<org> backup <yyyy-MM-dd>" - stable pre-restore recovery-copy location via IRecoveryCopyStore (FR-015) - cross-platform / cross-locale round-trip test (FR-013/FR-014) Constitution Check: all PASS (CommunityToolkit.Maui noted as a permitted- library addition). Adds plan.md, research.md, data-model.md, contracts/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The backup format had fallen behind the data model, so a restore silently dropped data. Bring it fully current (schema 1.1.0 -> 1.2.0): - add JournalEntryBackupDto, BankReconciliationBackupDto, ReconciliationLineBackupDto + BackupEnvelope collections 24/25/26 and their EntityCounts keys; BackupRepository reads them (IgnoreQueryFilters for the soft-deletable reconciliation pair) and upserts them in FK-safe order (JournalEntries before Transactions; reconciliations after) - add the 12 Settings fields added since specs 016/028/029: FinancialYearStartMonth/Day, CurrencyCode, ClosedThroughDate, InceptionDate, IsTaxApplicable, TaxRate, AnnualFeeTaxCode, AttendanceFeeTaxCode, TaxEntryMode, LanguageCode, ShowParticipationGraphs ([DefaultValue(true)] so a real false round-trips and an older file restores to the entity default) - add Fee.TaxCode and Transaction.TaxCode / Transaction.JournalEntryId to their DTOs (silent financial data loss before this) - older .sfbak files still restore: the 3 new collection keys are NOT added to the completeness gate; absent collections normalise to empty and absent settings fall back to entity defaults New tests: BackupDtoFieldParityTests (reflection guard - every persisted entity scalar must have a backup-DTO field, so this cannot regress); finance round-trips and expanded-settings round-trips in BackupServiceTests, BackupImportTests (fresh-restore incl. an archived draft reconciliation) and V9_BackupRestoreTests (schema version assertion updated to 1.2.0). Build 0 warnings / 0 errors; full suite 2089 passed / 0 failed. Spec docs (data-model, research, contracts) updated to match: new DateTime DTO members use the plain [ProtoMember] default like the existing 20 DTOs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
39 tasks across 7 phases (Setup, Foundational, US1 first-run restore [P1/MVP], US2 verified backup + native Save dialog [P2], US3 cross-platform round trip [P2], US4 format completeness [P3], Polish), laid out as dependency-ordered waves with explicit join points. US4's format-drift tasks (T029-T034) are checked off and attributed to commit 7d2cc93, which already delivered them; US1/US2/US3, the FR-019 full-semver version check, and the doc updates remain. Requirement->task map covers all 24 functional requirements; the same map is recorded in .spec-context.json for the Companion panel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-artifact analysis found 14 issues (0 critical). Applied the agreed fixes: - spec: FR-011 enumeration widened + points at the SC-008 parity guard; FR-018 reworded as a "same code paths" rule; US4 acceptance-scenario 4 rewritten to the real mechanism (Settings-row existence + EF migration) since no schema/version-marker startup check exists; FR-003 now requires the originating app version to reflect the real build, not the "1.0.0" placeholder - tasks: T016 aligned to the single-ShellLayout reality + a RestartRequiredScreen no-escape-control bUnit test (FR-007); T035 spells out the synthetic pre-1.2.0 envelope fixture; T011/T012/T025 fold in the stale IBackupService XML-doc refresh; T024 points failure UI at the caught exception; new T040 (US2 cross-layer journey test, constitution §11.2/§11.4/§11.5) and T041 (SC-001 timed walk-through); Success Criteria -> Task map added; phase-order notation de-collided with story priorities - plan/contracts/research: corrected the "13 -> 16 entity types" figure to the real BackupSnapshot count (20); research D3 and the first-run-flow contract no longer claim the restart screen renders "outside the shell nav" Docs only - no code changed, so no build/test run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ayout Follow-up to 8dd2de0 — replaces the Option-A remediation for finding I1 (reuse ShellLayout + assert no escape controls) with the stronger Option B: - T016 now creates a new BlankLayout (LayoutComponentBase, CultureProvider / ThemeProvider wrappers, no <nav class="shell-sidebar">, no theme toggle); RestartRequiredScreen gets @layout BlankLayout; the bUnit test also asserts BlankLayout emits no sidebar / NavLinks (FR-007) - T038 records the new BlankLayout in the CLAUDE.md Navigation section - first-run-flow contract: /restart-required Layout column -> BlankLayout (new); identifiers table notes the companion no-sidebar assertion - research D3: decision rewritten to BlankLayout; "keep it on ShellLayout" added as a rejected alternative - plan Constitution Check (§4.7.1/§4.7.2) and Project Structure (Layout/ tree, RestartRequiredScreen @layout note, RestartRequiredScreenTests, Scale/Scope) updated; also corrected the leftover "16 entity types" note on IBackupRepository.cs to "all 20 BackupSnapshot members" Docs only - no code changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… every language Adds a non-negotiable localization rule to CLAUDE.md: any key added to (or reworded in) a neutral .resx must land, with a real translation, in every shipped culture set (de-DE, en-US, es-ES, fr-FR, it-IT, ja-JP, pl-PL) in the same change — en-AU is the neutral file itself, qps-ploc is script-generated. The key-by-key fallback is a regression safety net, not licence to ship gaps. - docs/localization/adding-a-language.md: §3.2 "Partial languages are fine" -> "A shipped language set must be complete"; §8 notes completeness is a review gate (not yet a build-breaking test). - spec 030: new FR-025 + SC-010; plan.md Constitution Check gains a Localization-completeness row; Scale/Scope file count corrected for the 21 satellite .resx updates. - spec 030 tasks: T011/T017/T027 now add keys to the neutral file AND all seven culture sets (was "neutral only, others fall back"); T039 walks SC-001..SC-010 and checks for no Missing-localization-key warnings; requirement/SC maps and dependency notes updated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e examples Enforcement for the FR-025 rule added last commit: - Us2LocalizationGuardTests.Should_TranslateEveryNeutralKey_When_ShippedCultureResxScanned — the mirror of the no-orphan-key check: every shipped .<culture>.resx must carry a non-blank value for every neutral key. qps-ploc is excluded (it deliberately omits ~3 keys to exercise the fallback path). Green today: all 7 shipped cultures (de-DE, en-US, es-ES, fr-FR, it-IT, ja-JP, pl-PL) are already complete. - Removed the stray Visual-Studio .resx-template example entries (Name1, Color1, Bitmap1, Icon1) from the schema-doc comment of all 12 neutral .resx files. They are inside the <!-- --> header comment, never parsed as <data>; no code or test references them; the culture satellites already omit them. - Doc sync: CLAUDE.md and adding-a-language.md (§1, §8) now point at the guard as build-enforcement; spec 030 tasks.md gains T042 (delivered) under SC-010. Build: 0 warnings / 0 errors (full solution). Tests: 2090 passed, 0 failed (Localization 29, Core 769, UI 636, Data 159, Integration 261, Reports 236). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements tasks T001-T012 of spec 030 (no user-visible behaviour yet).
- T001: add CommunityToolkit.Maui 15.0.1 (FileSaver) + UseMauiCommunityToolkit()
- T002: BackupSchema.IsRestorable — full three-part semver check (FR-019); a file
newer than 1.2.0 on any component (newer same-major build included) is rejected,
equal/older accepted
- T003/T004: BackupVerificationException, BackupVerificationResult
- T005/T006: IRecoveryCopyStore, IBackupDestinationPicker (+ BackupDestinationResult)
Core seams; the record's positional flag is WasCancelled (not Cancelled) to avoid
a CS0102 clash with the static Cancelled factory — contract doc updated to match
- T007: BackupFileNameBuilder — "<org> backup <yyyy-MM-dd>.sfbak", invariant date,
invalid chars stripped, blank org -> "StageFright" (FR-010)
- T008/T009: MauiRecoveryCopyStore (AppDataDirectory/recovery),
MauiBackupDestinationPicker (FileSaver; user dismiss -> Cancelled, platform error
-> DataAccessException)
- T010: register both seams as singletons in MauiProgram
- T011: BackupService.ValidateVersion now calls BackupSchema.IsRestorable;
SupportedMajorVersion removed; Validation_Backup_UnsupportedSchemaVersion reworded
to the "update the application and retry" message and re-translated across
de-DE/en-US/es-ES/fr-FR/it-IT/ja-JP/pl-PL (placeholder set now {SchemaVersion}
only); qps-ploc regenerated; IBackupService XML docs updated
- T012: every restore (first-run included) now writes a durable pre-restore recovery
copy (StageFright-Recovery-<ts>.sfbak) under IRecoveryCopyStore's directory,
replacing the import-file-adjacent checkpoint
Test helpers for the 3 direct BackupService constructions updated with a temp-dir
IRecoveryCopyStore fake; the "creates checkpoint" assertions and cleanup globs
follow the Checkpoint->Recovery filename rename; one V9 assertion that checked the
old "upgrade" wording now checks "update".
Full rebuild: 0 warnings, 0 errors. Full suite: 2090 passed, 0 failed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TASK-SYNC of the Companion tracking file: currentStep tasks -> implement, status -> implementing (19/42). Records the 12 phase 1-2 tasks completed this pass (T001-T012) alongside the already-delivered T029-T034 / T042. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A clean install can now be populated from a .sfbak file during first-run setup, closing the reachability gap for the treasurer-handover scenario. - FirstRunRestoreScreen (@page "/first-run-restore", ShellLayout): a plain #restore-from-backup checkbox (the Verbatim Constraint exception to the RadzenSwitch rule) reveals a checkbox-gated <InputFile accept=".sfbak">; a valid pick shows .first-run-restore-summary (per-type counts, GeneratedAt, originating ApplicationVersion); #confirm-restore runs ImportAsync on Task.Run behind a blocking .first-run-restore-progress overlay and, on success, routes to /restart-required. #cancel-restore or any invalid/corrupt/newer-version file leaves the DB untouched and keeps the manual #continue-setup -> /setup path available. - BlankLayout: chrome-free layout (CultureProvider/ThemeProvider cascade, no nav/sidebar/theme-toggle) used only by RestartRequiredScreen (@page "/restart-required") — a terminal screen with no control that routes anywhere, so a post-restore user has no navigation surface into pre-restore in-memory state (FR-007). - Routing: App.razor.cs and FirstRunLanguageScreen now reach /setup only via /first-run-restore; /setup is never the direct first-run target (FR-001). - 16 new SetupResource keys (Setup_FirstRunRestore_*, Setup_RestartRequired_*) added to the neutral .resx and fully translated into all seven shipped cultures; qps-ploc regenerated. - Tests: FirstRunRestoreScreenTests, RestartRequiredScreenTests, FirstRunRestoreJourneyTests (integration); AppRoutingTests + FirstRunLanguageScreenTests updated for the new target. tasks.md T013-T020 checked. Full rebuild: 0 warnings. Full suite: 2103 pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s callback Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CreateBackupAsync serialises the snapshot, hands it to the OS-native Save dialog (CommunityToolkit.Maui FileSaver behind IBackupDestinationPicker) pre-filled with the FR-010 default name "<org> backup <yyyy-MM-dd>.sfbak", then reads the written file back from disk and verifies its per-record-type counts against the just-captured live data and the file's own recorded counts (FR-021–FR-024). A pass writes an AuditAction.Export audit entry and returns BackupVerificationResult.Passed; any mismatch or unreadable file throws BackupVerificationException with the discrepancy list, leaving the file on disk. A dismissed dialog is OperationCanceledException (no file, no audit); a platform failure is DataAccessException. ExportAsync now runs the same read-back check so a pre-restore recovery copy is never silently bad. BackupRestoreTab: Create button drives CreateBackupAsync (user-cancel = silent no-op); .backup-unencrypted-notice near Create (FR-020); .backup-verify-result renders "verified" / "failed — do not rely on this file" + discrepancies; a successful Settings-path restore now navigates to /restart-required, matching the first-run path (FR-018). Localization: Settings_Backup_UnencryptedNotice / _Verified / _VerifyFailed added to the neutral resx and fully translated into all seven shipped cultures; qps-ploc regenerated. Tasks: T021 (BackupFileNameBuilderTests), T022 (BackupSchemaTests), T023 (BackupServiceTests CreateBackupAsync coverage), T024 (BackupRestoreTabTests), T025 (BackupService/IBackupService), T026 (BackupRestoreTab), T027 (SettingsResource keys), T040 (SettingsBackupJourneyTests). Full rebuild: 0 warnings. dotnet test: 2155 passed, 0 failed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CrossPlatformRoundTripTests: a representative multi-year dataset (200 members incl. archived, ~700 fees, ~500 payments, 2000 balanced GL transactions, 30 journal entries, a finalised + a soft-deleted draft bank reconciliation with lines, non-default currency/language/FY-start/ tax config + TaxEntryMode) is ExportAsync'd then ImportAsync'd into a fresh in-memory SQLite connection with CultureInfo.CurrentCulture / CurrentUICulture swapped to de-DE for the restore half. Asserts field-for-field equality per record type (per-type counts, values, DateTime Kind, archived flags) via ordered JSON snapshots read straight from the source and restored databases; audit-trail parity plus the one restore-side AuditAction.Import entry (FR-017); and DateTime instant/day preservation (ticks + kind + ToUniversalTime + TimeZoneInfo cross-check into a far-from-UTC zone). No timing assertion. No production code: the .sfbak round trip is already byte-faithful under a swapped locale and time zone (FR-013, FR-014, FR-016; SC-002, SC-003). Full rebuild: 0 warnings. Full suite: 2156 passed (+1). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…k-through — phase 6-7 Finishes spec 030 implementation (T035-T041): - T035 — V9_BackupRestoreTests: a synthetic pre-1.2.0 envelope restores (schema-1.2.0 collections come back empty, new Settings fields take their entity defaults, a restored Settings row routes startup to /dashboard); a current 1.2.0 file with non-default currency/language/FY-start/tax config + TaxEntryMode + journal entries + a finalised reconciliation round-trips field-for-field across a simulated restart (fresh DB). - T041 — FirstRunRestoreJourneyTests: SC-001 acceptance walk-through — traces language -> tick restore -> pick file (summary) -> confirm -> restart -> populated /dashboard with no SetupService.InitializeAsync call, times the interaction, asserts < 2 minutes, logs the elapsed time for the PR notes. - T036 — ARCHITECTURE.md: new Backup & Restore section (BackupSnapshot's real 20 members = 19 collections + Settings singleton; native Save dialog + post-write read-back verification; pre-restore recovery copy; first-run reachability via /first-run-restore); fixed the stale Navigation first-run sentence. - T037 — SETUP.md: first-run description now covers /language-select -> /first-run-restore -> /setup and the restore-then-/restart-required path; new bullet for the recovery copies under FileSystem.AppDataDirectory/recovery. - T038 — CLAUDE.md: Navigation section (first-run reaches /setup via /first-run-restore; /restart-required under the chrome-free BlankLayout); CommunityToolkit.Maui (FileSaver) tech-stack note; first-run restore checkbox recorded as the second sanctioned RadzenSwitch exception. - T039 — full rebuild 0 warnings / 0 errors; dotnet test 2159 passed, 0 failed, 0 skipped. Living spec: folded the first-run routing change (MODIFIED) and the post-restore chrome-free restart screen (ADDED) into capabilities/app-host. Spec marked completed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Spec 030 — Restore From Backup During First-Run Setup. Implements GitHub issue #340: an incoming treasurer can populate a brand-new install straight from the outgoing treasurer's backup file, with no manual organisation/fee/tax setup, and the handover file is proven readable and complete before it changes hands.
Closes #340.
What changed
1. Restore reachable from first-run (US1)
/first-run-restorepre-wizard screen (FirstRunRestoreScreen) — a plain<input type="checkbox">(Verbatim Constraint from [FEATURE] Add restore from backup to setup wizard #340; sanctioned exception to the RadzenSwitch rule), file picker gated on the checkbox, a contents summary (per-record-type counts, creation date, originating app version), confirm/cancel, and Continue to the manual wizard./first-run-restore→/setup(the wizard is never the direct first-run target).App.razor.csandFirstRunLanguageScreenupdated./restart-required, rendered under a new chrome-freeBlankLayout(sameCultureProvider/ThemeProviderchrome asShellLayoutbut no sidebar, nav links or theme toggle) so a user on pre-restore in-memory state has no navigation surface (FR-007).2. Verified backup + native Save dialog (US2)
BackupService.CreateBackupAsync— serialises the snapshot, hands it toIBackupDestinationPicker.SaveAsync(CommunityToolkit.MauiFileSaver, viaMauiBackupDestinationPicker) with a default filename fromBackupFileNameBuilder, then reads the file back from disk and asserts per-record-type counts three ways (re-read collections == re-readEntityCounts== snapshot counts, archived rows included). Pass →AuditAction.Exportentry +BackupVerificationResult { Passed = true }; any failure →BackupVerificationException(file left on disk for diagnosis, backup reported as not to be relied upon).BackupFileNameBuilder—"<sanitised org> backup <yyyy-MM-dd>.sfbak", literal wordbackupalways present (Verbatim Constraint), invalid filename chars stripped,InvariantCulturedate, fallback base nameStageFrightwhen the org name is blank.BackupRestoreTabgains the Create-with-verification flow, a.backup-unencrypted-noticealert (FR-020), a.backup-verify-resultpanel, and now navigates to/restart-requiredafter a successful restore instead of an inline message.3. Cross-platform round-trip guarantee (US3)
CrossPlatformRoundTripTests— representative multi-year dataset exported then imported into a fresh in-memory SQLite connection withCultureInfoandTimeZoneInfoswapped for the restore; asserts field-for-field equality including counts, values,DateTime.Kind/instants and archived flags. Lives inIntegration.Tests(notUI.Tests) per the culture-mutation-race note.4. Backup captures the complete current state (US4)
1.2.0. Adds three previously-missing record types —JournalEntry,BankReconciliation,ReconciliationLine(new*BackupDtos, append-only[ProtoMember]slots) — and 12 previously-missingSettingsfields (currency code, language code, financial-year start, tax applicability/rate, per-feeTaxCode,TaxEntryMode, …), plusTransaction.TaxCode/JournalEntryId.ValidateCompleteness, nulls normalise to[], new settings take entity defaults.BackupSchema.IsRestorablereplaces the major-version-only check — a file whose recorded version is newer than the running build on any semver component (a newer same-major build included) is rejected with an "update the application and retry" message; older/equal accepted.BackupDtoFieldParityTests— reflection guard so a future entity scalar can't silently fall out of the backup.5. Localization (FR-025)
de-DE,en-US,es-ES,fr-FR,it-IT,ja-JP,pl-PL) alongside theen-AUneutral baseline:SetupResource(first-run restore + restart screen),SettingsResource(unencrypted notice, verify result),ValidationResource(reworded unsupported-schema-version message).Us2LocalizationGuardTests.Should_TranslateEveryNeutralKey_When_ShippedCultureResxScanned— build-enforced: no shipped satellite.resx(barqps-ploc) may omit a neutral key. Also strips the stray VS.resx-templateName1/Color1/Bitmap1/Icon1examples from the 12 neutral files.Docs
docs/ARCHITECTURE.md,docs/SETUP.mdandCLAUDE.mdupdated for the new first-run route,BlankLayout,CommunityToolkit.MauiFileSaver, the post-write read-back verification, and the first-run restore checkbox as a second sanctioned RadzenSwitch exception. Full spec-kit artifact set underspecs/030-backup-restore/.Tasks
All 42 tasks (T001–T042) complete;
specs/030-backup-restore/.spec-context.json→status: completed. SC-001 walk-through recorded inFirstRunRestoreJourneyTests(language → tick restore → pick file → confirm → restart → dashboard, timed under 2 minutes).Build & tests
dotnet build -t:Rebuild(full rebuild) — 0 warnings, 0 errorsdotnet test --no-build— 2159 passed, 0 failed, 0 skipped🤖 Generated with Claude Code