import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from './ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "./ui/alert-dialog" import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { apiFetch } from '@/lib/api'; import { toast } from 'sonner'; import { Trash2, AlertTriangle, MonitorX, PackageMinus, Network, HardDrive } from 'lucide-react'; import { formatBytes } from '@/lib/utils'; interface MaintenanceModalProps { isOpen: boolean; onClose: () => void; } interface ContainerInfo { Id: string; Names: string[]; State: string; Status: string; Image: string; } interface DockerUsage { reclaimableImages: number; reclaimableContainers: number; reclaimableVolumes: number; } export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalProps) { const [activeTab, setActiveTab] = useState<'ghosts' | 'system'>('ghosts'); // Docker Usage State const [dockerUsage, setDockerUsage] = useState(null); const [isLoadingUsage, setIsLoadingUsage] = useState(false); // Ghost Hunter state const [orphans, setOrphans] = useState>({}); const [isLoadingOrphans, setIsLoadingOrphans] = useState(false); const [selectedOrphans, setSelectedOrphans] = useState([]); const [isPurging, setIsPurging] = useState(false); // System Cleanup state const [isPruning, setIsPruning] = useState(false); const [pruneResult, setPruneResult] = useState<{ message: string; stdout: string; stderr: string } | null>(null); // Confirm Modals state const [purgeConfirmOpen, setPurgeConfirmOpen] = useState(false); const [pruneConfirmOpen, setPruneConfirmOpen] = useState(false); const [pruneTarget, setPruneTarget] = useState<'containers' | 'images' | 'networks' | 'volumes' | null>(null); useEffect(() => { if (isOpen && activeTab === 'ghosts') { fetchOrphans(); } else if (isOpen && activeTab === 'system') { fetchDockerUsage(); setPruneResult(null); // Reset when tab changes } else { setPruneResult(null); } }, [isOpen, activeTab]); const fetchDockerUsage = async () => { setIsLoadingUsage(true); try { const res = await apiFetch('/system/docker-df'); const data = await res.json(); setDockerUsage(data); } catch (error) { console.error('Failed to fetch docker usage:', error); } finally { setIsLoadingUsage(false); } }; const fetchOrphans = async () => { setIsLoadingOrphans(true); try { const res = await apiFetch('/system/orphans'); const data = await res.json(); setOrphans(data); setSelectedOrphans([]); } catch (error) { console.error('Failed to fetch orphans:', error); } finally { setIsLoadingOrphans(false); } }; const toggleOrphanSelection = (containerId: string) => { setSelectedOrphans(prev => prev.includes(containerId) ? prev.filter(id => id !== containerId) : [...prev, containerId] ); }; const selectAllOrphans = () => { const allIds = Object.values(orphans).flat().map(c => c.Id); if (selectedOrphans.length === allIds.length) { setSelectedOrphans([]); } else { setSelectedOrphans(allIds); } }; const requestPurgeOrphans = () => { if (selectedOrphans.length === 0) return; setPurgeConfirmOpen(true); }; const confirmPurgeOrphans = async () => { setPurgeConfirmOpen(false); setIsPurging(true); try { const res = await apiFetch('/system/prune/orphans', { method: 'POST', body: JSON.stringify({ containerIds: selectedOrphans }) }); if (!res.ok) throw new Error('Purge failed'); await fetchOrphans(); // Refresh the list toast.success(`Purged ${selectedOrphans.length} ghost container(s)`); } catch (error) { console.error('Failed to purge orphans:', error); toast.error('Failed to purge selected containers.'); } finally { setIsPurging(false); } }; const requestPruneSystem = (target: 'containers' | 'images' | 'networks' | 'volumes') => { setPruneTarget(target); setPruneConfirmOpen(true); }; const confirmPruneSystem = async () => { if (!pruneTarget) return; setPruneConfirmOpen(false); setIsPruning(true); setPruneResult(null); try { const res = await apiFetch('/system/prune/system', { method: 'POST', body: JSON.stringify({ target: pruneTarget }) }); const data = await res.json(); setPruneResult(data); if (data.reclaimedBytes !== undefined) { toast.success(`Prune complete! Reclaimed ${formatBytes(data.reclaimedBytes)}.`); } else { toast.success(`Successfully pruned ${pruneTarget}`); } await fetchDockerUsage(); } catch (error) { console.error(`Failed to prune ${pruneTarget}: `, error); toast.error(`Failed to prune ${pruneTarget}.`); } finally { setIsPruning(false); setPruneTarget(null); } }; const totalOrphansCount = Object.values(orphans).flat().length; return ( !open && onClose()}> System Janitor Clean up orphaned containers and perform generic system maintenance. setActiveTab(val as 'ghosts' | 'system')} className="flex-1 flex flex-col min-h-0 mt-4" > Ghost Hunter System Cleanup

Detected Orphan Stacks {totalOrphansCount}

{isLoadingOrphans ? (
Hunting for ghosts...
) : totalOrphansCount === 0 ? (

No orphaned containers detected.

Your system is clean!

) : (
0} className="rounded border-gray-300 focus:ring-primary" /> Select All
{Object.entries(orphans).map(([project, containers]) => (
Project: {project}
{containers.map(container => (
toggleOrphanSelection(container.Id)} className="rounded border-gray-300" />
{container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12)} {container.State}
Image: {container.Image}
))}
))}
)}

Reclaimable Space Summary

{isLoadingUsage && !dockerUsage ? (
Calculating reclaimable space...
) : dockerUsage ? (
Stopped Containers
{formatBytes(dockerUsage.reclaimableContainers)}
Unused & Dangling Images
{formatBytes(dockerUsage.reclaimableImages)}
Unused Volumes
{formatBytes(dockerUsage.reclaimableVolumes)}
) : (
Reclaimable space data unavailable.
)}

Global Docker Pruning

{pruneResult && (
Result:
{pruneResult.message}
{pruneResult.stdout &&
{pruneResult.stdout}
} {pruneResult.stderr &&
{pruneResult.stderr}
}
)}
{/* Purge Ghost Containers Confirmation */} Forcefully Remove Containers Are you sure you want to forcefully remove {selectedOrphans.length} ghost container(s)? setPurgeConfirmOpen(false)}>Cancel Remove {/* Prune System Confirmation */} Prune {pruneTarget} Are you sure you want to prune all unused {pruneTarget}? This cannot be undone. setPruneConfirmOpen(false)}>Cancel Prune
); }