"use client";
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
import { Button } from "@kandev/ui/button";
import { Badge } from "@kandev/ui/badge";
import { Spinner } from "@kandev/ui/spinner";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kandev/ui/table";
import { IconDownload, IconTrash, IconArchive, IconRotateClockwise } from "@tabler/icons-react";
import { useBackups } from "@/hooks/domains/system/use-backups";
import { buildBackupDownloadUrl, createBackup, deleteBackup } from "@/lib/api/domains/system-api";
import { formatBytes } from "@/lib/utils/format-bytes";
import { JobProgressIndicator } from "./job-progress-indicator";
import { RestoreDialog } from "./restore-dialog";
import type { SnapshotInfo } from "@/lib/types/system";
const BACKUP_CREATE_TIMEOUT_MS = 15_000;
const BACKUP_CREATE_POLL_MS = 250;
function formatTimestamp(iso: string): string {
if (!iso) return "-";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleString();
}
function BackupRow({
row,
onRestore,
onDelete,
}: {
row: SnapshotInfo;
onRestore: (name: string) => void;
onDelete: (name: string) => void;
}) {
return (
{row.name}
{row.kind}
{formatBytes(row.size_bytes)}
{formatTimestamp(row.mtime)}
onRestore(row.name)}
data-testid="system-backups-restore"
>
onDelete(row.name)}
data-testid="system-backups-delete"
>
);
}
function BackupsList({
items,
onRestore,
onDelete,
}: {
items: SnapshotInfo[];
onRestore: (name: string) => void;
onDelete: (name: string) => void;
}) {
return (
Name
Kind
Size
Created
Actions
{items.map((row) => (
))}
);
}
function sleep(ms: number): Promise {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForCreatedBackup(
reload: () => Promise,
previousNames: Set,
): Promise {
const deadline = Date.now() + BACKUP_CREATE_TIMEOUT_MS;
while (Date.now() < deadline) {
const items = await reload();
if (items.some((item) => item.kind === "manual" && !previousNames.has(item.name))) {
return;
}
await sleep(BACKUP_CREATE_POLL_MS);
}
throw new Error("Create snapshot did not finish within 15s");
}
export function BackupsTable() {
const { backups, loaded, isLoading, reload } = useBackups();
const [creating, setCreating] = useState(false);
const [restoreName, setRestoreName] = useState(null);
const [error, setError] = useState(null);
const onCreate = async () => {
setCreating(true);
setError(null);
const previousNames = new Set(backups.map((backup) => backup.name));
try {
await createBackup();
await waitForCreatedBackup(reload, previousNames);
} catch (err) {
setError(err instanceof Error ? err.message : "Create snapshot failed");
} finally {
setCreating(false);
}
};
const onDelete = async (name: string) => {
setError(null);
try {
await deleteBackup(name);
await reload();
} catch (err) {
setError(err instanceof Error ? err.message : "Delete failed");
}
};
const items = backups;
return (
Backups
void onCreate()}
className="cursor-pointer"
data-testid="system-backups-create"
>
{creating ? "Creating..." : "Create snapshot"}
A backup is a full copy of Kandev's database (tasks, sessions, settings). Kandev
automatically creates one before every version upgrade so you can roll back if something
goes wrong. You can also create one manually before risky operations like factory reset.
Only the latest 2 backups are kept on disk.
{error && (
{error}
)}
{!loaded && isLoading && (
Loading backups...
)}
{loaded && items.length === 0 && (
No backups yet. Snapshots created automatically on version upgrade or manually via the
button above will appear here.
)}
{items.length > 0 && (
setRestoreName(n)}
onDelete={(n) => void onDelete(n)}
/>
)}
{
if (!open) setRestoreName(null);
}}
name={restoreName ?? ""}
/>
);
}