"use client"; import React, { useCallback, useEffect, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { apiFetch, API_BASE } from "@/lib/api"; import { useDismissingState } from "@/hooks/useDismissingState"; import { DatabaseBackup, Download } from "lucide-react"; interface BackupEntry { filename: string; size: number; createdAt: string; } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } export function BackupsTab({ active }: { active: boolean }) { const { token } = useAuth(); const [backups, setBackups] = useState([]); const [loading, setLoading] = useState(true); const [running, setRunning] = useState(false); const [message, setMessage] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null); const [retainCount, setRetainCount] = useState("14"); const [savingRetain, setSavingRetain] = useState(false); const load = useCallback(async () => { if (!token) return; setLoading(true); try { const [list, allSettings] = await Promise.all([ apiFetch("/api/backups", { authToken: token }), apiFetch>("/api/settings/all", { authToken: token }), ]); setBackups(list || []); setRetainCount(allSettings?.backup_retain_count || "14"); } catch (e: any) { setMessage({ type: "err", text: e?.message || "Failed to load backups" }); } finally { setLoading(false); } }, [token]); const saveRetainCount = async () => { if (!token) return; setSavingRetain(true); try { await apiFetch("/api/settings", { method: "PUT", authToken: token, body: { backup_retain_count: retainCount } }); setMessage({ type: "ok", text: "Retention setting saved." }); } catch (e: any) { setMessage({ type: "err", text: e?.message || "Failed to save retention setting" }); } finally { setSavingRetain(false); } }; useEffect(() => { if (active) load(); }, [active, load]); const runBackup = async () => { if (!token) return; setRunning(true); setMessage(null); try { const res = await apiFetch<{ filename: string }>("/api/backups/run", { method: "POST", authToken: token }); setMessage({ type: "ok", text: `Backup created: ${res.filename}` }); await load(); } catch (e: any) { setMessage({ type: "err", text: e?.message || "Backup failed" }); } finally { setRunning(false); } }; return (

Database backups

Nightly automatic backups, stored locally on this server. Not uploaded anywhere else.

{message && (
{message.text}
)}
setRetainCount(e.target.value)} />
backups, delete the rest
{backups.map((b) => ( ))} {backups.length === 0 && !loading && ( )} {loading && ( )}
Created Size Download
{new Date(b.createdAt).toLocaleString()} {formatSize(b.size)} { // authenticated download: fetch as blob rather than a bare link, // since this route requires an admin bearer token e.preventDefault(); if (!token) return; fetch(`${API_BASE}/api/backups/${encodeURIComponent(b.filename)}/download`, { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => res.blob()) .then((blob) => { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = b.filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }) .catch(() => setMessage({ type: "err", text: "Download failed" })); }} > Download
No backups yet.
Loading…
); }