feat(523): Settings -> Data -> Scheduled backups - the verdict first, then destination, schedule, Test, Back up now and recent runs
BackupScheduleSettings.vue leads with whether the data is backed up (last good backup, or Overdue), then what is wrong (last failure in words, an S3 secret this SECRET_KEY cannot open, incomplete config), then the form: daily toggle and UTC hour, folder or S3 bucket, Test destination with unsaved values, Back up now (polls while running, re-armed in finally), and the last runs. The secret is write-only. formatBytes moves to utils/bytes.ts for both views. Restore from Backup now also takes the .tar, and says to use the CLI on a 413. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { apiGet, apiPost, apiPut } from "@/api/client";
|
||||
|
||||
/**
|
||||
* Scheduled backups (milestone 523) — `/api/admin/backups`. One dated,
|
||||
* uncompressed tar per run, delivered to a folder or an S3 bucket.
|
||||
*/
|
||||
export interface BackupConfig {
|
||||
enabled: boolean;
|
||||
/** UTC hour the daily backup runs at, 0–23. */
|
||||
hour: number;
|
||||
destination: "folder" | "s3";
|
||||
folder_path: string;
|
||||
s3_endpoint: string;
|
||||
s3_bucket: string;
|
||||
s3_access_key: string;
|
||||
s3_region: string;
|
||||
s3_verify_tls: boolean;
|
||||
}
|
||||
|
||||
export interface BackupRun {
|
||||
started_at: string;
|
||||
finished_at: string;
|
||||
trigger: "scheduled" | "manual";
|
||||
status: "ok" | "failed";
|
||||
destination?: string;
|
||||
file?: string;
|
||||
size_bytes?: number;
|
||||
where?: string;
|
||||
error?: string;
|
||||
duration_s: number;
|
||||
}
|
||||
|
||||
export interface BackupsOverview {
|
||||
config: BackupConfig;
|
||||
/** Never the secret itself: whether one is kept, and whether this
|
||||
* instance can still open it. */
|
||||
secret: { set: boolean; readable: boolean };
|
||||
/** What stops the saved config from sending a backup, in words. */
|
||||
problems: string[];
|
||||
running: boolean;
|
||||
/** Newest first, at most 30. */
|
||||
runs: BackupRun[];
|
||||
last_run: BackupRun | null;
|
||||
last_success: BackupRun | null;
|
||||
overdue: boolean;
|
||||
}
|
||||
|
||||
/** What the form sends: config fields, plus the secret when one was typed. */
|
||||
export type BackupChanges = Partial<BackupConfig> & {
|
||||
s3_secret?: string;
|
||||
clear_s3_secret?: boolean;
|
||||
};
|
||||
|
||||
export function getBackups(): Promise<BackupsOverview> {
|
||||
return apiGet<BackupsOverview>("/api/admin/backups");
|
||||
}
|
||||
|
||||
export function saveBackups(changes: BackupChanges): Promise<BackupsOverview> {
|
||||
return apiPut<BackupsOverview>("/api/admin/backups", changes);
|
||||
}
|
||||
|
||||
/** Ask the destination whether it would take a backup, with the form's
|
||||
* unsaved values. The server gives the destination 45s; this waits a little
|
||||
* longer so its own answer, not the client's deadline, is what arrives. */
|
||||
export function checkBackupDestination(
|
||||
changes: BackupChanges,
|
||||
): Promise<{ ok: boolean; message: string }> {
|
||||
return apiPost<{ ok: boolean; message: string }>("/api/admin/backups/check", changes, { timeoutMs: 60_000 });
|
||||
}
|
||||
|
||||
export function runBackupNow(): Promise<{ status: string }> {
|
||||
return apiPost<{ status: string }>("/api/admin/backups/run", {});
|
||||
}
|
||||
Reference in New Issue
Block a user