feat(categories): restore backup banner and permanent restore action (#122) #132

Merged
maximus merged 1 commit from issue-122-restore-backup-banner into main 2026-04-21 01:52:09 +00:00
Owner

Summary

Fixes #122.

Surfaces the automatic pre-migration SREF backup to the user so a category migration can be rolled back without digging into the filesystem.

  • Banner (90 days) — dismissable banner at the top of Settings > Categories pointing at the backup. Hidden once reverted / dismissed / past 90d.
  • Permanent entry — "Restore a backup" link in Settings > Categories, available as long as a migration journal exists (even past the 90-day window).
  • Confirm modal — two-step consent with a red Restore button, fallback file picker when the recorded path is missing, PIN prompt for encrypted SREF, full reload on success.

On success: categories_schema_version=v2 + merge reverted_at into last_categories_migration.

Files

  • New src/components/settings/CategoriesMigrationBackupBanner.tsx, CategoriesMigrationRestoreModal.tsx.
  • New src/services/categoryRestoreService.ts wrapping read_import_file + importTransactionsWithCategories with stable error codes.
  • New src/services/categoryRestoreService.test.ts for shouldShowBanner / isWithinBannerWindow.
  • New Tauri command file_exists in src-tauri/src/commands/backup_commands.rs.
  • Modified CategoriesCard.tsx to mount the banner + permanent restore entry.
  • FR/EN i18n keys under settings.categoriesCard.restore*.
  • CHANGELOG entries in both locales.

Test plan

  • npx tsc --noEmit — clean
  • npx vitest run — 193 tests pass (12 new)
  • npm run build — clean
  • cargo check — clean
  • cargo test --lib — 34 passed
  • Manual — migrate a v2 profile, see banner in Settings > Categories
  • Manual — click "Restore the backup" → modal → Restore → verify schema is v2 and data matches pre-migration state
  • Manual — move the backup file, retry → file picker fallback activates
  • Manual — encrypted profile → PIN prompt appears and rejects wrong PIN
  • Manual — dismiss the banner, reload → stays hidden
## Summary Fixes #122. Surfaces the automatic pre-migration SREF backup to the user so a category migration can be rolled back without digging into the filesystem. - **Banner (90 days)** — dismissable banner at the top of Settings > Categories pointing at the backup. Hidden once reverted / dismissed / past 90d. - **Permanent entry** — "Restore a backup" link in Settings > Categories, available as long as a migration journal exists (even past the 90-day window). - **Confirm modal** — two-step consent with a red *Restore* button, fallback file picker when the recorded path is missing, PIN prompt for encrypted SREF, full reload on success. On success: `categories_schema_version=v2` + merge `reverted_at` into `last_categories_migration`. ## Files - New `src/components/settings/CategoriesMigrationBackupBanner.tsx`, `CategoriesMigrationRestoreModal.tsx`. - New `src/services/categoryRestoreService.ts` wrapping `read_import_file` + `importTransactionsWithCategories` with stable error codes. - New `src/services/categoryRestoreService.test.ts` for `shouldShowBanner` / `isWithinBannerWindow`. - New Tauri command `file_exists` in `src-tauri/src/commands/backup_commands.rs`. - Modified `CategoriesCard.tsx` to mount the banner + permanent restore entry. - FR/EN i18n keys under `settings.categoriesCard.restore*`. - CHANGELOG entries in both locales. ## Test plan - [x] `npx tsc --noEmit` — clean - [x] `npx vitest run` — 193 tests pass (12 new) - [x] `npm run build` — clean - [x] `cargo check` — clean - [x] `cargo test --lib` — 34 passed - [ ] Manual — migrate a v2 profile, see banner in Settings > Categories - [ ] Manual — click "Restore the backup" → modal → Restore → verify schema is `v2` and data matches pre-migration state - [ ] Manual — move the backup file, retry → file picker fallback activates - [ ] Manual — encrypted profile → PIN prompt appears and rejects wrong PIN - [ ] Manual — dismiss the banner, reload → stays hidden
maximus added 1 commit 2026-04-21 01:48:03 +00:00
feat(categories): add restore backup banner and permanent restore action (#122)
All checks were successful
PR Check / rust (push) Successful in 21m45s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 21m1s
PR Check / frontend (pull_request) Successful in 2m13s
0132e6e164
Surfaces the pre-migration SREF backup to the user so they can roll back a
category migration without digging into the filesystem:

- 90-day dismissable banner at the top of Settings > Categories pointing to
  the automatic backup (hidden once reverted, once dismissed, or past 90d).
- Permanent "Restore a backup" entry in Settings > Categories, available as
  long as a migration journal exists (even past the 90-day window).
- Confirmation modal with two-step consent, red Restore button, fallback
  file picker when the recorded path is missing, PIN prompt for encrypted
  SREF files, full-page reload on success.

Internals:
- New `categoryRestoreService` wrapping `read_import_file` +
  `importTransactionsWithCategories` with stable error codes
  (file_missing, read_failed, parse_failed, wrong_envelope_type,
  needs_password, wrong_password, import_failed).
- New `file_exists` Tauri command for the pre-flight presence check.
- On success: `categories_schema_version=v2` + merge `reverted_at` into
  `last_categories_migration`.
- Pure `shouldShowBanner` / `isWithinBannerWindow` helpers with tests.
- FR/EN i18n keys under `settings.categoriesCard.restore*`.
- CHANGELOG entries in both locales.

Closes #122
Author
Owner

Self-review

Security (destructive action)

  • Two-step consent — the banner does not restore directly; it opens a modal. The modal's primary Restore button is red (bg-[var(--negative)]) and the warning text is visible on every phase.
  • Interruption lock — backdrop click, Escape key, and X button are all disabled while phase === "restoring". A user cannot accidentally close the modal mid-wipe.
  • Encrypted SREF — when is_file_encrypted returns true, the flow branches to a password prompt. The Restore CTA there is disabled while the field is empty. Wrong PIN surfaces wrong_password (i18n-mapped) and lets the user retry without re-opening the modal.
  • Missing file handlingfile_exists is checked before calling read_import_file, flipping to the missing phase and offering a file picker. No crash, no ambiguous error.
  • Scope limits respected — no new SREF service (reuses importTransactionsWithCategories), no migration SQL touched, no DB schema change.

Pre-existing residual risk (out of scope)

importTransactionsWithCategories performs DELETE … ; INSERT … outside a BEGIN/COMMIT block. A failure mid-import would leave the profile partially wiped. This predates #122 and the risk is identical to the existing Settings → Import data flow. Worth tracking in a follow-up issue but not in this PR.

i18n

  • settings.categoriesCard.restoreBanner.*, settings.categoriesCard.restoreEntry.*, settings.categoriesCard.restoreModal.* — present in both en.json and fr.json. Error codes mapped 1:1 from RestoreError.
  • Intl.NumberFormat / toLocaleString used for dates (matches user locale).

Changelog

  • Entries in both CHANGELOG.md and CHANGELOG.fr.md under ## [Unreleased] / ### Added, detailing banner + permanent entry + new Tauri command + new service.

Tests

  • 12 new tests on shouldShowBanner / isWithinBannerWindow (pure functions). Covers: no migration, recent/expired, dismissed, reverted, boundary at 90d, invalid timestamp, custom window, non-"1" dismissal values.
  • All 193 vitest tests pass, cargo test --lib 34 tests pass.

CI signals

  • npx tsc --noEmit clean.
  • npm run build clean.
  • cargo check clean.

LGTM by myself.

## Self-review ### Security (destructive action) - **Two-step consent** — the banner does not restore directly; it opens a modal. The modal's primary Restore button is red (`bg-[var(--negative)]`) and the warning text is visible on every phase. - **Interruption lock** — backdrop click, Escape key, and X button are all disabled while `phase === "restoring"`. A user cannot accidentally close the modal mid-wipe. - **Encrypted SREF** — when `is_file_encrypted` returns true, the flow branches to a password prompt. The Restore CTA there is disabled while the field is empty. Wrong PIN surfaces `wrong_password` (i18n-mapped) and lets the user retry without re-opening the modal. - **Missing file handling** — `file_exists` is checked before calling `read_import_file`, flipping to the `missing` phase and offering a file picker. No crash, no ambiguous error. - **Scope limits respected** — no new SREF service (reuses `importTransactionsWithCategories`), no migration SQL touched, no DB schema change. ### Pre-existing residual risk (out of scope) `importTransactionsWithCategories` performs `DELETE … ; INSERT …` outside a `BEGIN/COMMIT` block. A failure mid-import would leave the profile partially wiped. This predates #122 and the risk is identical to the existing Settings → *Import data* flow. Worth tracking in a follow-up issue but not in this PR. ### i18n - `settings.categoriesCard.restoreBanner.*`, `settings.categoriesCard.restoreEntry.*`, `settings.categoriesCard.restoreModal.*` — present in both `en.json` and `fr.json`. Error codes mapped 1:1 from `RestoreError`. - `Intl.NumberFormat` / `toLocaleString` used for dates (matches user locale). ### Changelog - Entries in both `CHANGELOG.md` and `CHANGELOG.fr.md` under `## [Unreleased] / ### Added`, detailing banner + permanent entry + new Tauri command + new service. ### Tests - 12 new tests on `shouldShowBanner` / `isWithinBannerWindow` (pure functions). Covers: no migration, recent/expired, dismissed, reverted, boundary at 90d, invalid timestamp, custom window, non-"1" dismissal values. - All 193 vitest tests pass, `cargo test --lib` 34 tests pass. ### CI signals - `npx tsc --noEmit` clean. - `npm run build` clean. - `cargo check` clean. LGTM by myself.
maximus merged commit 4c7f3d09e1 into main 2026-04-21 01:52:09 +00:00
maximus deleted branch issue-122-restore-backup-banner 2026-04-21 01:52:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#132
No description provided.