Skip to content

feat: automatic backups + harden startup to fail-open - #8

Merged
MerdanDev merged 2 commits into
mainfrom
feature/auto-backup-and-bootstrap-hardening
Jul 31, 2026
Merged

feat: automatic backups + harden startup to fail-open#8
MerdanDev merged 2 commits into
mainfrom
feature/auto-backup-and-bootstrap-hardening

Conversation

@MerdanDev

Copy link
Copy Markdown
Owner

Two related reliability changes.

1. Bootstrap hardening

bootstrap() ran a serial chain of awaited plugin init before runApp with no timeout or try/catch, so any hang or throw stranded the app on the splash screen — the class of bug behind the 2.1.0 getToken() hang.

Now only the first-frame essentials run before runApp: SharedPreferences and a guarded, time-boxed Firebase.initializeApp. Analytics network calls, notifications, FCM, and the home widget are deferred to after runApp, each isolated by a _guard (try/catch + 10s timeout). The analytics observer is nullable and fail-safe, so a missing Firebase app degrades analytics instead of crashing App.build.

2. Automatic backup (opt-in)

BackupService writes a complete snapshot on app open and close to two places:

  • Private copy (getApplicationSupportDirectory()/backups/) — feeds "restore latest automatic backup". Wiped on clear-data/uninstall.
  • Durable copy that survives uninstall — Android → MediaStore Download/Gapjyk/ via a Kotlin MethodChannel in MainActivity.kt (no new dependency, no runtime permission on Android 10+); iOS → Documents dir exposed in the Files app via Info.plist keys.

Details:

  • Filenames are date-only (gapjyk-backup-YYYY-MM-DD.xlsx), so same-day writes overwrite; each location prunes to the newest 14.
  • Reuses the existing encodeBackup; no-ops on an empty store so a wiped session can't overwrite a good same-day backup.
  • Introduced in a new onboarding step and a Settings toggle (+ restore-latest). Manual and auto restore now share _applyBackup.
  • Triggers are open/close only (no debounce): every data change already persists synchronously, so the next open covers a crashed session.

Design decisions (agreed up front)

  • Dual storage (private + durable); durable Android via self-written MediaStore channel rather than a third-party SAF plugin.
  • iOS durable = Files-exposed Documents (App Store listing isn't live yet).
  • On Android 10+ there is no system permission to request — enabling the toggle is the consent; API ≤28 leans on the manifest's legacy storage permission and degrades to private-only if denied.

Verification

  • flutter analyze clean · dart format clean · cspell 0 issues
  • 47 tests pass, incl. a new isolate-safe backup_service_test.dart (round-trip, empty-store skip, disabled skip, prune-to-14)
  • Shared-isolate CI-equivalent run green across seeds 1 / 999 / 12345 / random ×2
  • Minified production release APK builds with the new Kotlin channel

Not yet done (reviewer note)

  • On-device Android MediaStore smoke test could not be automated here (no Android device tool in this environment): enable → background → confirm Download/Gapjyk/…xlsx appears and survives clear-data. Worth a manual check before release.
  • No version bump in this PR — a release would be a minor bump (2.1.0 → 2.2.0).

Full design notes in .claude/docs/backup.md.

🤖 Generated with Claude Code

MerdanDev and others added 2 commits July 31, 2026 15:09
Two related reliability changes.

Bootstrap hardening: bootstrap() ran a serial chain of awaited plugin
init before runApp with no timeout or try/catch, so any hang or throw
stranded the app on the splash screen — the class of bug behind the
2.1.0 getToken() hang. Now only the first-frame essentials run before
runApp (SharedPreferences and a guarded, time-boxed Firebase core init);
analytics, notifications, FCM and the home widget are deferred after
runApp, each isolated by a _guard (try/catch + 10s timeout). The
analytics observer is nullable and fail-safe so a missing Firebase app
degrades analytics instead of crashing App.build.

Automatic backup (opt-in): BackupService writes a complete snapshot on
app open and close to two places — a private copy (support dir, feeds
"restore latest") and a durable copy that survives uninstall: Android
MediaStore Download/Gapjyk/ via a Kotlin MethodChannel in
MainActivity.kt (no new dependency, no runtime permission on Android
10+), iOS Documents exposed in the Files app via Info.plist. Files are
dated (gapjyk-backup-YYYY-MM-DD.xlsx) so same-day writes overwrite and
each location prunes to the newest 14. It reuses the existing
encodeBackup and no-ops on an empty store so a wiped session can't
overwrite a good same-day backup. Introduced in a new onboarding step
and a Settings toggle (+ restore-latest); manual and auto restore now
share _applyBackup.

Triggers are open/close only (no debounce): every data change already
persists synchronously, so the next open covers a crashed session.

Verified: flutter analyze/format clean, cspell clean, 47 tests pass
including an isolate-safe backup_service_test, shared-isolate run green
across seeds, and a minified production release APK builds.

Docs: CLAUDE.md gains the startup/backup gotchas and a Decisions entry;
full design in .claude/docs/backup.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Minor bump over 2.1.0+14. versionCode 15 for the next production
upload. Ships automatic backups (on open/close, private + durable) and
the fail-open bootstrap hardening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MerdanDev
MerdanDev merged commit 53a7a89 into main Jul 31, 2026
4 checks passed
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