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:
@@ -227,6 +227,54 @@ export function fetchLogs(params: {
|
||||
return apiFetch(`/api/logs${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
// --- Backup ---------------------------------------------------------------
|
||||
// On-site encrypted DB backup. See wiki/concepts/backup-recovery.md.
|
||||
|
||||
export interface BackupStatus {
|
||||
configured: boolean;
|
||||
/** Admin-chosen target directory (null = not set). */
|
||||
targetDir: string | null;
|
||||
/** Whether the env encryption key is present (a missing key is flagged distinctly). */
|
||||
keyPresent: boolean;
|
||||
running: boolean;
|
||||
lastSuccessAt: string | null;
|
||||
lastResult: { path: string; bytes: number; prunedFiles: number } | null;
|
||||
lastErrorAt: string | null;
|
||||
lastError: string | null;
|
||||
}
|
||||
|
||||
export async function fetchBackupStatus(): Promise<BackupStatus> {
|
||||
return apiFetch("/api/backup/status");
|
||||
}
|
||||
|
||||
/** Set (or clear, with "") the admin-chosen target directory. Returns the new status. */
|
||||
export async function setBackupTarget(targetDir: string | null): Promise<BackupStatus> {
|
||||
return apiFetch("/api/backup/config", { method: "PUT", body: JSON.stringify({ targetDir }) });
|
||||
}
|
||||
|
||||
export interface TargetCheck {
|
||||
ok: boolean;
|
||||
/** "empty" | "missing" | "not_a_dir" | "not_writable" when !ok. */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** Probe a candidate target path server-side (exists / is a dir / is writable). */
|
||||
export async function testBackupTarget(targetDir: string): Promise<TargetCheck> {
|
||||
return apiFetch("/api/backup/test", { method: "POST", body: JSON.stringify({ targetDir }) });
|
||||
}
|
||||
|
||||
export interface BackupRunResult {
|
||||
ok: true;
|
||||
path: string;
|
||||
bytes: number;
|
||||
prunedFiles: number;
|
||||
}
|
||||
|
||||
/** Trigger a manual "back up now". Throws on 409 (not configured) / 500 (run failed). */
|
||||
export async function runBackup(): Promise<BackupRunResult> {
|
||||
return apiFetch("/api/backup/run", { method: "POST" });
|
||||
}
|
||||
|
||||
// --- Device setup ---------------------------------------------------------
|
||||
|
||||
export interface ConfigField {
|
||||
|
||||
Reference in New Issue
Block a user