"use client"; import { useState, useEffect } from "react"; import { api } from "~/trpc/react"; import { Button } from "./ui/button"; import { Badge } from "./ui/badge"; import { RefreshCw, ChevronDown, ChevronRight, HardDrive, Database, Server, CheckCircle, AlertCircle, } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { ConfirmationModal } from "./ConfirmationModal"; import { LoadingModal } from "./LoadingModal"; interface Backup { id: number; backup_name: string; backup_path: string; size: bigint | null; created_at: Date | null; storage_name: string; storage_type: string; discovered_at: Date; server_id?: number; server_name: string | null; server_color: string | null; } interface ContainerBackups { container_id: string; hostname: string; backups: Backup[]; } export function BackupsTab() { const [expandedContainers, setExpandedContainers] = useState>( new Set(), ); const [hasAutoDiscovered, setHasAutoDiscovered] = useState(false); const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false); const [selectedBackup, setSelectedBackup] = useState<{ backup: Backup; containerId: string; } | null>(null); const [restoreProgress, setRestoreProgress] = useState([]); const [restoreSuccess, setRestoreSuccess] = useState(false); const [restoreError, setRestoreError] = useState(null); const [shouldPollRestore, setShouldPollRestore] = useState(false); const { data: backupsData, refetch: refetchBackups, isLoading, } = api.backups.getAllBackupsGrouped.useQuery(); const discoverMutation = api.backups.discoverBackups.useMutation({ onSuccess: () => { void refetchBackups(); }, }); // Poll for restore progress const { data: restoreLogsData } = api.backups.getRestoreProgress.useQuery( undefined, { enabled: shouldPollRestore, refetchInterval: 1000, // Poll every second refetchIntervalInBackground: true, }, ); // Update restore progress when log data changes useEffect(() => { if (restoreLogsData?.success && restoreLogsData.logs) { setRestoreProgress(restoreLogsData.logs); // Stop polling when restore is complete if (restoreLogsData.isComplete) { setShouldPollRestore(false); // Check if restore was successful or failed const lastLog = restoreLogsData.logs[restoreLogsData.logs.length - 1] ?? ""; if (lastLog.includes("Restore completed successfully")) { setRestoreSuccess(true); setRestoreError(null); } else if (lastLog.includes("Error:") || lastLog.includes("failed")) { setRestoreError(lastLog); setRestoreSuccess(false); } } } }, [restoreLogsData]); const restoreMutation = api.backups.restoreBackup.useMutation({ onMutate: () => { // Start polling for progress setShouldPollRestore(true); setRestoreProgress(["Starting restore..."]); setRestoreError(null); setRestoreSuccess(false); }, onSuccess: (result) => { // Stop polling - progress will be updated from logs setShouldPollRestore(false); if (result.success) { // Update progress with all messages from backend (fallback if polling didn't work) const progressMessages = restoreProgress.length > 0 ? restoreProgress : (result.progress?.map((p) => p.message) ?? [ "Restore completed successfully", ]); setRestoreProgress(progressMessages); setRestoreSuccess(true); setRestoreError(null); setRestoreConfirmOpen(false); setSelectedBackup(null); // Keep success message visible - user can dismiss manually } else { setRestoreError(result.error ?? "Restore failed"); setRestoreProgress( result.progress?.map((p) => p.message) ?? restoreProgress, ); setRestoreSuccess(false); setRestoreConfirmOpen(false); setSelectedBackup(null); // Keep error message visible - user can dismiss manually } }, onError: (error) => { // Stop polling on error setShouldPollRestore(false); setRestoreError(error.message ?? "Restore failed"); setRestoreConfirmOpen(false); setSelectedBackup(null); setRestoreProgress([]); }, }); // Update progress text in modal based on current progress const currentProgressText = restoreProgress.length > 0 ? restoreProgress[restoreProgress.length - 1] : "Restoring backup..."; // Auto-discover backups when tab is first opened useEffect(() => { if (!hasAutoDiscovered && !isLoading && backupsData) { // Only auto-discover if there are no backups yet if (!backupsData.backups?.length) { void handleDiscoverBackups(); } setHasAutoDiscovered(true); } }, [hasAutoDiscovered, isLoading, backupsData]); const handleDiscoverBackups = () => { discoverMutation.mutate(); }; const handleRestoreClick = (backup: Backup, containerId: string) => { setSelectedBackup({ backup, containerId }); setRestoreConfirmOpen(true); setRestoreError(null); setRestoreSuccess(false); setRestoreProgress([]); }; const handleRestoreConfirm = () => { if (!selectedBackup) return; setRestoreConfirmOpen(false); setRestoreError(null); setRestoreSuccess(false); restoreMutation.mutate({ backupId: selectedBackup.backup.id, containerId: selectedBackup.containerId, serverId: selectedBackup.backup.server_id ?? 0, }); }; const toggleContainer = (containerId: string) => { const newExpanded = new Set(expandedContainers); if (newExpanded.has(containerId)) { newExpanded.delete(containerId); } else { newExpanded.add(containerId); } setExpandedContainers(newExpanded); }; const formatFileSize = (bytes: bigint | null): string => { if (!bytes) return "Unknown size"; const b = Number(bytes); if (b === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(b) / Math.log(k)); return `${(b / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; }; const formatDate = (date: Date | null): string => { if (!date) return "Unknown date"; return new Date(date).toLocaleString(); }; const getStorageTypeIcon = (type: string) => { switch (type) { case "pbs": return ; case "local": return ; default: return ; } }; const getStorageTypeBadgeVariant = ( type: string, ): "default" | "secondary" | "outline" => { switch (type) { case "pbs": return "default"; case "local": return "secondary"; default: return "outline"; } }; const backups = backupsData?.success ? backupsData.backups : []; const isDiscovering = discoverMutation.isPending; return (
{/* Header with refresh button */}

Backups

Discovered backups grouped by container ID

{/* Loading state */} {(isLoading || isDiscovering) && backups.length === 0 && (

{isDiscovering ? "Discovering backups..." : "Loading backups..."}

)} {/* Empty state */} {!isLoading && !isDiscovering && backups.length === 0 && (

No backups found

Click "Discover Backups" to scan for backups on your servers.

)} {/* Backups list */} {!isLoading && backups.length > 0 && (
{backups.map((container: ContainerBackups) => { const isExpanded = expandedContainers.has(container.container_id); const backupCount = container.backups.length; return (
{/* Container header - collapsible */} {/* Container content - backups list */} {isExpanded && (
{container.backups.map((backup) => (
{backup.backup_name} {getStorageTypeIcon(backup.storage_type)} {backup.storage_name}
{backup.size && ( {formatFileSize(backup.size)} )} {backup.created_at && ( {formatDate(backup.created_at)} )} {backup.server_name && ( {backup.server_name} )}
{backup.backup_path}
handleRestoreClick( backup, container.container_id, ) } disabled={restoreMutation.isPending} className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > Restore Delete
))}
)}
); })}
)} {/* Error state */} {backupsData && !backupsData.success && (

Error loading backups: {backupsData.error ?? "Unknown error"}

)} {/* Restore Confirmation Modal */} {selectedBackup && ( { setRestoreConfirmOpen(false); setSelectedBackup(null); }} onConfirm={handleRestoreConfirm} title="Restore Backup" message={`This will destroy the existing container and restore from backup. The container will be stopped during restore. This action cannot be undone and may result in data loss.`} variant="danger" confirmText={selectedBackup.containerId} confirmButtonText="Restore" cancelButtonText="Cancel" /> )} {/* Restore Progress Modal */} {(restoreMutation.isPending || (restoreSuccess && restoreProgress.length > 0)) && ( { setRestoreSuccess(false); setRestoreProgress([]); }} /> )} {/* Restore Success */} {restoreSuccess && (
Restore Completed Successfully

The container has been restored from backup.

)} {/* Restore Error */} {restoreError && (
Restore Failed

{restoreError}

{restoreProgress.length > 0 && (
{restoreProgress.map((message, index) => (

{message}

))}
)}
)}
); }