Skip to content

feat(030): restore from backup during first-run setup (#340) - #380

Merged
SteveTeece merged 17 commits into
devfrom
030-backup-restore
Sep 8, 2026
Merged

feat(030): restore from backup during first-run setup (#340)#380
SteveTeece merged 17 commits into
devfrom
030-backup-restore

Conversation

@SteveTeece

Copy link
Copy Markdown
Owner

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)

  • New /first-run-restore pre-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 routing now goes language-select → /first-run-restore/setup (the wizard is never the direct first-run target). App.razor.cs and FirstRunLanguageScreen updated.
  • After a confirmed restore the app routes to /restart-required, rendered under a new chrome-free BlankLayout (same CultureProvider/ThemeProvider chrome as ShellLayout but no sidebar, nav links or theme toggle) so a user on pre-restore in-memory state has no navigation surface (FR-007).
  • A pre-restore recovery copy is written on every restore, first-run included (on first run it copies the freshly-seeded default DB).

2. Verified backup + native Save dialog (US2)

  • New BackupService.CreateBackupAsync — serialises the snapshot, hands it to IBackupDestinationPicker.SaveAsync (CommunityToolkit.Maui FileSaver, via MauiBackupDestinationPicker) with a default filename from BackupFileNameBuilder, then reads the file back from disk and asserts per-record-type counts three ways (re-read collections == re-read EntityCounts == snapshot counts, archived rows included). Pass → AuditAction.Export entry + 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 word backup always present (Verbatim Constraint), invalid filename chars stripped, InvariantCulture date, fallback base name StageFright when the org name is blank.
  • BackupRestoreTab gains the Create-with-verification flow, a .backup-unencrypted-notice alert (FR-020), a .backup-verify-result panel, and now navigates to /restart-required after 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 with CultureInfo and TimeZoneInfo swapped for the restore; asserts field-for-field equality including counts, values, DateTime.Kind/instants and archived flags. Lives in Integration.Tests (not UI.Tests) per the culture-mutation-race note.

4. Backup captures the complete current state (US4)

  • Format bumped to schema 1.2.0. Adds three previously-missing record types — JournalEntry, BankReconciliation, ReconciliationLine (new *BackupDtos, append-only [ProtoMember] slots) — and 12 previously-missing Settings fields (currency code, language code, financial-year start, tax applicability/rate, per-fee TaxCode, TaxEntryMode, …), plus Transaction.TaxCode/JournalEntryId.
  • Older files still restore: the three new collections are not added to ValidateCompleteness, nulls normalise to [], new settings take entity defaults.
  • BackupSchema.IsRestorable replaces 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)

  • Every new/reworded key ships fully translated in all seven cultures (de-DE, en-US, es-ES, fr-FR, it-IT, ja-JP, pl-PL) alongside the en-AU neutral baseline: SetupResource (first-run restore + restart screen), SettingsResource (unencrypted notice, verify result), ValidationResource (reworded unsupported-schema-version message).
  • New Us2LocalizationGuardTests.Should_TranslateEveryNeutralKey_When_ShippedCultureResxScanned — build-enforced: no shipped satellite .resx (bar qps-ploc) may omit a neutral key. Also strips the stray VS .resx-template Name1/Color1/Bitmap1/Icon1 examples from the 12 neutral files.

Docs

docs/ARCHITECTURE.md, docs/SETUP.md and CLAUDE.md updated for the new first-run route, BlankLayout, CommunityToolkit.Maui FileSaver, the post-write read-back verification, and the first-run restore checkbox as a second sanctioned RadzenSwitch exception. Full spec-kit artifact set under specs/030-backup-restore/.

Tasks

All 42 tasks (T001–T042) complete; specs/030-backup-restore/.spec-context.jsonstatus: completed. SC-001 walk-through recorded in FirstRunRestoreJourneyTests (language → tick restore → pick file → confirm → restart → dashboard, timed under 2 minutes).

Build & tests

  • dotnet build -t:Rebuild (full rebuild) — 0 warnings, 0 errors
  • dotnet test --no-build2159 passed, 0 failed, 0 skipped
    • Core 812 · UI 653 · Integration 270 · Reports 236 · Data 159 · Localization 29

🤖 Generated with Claude Code

SteveTeece and others added 17 commits September 7, 2026 21:41
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>
@SteveTeece
SteveTeece merged commit e0e921a into dev Sep 8, 2026
1 check passed
@SteveTeece
SteveTeece deleted the 030-backup-restore branch September 8, 2026 10:33
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