"use client"; import { useEffect, useState, useCallback } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Archive, Download, Trash2, RefreshCw, Plus, MoreHorizontal, Globe, Puzzle, Settings, Package, } from "lucide-react"; import { toast } from "sonner"; import { formatDistanceToNow, format } from "date-fns"; interface Backup { id: string; name: string; type: "worlds" | "plugins" | "config" | "full"; size: number; path: string; createdAt: number; status: "pending" | "running" | "completed" | "failed"; triggeredBy: string; } const TYPE_CONFIG = { worlds: { label: "Worlds", icon: Globe, color: "text-blue-400", bg: "bg-blue-500/10" }, plugins: { label: "Plugins", icon: Puzzle, color: "text-amber-400", bg: "bg-amber-500/10" }, config: { label: "Config", icon: Settings, color: "text-violet-400", bg: "bg-violet-500/10" }, full: { label: "Full", icon: Package, color: "text-emerald-400", bg: "bg-emerald-500/10" }, }; function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } export default function BackupsPage() { const [backups, setBackups] = useState([]); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); const [deleteId, setDeleteId] = useState(null); const fetchBackups = useCallback(async () => { try { const res = await fetch("/api/backups"); if (res.ok) setBackups((await res.json()).backups); } finally { setLoading(false); } }, []); useEffect(() => { fetchBackups(); // Poll every 5s to catch running->completed transitions const interval = setInterval(fetchBackups, 5000); return () => clearInterval(interval); }, [fetchBackups]); const createBackup = async (type: Backup["type"]) => { setCreating(true); try { const res = await fetch("/api/backups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type }), }); if (!res.ok) throw new Error((await res.json()).error); toast.success(`${TYPE_CONFIG[type].label} backup started`); fetchBackups(); } catch (err) { toast.error(err instanceof Error ? err.message : "Backup failed"); } finally { setCreating(false); } }; const deleteBackup = async (id: string) => { try { const res = await fetch(`/api/backups/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error((await res.json()).error); toast.success("Backup deleted"); setBackups((prev) => prev.filter((b) => b.id !== id)); } catch (err) { toast.error(err instanceof Error ? err.message : "Delete failed"); } setDeleteId(null); }; const downloadBackup = (id: string) => { window.open(`/api/backups/${id}`, "_blank"); }; const statusBadge = (status: Backup["status"]) => { const config = { pending: "bg-zinc-500/20 text-zinc-400 border-zinc-500/30", running: "bg-blue-500/20 text-blue-400 border-blue-500/30", completed: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30", failed: "bg-red-500/20 text-red-400 border-red-500/30", }; return ( {status === "running" && ( )} {status.charAt(0).toUpperCase() + status.slice(1)} ); }; return (

Backups

Create and manage server backups

New Backup {(Object.entries(TYPE_CONFIG) as [Backup["type"], typeof TYPE_CONFIG.full][]).map( ([type, config]) => { const Icon = config.icon; return ( createBackup(type)} className="text-zinc-300 focus:text-white focus:bg-zinc-800" > {config.label} backup ); }, )}
{/* Type summary cards */}
{(Object.entries(TYPE_CONFIG) as [Backup["type"], typeof TYPE_CONFIG.full][]).map( ([type, config]) => { const Icon = config.icon; const count = backups.filter( (b) => b.type === type && b.status === "completed", ).length; return ( createBackup(type)} >

{config.label}

{count} backup(s)

); }, )}
{/* Backups list */} {loading ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : backups.length === 0 ? (

No backups yet

Create your first backup above

) : (
Name Size Status Created
{backups.map((backup) => { const typeConfig = TYPE_CONFIG[backup.type]; const Icon = typeConfig.icon; return (

{backup.name}

{backup.type}

{backup.size > 0 ? formatBytes(backup.size) : "—"} {statusBadge(backup.status)} {formatDistanceToNow(new Date(backup.createdAt), { addSuffix: true, })} {backup.status === "completed" && ( downloadBackup(backup.id)} className="text-zinc-300 focus:text-white focus:bg-zinc-800" > Download )} setDeleteId(backup.id)} className="text-red-400 focus:text-red-300 focus:bg-zinc-800" > Delete
); })}
)}
{/* Delete confirmation */} setDeleteId(null)}> Delete backup? This will permanently delete the backup file from disk. This action cannot be undone. Cancel deleteId && deleteBackup(deleteId)} className="bg-red-600 hover:bg-red-500 text-white" > Delete
); }