feat(backup): add automatic weekly encrypted backups

A silent safety net against in-app data loss or DB corruption, with zero
friction. Driven off the app lifecycle (startup + when hidden), it writes a
sealed .tanemaki copy into the app's private storage once a week, keeping the
newest 3 (rotating). Reuses the manual backup's encryption via the extracted
ExportImportService.buildSealedBackup(); never throws, so a failed copy can't
crash startup or backgrounding. Not registered on web (no file storage).

Settings shows a quiet reassurance line with the date of the last automatic
copy (localised via intl), hidden where no automatic backup exists.
This commit is contained in:
vjrj 2026-07-10 10:17:36 +02:00
parent 0e7eb22c26
commit 39adc423b9
19 changed files with 559 additions and 29 deletions

View file

@ -46,6 +46,9 @@
},
"backup": {
"title": "Backup & restore",
"autoBackupTitle": "Automatic backups",
"autoBackupLast": "Last copy saved {date}",
"autoBackupNone": "A copy is kept automatically in the background",
"exportJson": "Save a backup",
"exportJsonSubtitle": "A complete copy to keep safe, restore later or move to another device",
"importJson": "Restore a backup",

View file

@ -46,6 +46,9 @@
},
"backup": {
"title": "Copia de seguridad",
"autoBackupTitle": "Copias automáticas",
"autoBackupLast": "Última copia guardada el {date}",
"autoBackupNone": "Se guarda una copia automáticamente en segundo plano",
"exportJson": "Guardar una copia de seguridad",
"exportJsonSubtitle": "Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo",
"importJson": "Restaurar una copia",

View file

@ -46,6 +46,9 @@
},
"backup": {
"title": "Cópia de segurança",
"autoBackupTitle": "Cópias automáticas",
"autoBackupLast": "Última cópia guardada em {date}",
"autoBackupNone": "Uma cópia é guardada automaticamente em segundo plano",
"exportJson": "Guardar uma cópia de segurança",
"exportJsonSubtitle": "Uma cópia completa para guardar em segurança, restaurar depois ou levar para outro aparelho",
"importJson": "Restaurar uma cópia",

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 3
/// Strings: 923 (307 per locale)
/// Strings: 932 (310 per locale)
///
/// Built on 2026-07-10 at 01:06 UTC
/// Built on 2026-07-10 at 08:15 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View file

@ -236,6 +236,15 @@ class Translations$backup$en {
/// en: 'Backup & restore'
String get title => 'Backup & restore';
/// en: 'Automatic backups'
String get autoBackupTitle => 'Automatic backups';
/// en: 'Last copy saved {date}'
String autoBackupLast({required Object date}) => 'Last copy saved ${date}';
/// en: 'A copy is kept automatically in the background'
String get autoBackupNone => 'A copy is kept automatically in the background';
/// en: 'Save a backup'
String get exportJson => 'Save a backup';
@ -1592,6 +1601,9 @@ extension on Translations {
'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.',
'settings.aboutOpen' => 'About Tanemaki',
'backup.title' => 'Backup & restore',
'backup.autoBackupTitle' => 'Automatic backups',
'backup.autoBackupLast' => ({required Object date}) => 'Last copy saved ${date}',
'backup.autoBackupNone' => 'A copy is kept automatically in the background',
'backup.exportJson' => 'Save a backup',
'backup.exportJsonSubtitle' => 'A complete copy to keep safe, restore later or move to another device',
'backup.importJson' => 'Restore a backup',

View file

@ -166,6 +166,9 @@ class _Translations$backup$es extends Translations$backup$en {
// Translations
@override String get title => 'Copia de seguridad';
@override String get autoBackupTitle => 'Copias automáticas';
@override String autoBackupLast({required Object date}) => 'Última copia guardada el ${date}';
@override String get autoBackupNone => 'Se guarda una copia automáticamente en segundo plano';
@override String get exportJson => 'Guardar una copia de seguridad';
@override String get exportJsonSubtitle => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo';
@override String get importJson => 'Restaurar una copia';
@ -994,6 +997,9 @@ extension on TranslationsEs {
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
'settings.aboutOpen' => 'Acerca de Tanemaki',
'backup.title' => 'Copia de seguridad',
'backup.autoBackupTitle' => 'Copias automáticas',
'backup.autoBackupLast' => ({required Object date}) => 'Última copia guardada el ${date}',
'backup.autoBackupNone' => 'Se guarda una copia automáticamente en segundo plano',
'backup.exportJson' => 'Guardar una copia de seguridad',
'backup.exportJsonSubtitle' => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo',
'backup.importJson' => 'Restaurar una copia',

View file

@ -166,6 +166,9 @@ class _Translations$backup$pt extends Translations$backup$en {
// Translations
@override String get title => 'Cópia de segurança';
@override String get autoBackupTitle => 'Cópias automáticas';
@override String autoBackupLast({required Object date}) => 'Última cópia guardada em ${date}';
@override String get autoBackupNone => 'Uma cópia é guardada automaticamente em segundo plano';
@override String get exportJson => 'Guardar uma cópia de segurança';
@override String get exportJsonSubtitle => 'Uma cópia completa para guardar em segurança, restaurar depois ou levar para outro aparelho';
@override String get importJson => 'Restaurar uma cópia';
@ -990,6 +993,9 @@ extension on TranslationsPt {
'settings.aboutText' => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.',
'settings.aboutOpen' => 'Acerca do Tanemaki',
'backup.title' => 'Cópia de segurança',
'backup.autoBackupTitle' => 'Cópias automáticas',
'backup.autoBackupLast' => ({required Object date}) => 'Última cópia guardada em ${date}',
'backup.autoBackupNone' => 'Uma cópia é guardada automaticamente em segundo plano',
'backup.exportJson' => 'Guardar uma cópia de segurança',
'backup.exportJsonSubtitle' => 'Uma cópia completa para guardar em segurança, restaurar depois ou levar para outro aparelho',
'backup.importJson' => 'Restaurar uma cópia',