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