feat(backup): admin UI with admin-chosen target directory
Build desktop / desktop (push) Successful in 4m42s
Build & push images / images (push) Successful in 2m53s
CI / check (push) Successful in 39s

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:
2026-06-29 12:21:26 +02:00
parent 0c218179c4
commit d5e41500a8
16 changed files with 630 additions and 74 deletions
+14
View File
@@ -1934,3 +1934,17 @@ route — out-of-band by design). New `backup` permission resource in @parking/s
daily timer, no-op until configured, NOT run at startup. `openRawDb()` added to @parking/db/testing.
SMB/NFS work as mount paths; SFTP + admin UI + restore runbook deferred. build/lint/test green (212 server
tests, 25 files). Updated [[open-questions]] #5 (first slice BUILT). NOT yet committed beyond this branch.
## [2026-06-29] feat | Backup admin UI + admin-chosen target (site_config, not env)
The backup TARGET DIRECTORY is now chosen by the on-site admin in the UI, not a server env var — env
target defeats the purpose (admin can't change where backups land without editing .env + restart). Moved
to `site_config.backup_target_dir` (migration 0016, nullable); BackupService reads it fresh each run (no
restart to change). Only BACKUP_KEY stays an env secret — a key must NEVER live in the DB it backs up.
New routes: PUT /api/backup/config (set/clear target, backup:update, upserts the id=1 row), POST
/api/backup/test (server-side path probe: exists/is-dir/writable, backup:update). status() now exposes
targetDir + keyPresent so the UI tells "no target" from "no key". UI: Setup → Backup tab
(apps/web/src/BackupSettings.tsx) — editable target field + Test-target probe (localized reasons) + Save +
status panel (distinct amber "BACKUP_KEY missing" warning) + Back-up-now + restore-out-of-band note; full
i18n sq+en; nav.backup. Verified live with Playwright: typed path → Test "writable" → Save persisted →
status reflects it + key-missing warning shown. build/lint/test green (whole monorepo). Updated
[[backup-recovery]] as-built + [[open-questions]] #5.