/** * @file RetentionSettings.tsx * @description Settings → Data → Session cleanup, under the manual purge: the * OPT-IN automatic cleanup (`AGENTHUB_RETENTION_DAYS`, 0 = off, the default) * that the hourly maintenance applies, the database's size and free space, and * **Compact now** — a full VACUUM behind a confirm, polled through * `GET /api/settings/retention` while it runs, then toasted. Backed by * `server/lib/db-retention.js`; a value the environment set is read-only. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle, Minimize2, RefreshCw } from "lucide-react"; import { api } from "../../lib/api"; import type { RetentionState } from "../../lib/api"; import { confirmDialog } from "../../lib/confirm"; import { formatBytes, formatDateTime } from "../../lib/format"; import { toast } from "../../lib/toast"; export function RetentionSettings() { const { t } = useTranslation("settings"); const [state, setState] = useState(null); const [days, setDays] = useState("0"); const [saving, setSaving] = useState(false); useEffect(() => { api.settings.retention .get() .then((s) => { setState(s); setDays(String(s.days)); }) .catch(() => undefined); }, []); // A Compact takes a while on a large database: poll until it ends. const compacting = !!state?.compacting; useEffect(() => { if (!compacting) return; const id = window.setInterval(() => { api.settings.retention .get() .then((s) => { setState(s); // the draft keeps its value if (s.compacting || !s.last_compact) return; if (s.last_compact.ok) { toast.success( t("retention.compactDone", { before: formatBytes(s.last_compact.size_before ?? 0), after: formatBytes(s.last_compact.size_after ?? 0), }) ); } else { toast.error(t("retention.compactFailed", { error: s.last_compact.error })); } }) .catch(() => undefined); }, 2000); return () => window.clearInterval(id); }, [compacting, t]); const max = state?.max_days ?? 3650; const n = Number(days); const valid = days.trim() !== "" && Number.isInteger(n) && n >= 0 && n <= max; const changed = state !== null && valid && n !== state.days; const owned = !!state?.env_owned.days; async function save() { setSaving(true); try { const s = await api.settings.retention.save(n); setState(s); setDays(String(s.days)); toast.success(t("retention.saved")); } catch (err) { toast.error(err); } finally { setSaving(false); } } async function compact() { const ok = await confirmDialog({ title: t("retention.compactConfirmTitle"), message: t("retention.compactConfirmBody"), confirmLabel: t("retention.compact"), }); if (!ok) return; try { setState(await api.settings.retention.compact()); } catch (err) { toast.error(err); } } const purged = state?.last_run?.retention?.purged_sessions; return (

{t("retention.title")}

{t("retention.description")}

setDays(e.target.value)} aria-invalid={!valid || undefined} aria-describedby={!valid ? "retention-days-error" : undefined} className="input w-20 text-sm text-right font-mono" /> {t("common:days")}
{!valid && (

{t("retention.invalid", { max })}

)}

{state && state.days > 0 ? t("retention.on", { days: state.days }) : t("retention.off")} {state?.last_run && purged !== undefined && ( <> {t("retention.lastRun", { time: formatDateTime(state.last_run.at), n: purged })} )}

{owned && (

{t("privacy.envOwned")}

)}
{state?.space && ( {t("retention.space", { size: formatBytes(state.db_size), free: formatBytes(state.space.free_bytes), mode: state.space.auto_vacuum, })} )}
); }