Beta-tester feedback triage. The data model already had LotType.seedling as first class; the gaps were copy and UX. - Seedlings/plantones: broaden landing hero (EN/ES), in-app legal notice, and all legal texts (site + docs masters) from seeds-only to "seeds and seedlings", with a live-plant phytosanitary/transport caveat. Seeds stay the hero — targeted, not a blanket rename. - Backups: settings line now states the 7-day cadence explicitly; cadence lives in AutoBackupService.backupInterval as the single source for text and schedule. - Community servers: replace the manual wss:// text box with a checklist of the known servers (defaults visible/toggleable) plus an "add server" affordance with basic validation. No jargon in the UI. i18n across en/es/pt/fr/de/ast (ja falls back). Tests updated + a new server-picker widget test.
97 lines
3.6 KiB
Dart
97 lines
3.6 KiB
Dart
import 'dart:io';
|
|
|
|
import 'auto_backup_store.dart';
|
|
import 'export_import_service.dart';
|
|
|
|
/// Makes a silent, encrypted safety copy of the inventory on a schedule — a net
|
|
/// against in-app data loss or DB corruption, with zero friction. Triggered
|
|
/// from the app lifecycle (see [AutoBackupGate]), it writes into the app's
|
|
/// private storage; the manual "save a copy" stays the way to move one off the
|
|
/// device.
|
|
///
|
|
/// Copies are **sealed** (same [ExportImportService.buildSealedBackup] bytes as
|
|
/// a manual backup): they reveal nothing at rest and open with the recovery
|
|
/// code. The newest few are kept and older ones rotated out.
|
|
class AutoBackupService {
|
|
AutoBackupService({
|
|
required ExportImportService exporter,
|
|
required AutoBackupStore store,
|
|
required Future<Directory> Function() directory,
|
|
DateTime Function() now = DateTime.now,
|
|
}) : _exporter = exporter,
|
|
_store = store,
|
|
_directory = directory,
|
|
_now = now;
|
|
|
|
final ExportImportService _exporter;
|
|
final AutoBackupStore _store;
|
|
final Future<Directory> Function() _directory;
|
|
final DateTime Function() _now;
|
|
|
|
/// Filename prefix so rotation only ever touches automatic copies, never a
|
|
/// backup the user saved by hand.
|
|
static const _prefix = 'tane-auto-';
|
|
|
|
/// How many automatic copies to keep. A rotating window (not a single file)
|
|
/// so a corrupt copy can't overwrite the only backup.
|
|
static const _keep = 3;
|
|
|
|
/// How often an automatic copy is made. The single source of truth for both
|
|
/// the schedule ([runIfDue]'s default) and the reassurance copy in Settings,
|
|
/// so the two can never drift apart.
|
|
static const Duration backupInterval = Duration(days: 7);
|
|
|
|
/// When the last automatic copy was made, or null if none has run yet. For a
|
|
/// reassurance line in Settings.
|
|
Future<DateTime?> lastBackupAt() => _store.lastBackupAt();
|
|
|
|
/// Makes a fresh copy when at least [interval] has passed since the last one.
|
|
/// Returns true when it wrote a copy, false when not due (or on any failure —
|
|
/// a backup must never crash startup or the move to the background).
|
|
Future<bool> runIfDue({Duration interval = backupInterval}) async {
|
|
try {
|
|
final last = await _store.lastBackupAt();
|
|
if (last != null && _now().difference(last) < interval) return false;
|
|
} on Object {
|
|
return false;
|
|
}
|
|
return backupNow();
|
|
}
|
|
|
|
/// Makes a copy right now, ignoring the schedule — for an explicit "back up
|
|
/// now" tap. Returns true on success, false on any failure (never throws).
|
|
Future<bool> backupNow() async {
|
|
try {
|
|
final now = _now();
|
|
final dir = await _directory();
|
|
await dir.create(recursive: true);
|
|
final sealed = await _exporter.buildSealedBackup();
|
|
final date = now.toIso8601String().substring(0, 10);
|
|
final file = File('${dir.path}/$_prefix$date.tane');
|
|
await file.writeAsBytes(sealed, flush: true);
|
|
|
|
await _rotate(dir);
|
|
await _store.markBackupAt(now);
|
|
return true;
|
|
} on Object {
|
|
// Silent net: a failed copy is logged nowhere sensitive and never thrown.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Keeps the newest [_keep] automatic copies; deletes the rest. Filenames end
|
|
/// in an ISO date, so a lexical sort is chronological.
|
|
Future<void> _rotate(Directory dir) async {
|
|
final copies =
|
|
(await dir.list().toList())
|
|
.whereType<File>()
|
|
.where((f) => f.uri.pathSegments.last.startsWith(_prefix))
|
|
.toList()
|
|
..sort((a, b) => a.path.compareTo(b.path));
|
|
final excess = copies.length - _keep;
|
|
if (excess <= 0) return;
|
|
for (final stale in copies.take(excess)) {
|
|
await stale.delete();
|
|
}
|
|
}
|
|
}
|