feat(backup): admin UI with admin-chosen target directory
The backup destination is now chosen by the on-site admin in the UI (Setup -> Backup), not a server env var. An env-pinned target defeats the purpose: the admin can't point backups at a freshly-plugged USB or a NAS mount without editing .env and restarting. The encryption key stays a server secret. Target storage: - New site_config.backup_target_dir (migration 0016, nullable; null = not configured). BackupService reads it fresh each run, so a UI change takes effect with no restart. Only BACKUP_KEY stays env -- a key must never live in the DB it backs up. Routes: - PUT /api/backup/config -- set/clear the target (backup:update; upserts id=1). - POST /api/backup/test -- probe a candidate path server-side (exists / is a directory / writable) so the admin gets feedback before relying on it. - status() now exposes targetDir + keyPresent, so the UI distinguishes 'no target set' from 'BACKUP_KEY missing'. UI (apps/web/src/BackupSettings.tsx): - A Setup -> Backup tab (gated backup:read): an editable target-path field with a Test-target probe (localized ok/missing/not-a-dir/not-writable), Save, the status panel (config state, last-run size/pruned/error, a distinct amber missing-key warning), a Back up now button, and the restore-is-out-of-band note. Full i18n (sq + en); nav.backup. - API client: fetchBackupStatus / setBackupTarget / testBackupTarget / runBackup. Also includes a small in-progress copy trim to the setup-intro i18n strings. Verified live with Playwright: typed a path -> Test reported writable -> Save persisted it -> status reflected it and showed the key-missing warning. Whole monorepo build/lint/test green. Wiki backup-recovery + open-question #5 updated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -21,7 +21,7 @@ import { DeviceMonitor } from "./device-monitor.js";
|
||||
import { buildSigner, buildVerifier } from "./signer.js";
|
||||
import { LogService, pinoDbStream } from "./log-service.js";
|
||||
import { pruneSnapshots } from "./snapshot-retention.js";
|
||||
import { BackupService, backupConfigFromEnv } from "./backup-service.js";
|
||||
import { BackupService } from "./backup-service.js";
|
||||
import { backupRoutes } from "./routes/backup.js";
|
||||
import { logRoutes } from "./routes/logs.js";
|
||||
import { VisionClient } from "./vision-client.js";
|
||||
@@ -276,11 +276,12 @@ export async function buildServer(opts: BuildOptions = {}): Promise<FastifyInsta
|
||||
// read the store (GET /api/logs, log:read). See wiki/concepts/app-logs.md.
|
||||
await logRoutes(app, logService);
|
||||
|
||||
// On-site encrypted DB backup (durability for the signed ledger). Admin-driven: status +
|
||||
// a manual "back up now"; the scheduled run is the daily timer below. A no-op until
|
||||
// BACKUP_TARGET_DIR + BACKUP_KEY are set. See wiki/concepts/backup-recovery.md.
|
||||
const backupService = new BackupService(db, backupConfigFromEnv(), app.log);
|
||||
await backupRoutes(app, backupService);
|
||||
// On-site encrypted DB backup (durability for the signed ledger). Admin-driven: the target
|
||||
// directory is admin-chosen (site_config), the key is an env secret; status + a manual "back
|
||||
// up now"; the scheduled run is the daily timer below. A no-op until a target dir is set AND
|
||||
// BACKUP_KEY is present. See wiki/concepts/backup-recovery.md.
|
||||
const backupService = new BackupService(db, app.log);
|
||||
await backupRoutes(app, db, backupService);
|
||||
|
||||
// Periodic retention prune (age + row cap) so the log table stays bounded on the
|
||||
// offline appliance. Runs hourly; unref'd so it never holds the process open.
|
||||
|
||||
Reference in New Issue
Block a user