'use client'; import { useEffect, useState } from 'react'; import { useMutation, useQuery } from 'convex/react'; import { Copy, RefreshCw, Trash2, Wrench } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@spoon/backend/convex/_generated/api.js'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input, } from '@spoon/ui'; type WorkerHealth = { ok: boolean; workerId: string; convexUrl: string; runtime: string; containerRuntime: string; containerAccess: string; jobImage: string; workdir: string; network?: string; httpPort: number; activeWorkspaceCount: number; workspaceContainers: string[]; }; type CleanupResult = { removedContainers: string[]; removedWorkdirs: string[]; }; export const WorkerHealthPanel = () => { const [health, setHealth] = useState(null); const [healthError, setHealthError] = useState(); const [loadingHealth, setLoadingHealth] = useState(false); const [cleaning, setCleaning] = useState(false); const [deleting, setDeleting] = useState(false); const [olderThanDays, setOlderThanDays] = useState(7); const deletableCount = useQuery(api.agentJobs.countOldWorkspaces, { olderThanDays }) ?? 0; const deleteOldWorkspaces = useMutation(api.agentJobs.deleteOldWorkspaces); const copy = async (value: string) => { await navigator.clipboard.writeText(value); toast.success('Copied.'); }; const DiagnosticValue = ({ value }: { value: string }) => (
{value}
); const refreshHealth = async () => { setLoadingHealth(true); setHealthError(undefined); try { const response = await fetch('/api/agent-worker/health'); if (!response.ok) throw new Error(await response.text()); setHealth((await response.json()) as WorkerHealth); } catch (error) { const message = error instanceof Error ? error.message : String(error); setHealthError(message); setHealth(null); } finally { setLoadingHealth(false); } }; useEffect(() => { void refreshHealth(); }, []); const cleanupOrphans = async () => { setCleaning(true); try { const response = await fetch('/api/agent-worker/cleanup', { method: 'POST', }); if (!response.ok) throw new Error(await response.text()); const result = (await response.json()) as CleanupResult; toast.success( `Cleaned ${result.removedContainers.length} containers and ${result.removedWorkdirs.length} workdirs.`, ); await refreshHealth(); } catch (error) { console.error(error); toast.error('Could not clean worker resources.'); } finally { setCleaning(false); } }; const deleteOld = async () => { if ( !window.confirm( `Delete up to 100 stopped, cancelled, failed, or expired workspaces older than ${olderThanDays} days?`, ) ) { return; } setDeleting(true); try { const result = await deleteOldWorkspaces({ olderThanDays, limit: 100, }); toast.success(`Deleted ${result.deleted} workspaces.`); } catch (error) { console.error(error); toast.error('Could not delete old workspaces.'); } finally { setDeleting(false); } }; return (
Worker health

Runtime status for the server-side agent worker.

{healthError ? (
{healthError}
) : null} {health ? ( <>
{health.ok ? 'healthy' : 'unhealthy'} {health.workerId} {health.containerRuntime} / {health.containerAccess}
Convex
Job image
Workdir
Network
{health.network ?? 'none'}
HTTP port
{health.httpPort}
Active workspaces
{health.activeWorkspaceCount}

Workspace containers

{health.workspaceContainers.length ? health.workspaceContainers.join(', ') : 'none'}

) : !healthError ? (

{loadingHealth ? 'Checking worker...' : 'No worker response yet.'}

) : null}
Cleanup

Remove stopped workspace records and orphaned local worker resources.

{deletableCount} stopped, cancelled, failed, timed out, or expired workspaces match this age filter.

Orphaned worker resources

Remove inactive Spoon job containers and inactive directories under the configured worker workdir.

); };