/** * @file BackupSettings.tsx * @description Settings → Data → Backup: the daily database backup of * `server/lib/db-backup.js` — the folder (best on another disk), the server's * local hour, how many to keep, "Back up now" (polls `GET /api/settings/backup` * while it runs, then toasts the outcome) and the newest backup in the folder. * Saved through `PUT /api/settings/backup`, sending only the fields changed; a * field the environment set is read-only. Restoring is manual and the card says * how. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle, DatabaseBackup } from "lucide-react"; import { api } from "../../lib/api"; import type { BackupState } from "../../lib/api"; import { formatBytes, formatDateTime } from "../../lib/format"; import { toast } from "../../lib/toast"; import { Select } from "../Select"; import { SettingsSection } from "./SettingsRow"; const HOURS = Array.from({ length: 24 }, (_, h) => h); const pad = (n: number) => String(n).padStart(2, "0"); export function BackupSettings() { const { t } = useTranslation("settings"); const [state, setState] = useState(null); const [dir, setDir] = useState(""); const [hour, setHour] = useState(2); const [keep, setKeep] = useState("7"); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const apply = (s: BackupState) => { setState(s); setDir(s.dir); setHour(s.hour); setKeep(String(s.keep)); }; useEffect(() => { api.settings.backup .get() .then(apply) .catch(() => undefined); }, []); // A backup takes tens of seconds on a large database: poll until it ends. const running = !!state?.running; useEffect(() => { if (!running) return; const id = window.setInterval(() => { api.settings.backup .get() .then((s) => { setState(s); // the form keeps its drafts if (s.running || !s.last) return; if (s.last.ok) toast.success(t("backup.done", { file: s.last.file })); else toast.error(t("backup.failed", { error: s.last.error })); }) .catch(() => undefined); }, 2000); return () => window.clearInterval(id); }, [running, t]); const keepN = Number(keep); const keepValid = Number.isInteger(keepN) && keepN >= 1 && keepN <= 365; const patch: { dir?: string; hour?: number; keep?: number } = {}; if (state) { if (dir.trim() !== state.dir) patch.dir = dir.trim(); if (hour !== state.hour) patch.hour = hour; if (keepN !== state.keep) patch.keep = keepN; } const changed = Object.keys(patch).length > 0; async function save() { setSaving(true); setError(null); try { apply(await api.settings.backup.save(patch)); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function runNow() { try { setState(await api.settings.backup.run()); } catch (err) { toast.error(err); } } const owned = state?.env_owned; const newest = state?.files[0]; const total = state?.files.reduce((n, f) => n + f.size, 0) ?? 0; return ( {t("backup.title")} } description={t("backup.description")} >

{t("backup.folderHint")}

setKeep(e.target.value)} />
{error &&

{error}

}

{newest ? t("backup.newest", { time: formatDateTime(newest.mtime), size: formatBytes(newest.size), n: state?.files.length, total: formatBytes(total), }) : state?.dir ? t("backup.none") : t("backup.off")}

{state?.last && !state.last.ok && (

{t("backup.lastFailed", { time: formatDateTime(state.last.at), error: state.last.error, })}

)} {state?.dir_error &&

{state.dir_error}

}

{t("backup.restoreHint")}

{(owned?.dir || owned?.hour || owned?.keep) && (

{t("privacy.envOwned")}

)}
); }