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:
@@ -61,6 +61,7 @@ export const en: Catalog = {
|
||||
reports: "Reports",
|
||||
recycleBin: "Recycle bin",
|
||||
logs: "Logs",
|
||||
backup: "Backup",
|
||||
profile: "Profile",
|
||||
},
|
||||
profile: {
|
||||
@@ -307,7 +308,7 @@ export const en: Catalog = {
|
||||
setup: {
|
||||
title: "Setup",
|
||||
intro:
|
||||
"Add your barrier controllers first — set which relay is entry/exit and which terminal the entry button is wired to. Then add readers, cameras and printers and point each at the barrier it serves.",
|
||||
"Add your barrier controllers first. Then add readers (QR/RF), cameras, printers",
|
||||
catControllers: "Controllers (barriers + entry button)",
|
||||
catReaders: "Readers (QR / RFID)",
|
||||
catCameras: "Cameras (snapshot + plate)",
|
||||
@@ -830,6 +831,41 @@ export const en: Catalog = {
|
||||
path: "Path",
|
||||
empty: "No logs.",
|
||||
},
|
||||
backup: {
|
||||
title: "Backup",
|
||||
intro:
|
||||
"An encrypted copy of the database (the signed ledger) to an external disk. Runs automatically every day and from the button below.",
|
||||
statusTitle: "Status",
|
||||
configured: "Enabled",
|
||||
notConfigured: "Not configured",
|
||||
notConfiguredHint: "Set BACKUP_TARGET_DIR and BACKUP_KEY on the server to enable backups.",
|
||||
running: "Running…",
|
||||
idle: "Idle",
|
||||
lastSuccess: "Last successful backup",
|
||||
lastError: "Last error",
|
||||
never: "Never",
|
||||
lastFile: "File",
|
||||
size: "Size",
|
||||
pruned: "Pruned",
|
||||
runNow: "Back up now",
|
||||
runSuccess: "Backup complete.",
|
||||
runFailed: "Backup failed.",
|
||||
notConfiguredError: "Backup is not configured.",
|
||||
restoreNote:
|
||||
"Restore is not done here — it's an out-of-band step when provisioning a fresh appliance (needs the backup file + the keys you escrowed offsite).",
|
||||
targetLabel: "Backup location",
|
||||
targetPlaceholder: "e.g. /mnt/backup or /media/usb",
|
||||
targetHint: "An absolute path to a mounted disk (USB/SATA) or a network share (SMB/NFS).",
|
||||
save: "Save",
|
||||
saved: "Saved.",
|
||||
test: "Test target",
|
||||
testOk: "The location is writable.",
|
||||
testEmpty: "Enter a path.",
|
||||
testMissing: "The location does not exist.",
|
||||
testNotDir: "The path is not a directory.",
|
||||
testNotWritable: "The directory is not writable.",
|
||||
keyMissing: "The encryption key (BACKUP_KEY) is missing on the server — set it to enable backups.",
|
||||
},
|
||||
pay: {
|
||||
ticket: "Ticket",
|
||||
entry: "Entry",
|
||||
|
||||
Reference in New Issue
Block a user