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>
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
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", {});
|
||
}
|