'use client'; import { useEffect, useState, useCallback } from 'react'; import { SidebarNav } from '@/components/sidebar-nav'; import { vmApi, type VmHealthResult, type VmCheckLevel, type CronStatusResponse, type UnhealthyContainer, type OllamaModelsResponse, } from '@/lib/api'; import { CheckCircle, AlertTriangle, XCircle, RefreshCw, HardDrive, Cpu, Database, Server, Activity, Layers, ScrollText, Trash2, Terminal, ChevronDown, ChevronUp, Clock, Bot, RotateCw, Gauge, Shield, Zap, MemoryStick, } from 'lucide-react'; // ── Types ────────────────────────────────────────────────────────────────── type Level = VmCheckLevel; // ── Shared helpers ───────────────────────────────────────────────────────── function levelColor(level: Level) { switch (level) { case 'OK': return 'text-green-700 bg-green-50 border-green-200'; case 'WARN': return 'text-yellow-700 bg-yellow-50 border-yellow-200'; case 'CRIT': return 'text-red-700 bg-red-50 border-red-200'; } } function levelBadge(level: Level) { switch (level) { case 'OK': return 'bg-green-100 text-green-800'; case 'WARN': return 'bg-yellow-100 text-yellow-800'; case 'CRIT': return 'bg-red-100 text-red-800'; } } function LevelIcon({ level, className = 'w-5 h-5' }: { level: Level; className?: string }) { switch (level) { case 'OK': return ; case 'WARN': return ; case 'CRIT': return ; } } function relativeTime(iso: string | null | undefined): string { if (!iso) return '—'; const diff = Date.now() - new Date(iso).getTime(); if (isNaN(diff)) return '—'; const mins = Math.floor(diff / 60000); if (mins < 2) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); return `${days}d ago`; } function formatDate(iso: string | null | undefined): string { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d.getTime())) return '—'; return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } // ── Score card ───────────────────────────────────────────────────────────── interface ScoreDim { label: string; pts: number; maxPts: number; detail: string } function computeScore( health: VmHealthResult | null, unhealthyCount: number, cronData: CronStatusResponse | null, ): { total: number; dims: ScoreDim[] } { const checks = health?.checks ?? {}; // 1. CPU efficiency — steal % const stealPct = parseFloat((checks.steal?.value ?? '0%').match(/^([\d.]+)/)?.[1] ?? '0'); const cpuPts = stealPct < 2 ? 20 : stealPct < 5 ? 15 : stealPct < 10 ? 10 : 5; // 2. Memory pressure — available GB (first number in value, e.g. "11G / 15G") const ramAvailGb = parseFloat((checks.ram?.value ?? '0G').match(/^([\d.]+)/)?.[1] ?? '0'); const ramPts = ramAvailGb > 6 ? 20 : ramAvailGb > 3 ? 15 : ramAvailGb > 1 ? 5 : 0; // 3. Disk health — used % (first number in value, e.g. "37% used, 123G free") const diskPct = parseInt((checks.disk?.value ?? '100%').match(/^(\d+)/)?.[1] ?? '100', 10); const diskPts = diskPct < 40 ? 15 : diskPct < 55 ? 10 : diskPct < 70 ? 5 : 0; // 4. Service health — unhealthy container count const svcPts = unhealthyCount === 0 ? 20 : unhealthyCount <= 2 ? 15 : unhealthyCount <= 5 ? 8 : 2; // 5. Maintenance hygiene — last successful cleanup let maintPts = 0; let maintDetail = 'no history'; if (cronData?.recentRuns?.length) { const lastRun = cronData.recentRuns.find(r => r.success); if (lastRun) { const daysSince = (Date.now() - new Date(lastRun.timestamp).getTime()) / 86_400_000; maintPts = daysSince < 7 && lastRun.freedMB > 0 ? 15 : daysSince < 30 ? 8 : 0; maintDetail = `${Math.floor(daysSince)}d ago, freed ${lastRun.freedMB > 0 ? lastRun.freedMB + ' MB' : '0 MB'}`; } } // 6. LLM readiness — available RAM const llmPts = ramAvailGb > 8 ? 10 : ramAvailGb > 4 ? 7 : ramAvailGb > 2 ? 4 : 1; const total = cpuPts + ramPts + diskPts + svcPts + maintPts + llmPts; return { total, dims: [ { label: 'CPU Efficiency', pts: cpuPts, maxPts: 20, detail: `${stealPct.toFixed(1)}% steal` }, { label: 'Memory Pressure', pts: ramPts, maxPts: 20, detail: `${ramAvailGb}G available` }, { label: 'Disk Health', pts: diskPts, maxPts: 15, detail: `${diskPct}% used` }, { label: 'Service Health', pts: svcPts, maxPts: 20, detail: `${unhealthyCount} unhealthy` }, { label: 'Maintenance', pts: maintPts, maxPts: 15, detail: maintDetail }, { label: 'LLM Readiness', pts: llmPts, maxPts: 10, detail: `${ramAvailGb}G free` }, ], }; } function ScoreCard({ health, unhealthyCount, cronData, }: { health: VmHealthResult | null; unhealthyCount: number; cronData: CronStatusResponse | null; }) { const { total, dims } = computeScore(health, unhealthyCount, cronData); const scoreColor = total >= 80 ? 'text-green-600' : total >= 60 ? 'text-yellow-600' : 'text-red-600'; const scoreBg = total >= 80 ? 'bg-green-50 border-green-200' : total >= 60 ? 'bg-yellow-50 border-yellow-200' : 'bg-red-50 border-red-200'; return (
{/* Score gauge */}
{total} / 100
{/* Dimension breakdown */}
{dims.map(d => { const pct = Math.round((d.pts / d.maxPts) * 100); const barColor = pct >= 75 ? 'bg-green-400' : pct >= 50 ? 'bg-yellow-400' : 'bg-red-400'; return (
{d.label} {d.pts}/{d.maxPts}

{d.detail}

); })}
); } // ── Unhealthy containers panel ───────────────────────────────────────────── function UnhealthyContainersPanel({ containers, onRestart, restarting, }: { containers: UnhealthyContainer[]; onRestart: (name: string) => Promise; restarting: Set; }) { const [expanded, setExpanded] = useState>(new Set()); const toggle = (name: string) => setExpanded(prev => { const next = new Set(prev); next.has(name) ? next.delete(name) : next.add(name); return next; }); if (containers.length === 0) return null; return (
{containers.length} Unhealthy Container{containers.length !== 1 ? 's' : ''} process alive, health endpoint failing
{containers.map(c => (
{expanded.has(c.name) && c.lastHealthLogs.length > 0 && (

Last health check output:

                  {c.lastHealthLogs.join('\n') || '(no output)'}
                
)}
))}
); } // ── Cron status panel ────────────────────────────────────────────────────── function CronStatusPanel({ data }: { data: CronStatusResponse | null }) { const [expandedRun, setExpandedRun] = useState(null); if (!data) { return (
Maintenance schedule not available
); } const { jobs, recentRuns } = data; return (

Maintenance Schedule

{recentRuns.length > 0 && ( {recentRuns.length} run{recentRuns.length !== 1 ? 's' : ''} in log )}
{/* Jobs table */}
{jobs.map(job => { const lr = job.lastRun; return ( ); })}
Job Schedule Last Run Freed Status Next Run

{job.description}

{job.name}

{job.schedule} {lr ? ( {relativeTime(lr.timestamp)} ) : ( never )} {lr && lr.freedMB > 0 ? +{lr.freedMB} MB : } {lr ? ( {lr.success ? : } {lr.success ? 'OK' : 'Failed'} ) : ( )} {job.nextRun ? ( {relativeTime(job.nextRun).replace('ago', '').trim() || formatDate(job.nextRun)} ) : '—'}
{/* Recent runs — collapsible step log */} {recentRuns.length > 0 && (

Recent Runs

{recentRuns.slice(0, 10).map((run, i) => (
{expandedRun === run.timestamp && run.steps.length > 0 && (
                        {run.steps.join('\n')}
                      
)}
))}
)}
); } // ── Ollama / LLM panel ───────────────────────────────────────────────────── function OllamaPanel({ data, ramAvailGb, onUnload, unloading, }: { data: OllamaModelsResponse | null; ramAvailGb: number; onUnload: (name: string) => Promise; unloading: Set; }) { if (!data) return null; const { models, running } = data; if (models.length === 0 && running.length === 0) return null; return (

LLM Models (Ollama)

{models.length} on disk · {running.length} loaded
{/* Currently loaded */} {running.length > 0 && (

Currently Loaded

{running.map(r => { const ramAfterUnloadGb = ramAvailGb + r.sizeGB; const pressureAfter = ramAfterUnloadGb < 2; return (
{r.name} {r.sizeGB} GB · {r.processor || 'CPU'} {r.expiresAt && ( expires {relativeTime(r.expiresAt)} )} {ramAvailGb < 4 && ( low RAM — swap pressure likely )} {pressureAfter && ( (unloading frees {r.sizeGB} GB) )}
); })}
)} {/* RAM bar */} {ramAvailGb > 0 && (
RAM available {ramAvailGb.toFixed(1)} GB free
{running.map(r => (
))}

{running.length > 0 ? `${running.reduce((s, r) => s + r.sizeGB, 0).toFixed(1)} GB used by loaded models` : 'No models loaded'}

)} {/* All models */}
{models.map(m => { const isLoaded = running.some(r => r.name === m.name); return (
{m.name} {m.sizeGB} GB {isLoaded && ( loaded )} {m.modifiedAt && ( {relativeTime(m.modifiedAt)} )}
); })}
); } // ── Check card meta ──────────────────────────────────────────────────────── const CHECK_META: Record = { disk: { label: 'Disk', icon: HardDrive }, load: { label: 'CPU Load', icon: Cpu }, steal: { label: 'CPU Steal', icon: Shield }, ram: { label: 'Memory', icon: Database }, swap: { label: 'Swap', icon: Server }, container_loops: { label: 'Crash Loops', icon: Activity }, container_health: { label: 'Container Health', icon: Layers }, docker_daemon: { label: 'Docker Daemon', icon: Activity }, build_cache: { label: 'Build Cache', icon: Layers }, docker_images: { label: 'Docker Images', icon: Layers }, journal: { label: 'Journal Logs', icon: ScrollText }, syslog: { label: 'Syslog', icon: ScrollText }, failed_units: { label: 'Systemd Units', icon: Activity }, cron_missing_paths: { label: 'Cron Paths', icon: Clock }, }; const CHECK_ORDER = [ 'disk', 'load', 'steal', 'ram', 'swap', 'container_loops', 'container_health', 'docker_daemon', 'build_cache', 'docker_images', 'journal', 'syslog', 'failed_units', 'cron_missing_paths', ]; // ── Main page ────────────────────────────────────────────────────────────── export default function VmHealthPage() { const [health, setHealth] = useState(null); const [cronData, setCronData] = useState(null); const [unhealthy, setUnhealthy] = useState([]); const [ollamaData, setOllamaData] = useState(null); const [cleanupLog, setCleanupLog] = useState(''); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [cleanupRunning, setCleanupRunning] = useState(false); const [cleanupResult, setCleanupResult] = useState<{ success: boolean; output: string } | null>(null); const [restarting, setRestarting] = useState>(new Set()); const [restartMsg, setRestartMsg] = useState<{ name: string; ok: boolean; msg: string } | null>(null); const [unloading, setUnloading] = useState>(new Set()); const [showLog, setShowLog] = useState(false); const [lastRefreshed, setLastRefreshed] = useState(null); const loadAll = useCallback(async () => { try { const [healthData, logData, cronResult, unhealthyResult, ollamaResult] = await Promise.allSettled([ vmApi.getHealth(), vmApi.getCleanupLog(40), vmApi.getCronStatus(), vmApi.getUnhealthyContainers(), vmApi.getOllamaModels(), ]); if (healthData.status === 'fulfilled') setHealth(healthData.value); if (logData.status === 'fulfilled') setCleanupLog(logData.value.log); if (cronResult.status === 'fulfilled') setCronData(cronResult.value); if (unhealthyResult.status === 'fulfilled') setUnhealthy(unhealthyResult.value.containers); if (ollamaResult.status === 'fulfilled') setOllamaData(ollamaResult.value); setLastRefreshed(new Date()); } catch (e) { console.error('Failed to load VM data:', e); } finally { setLoading(false); setRefreshing(false); } }, []); useEffect(() => { loadAll(); const interval = setInterval(loadAll, 60_000); return () => clearInterval(interval); }, [loadAll]); const handleRefresh = () => { setRefreshing(true); loadAll(); }; const handleCleanup = async (mode: 'weekly' | 'monthly' | 'dry-run') => { const msg = mode === 'monthly' ? 'Run MONTHLY full cleanup? This removes build cache, pnpm store, old logs, and HOLD node_modules.' : mode === 'dry-run' ? 'Run cleanup in DRY-RUN mode? Nothing will be deleted.' : 'Run weekly cleanup? This prunes Docker build cache, journal, apt, and .next/cache.'; if (!confirm(msg)) return; setCleanupRunning(true); setCleanupResult(null); try { const result = await vmApi.runCleanup(mode); setCleanupResult(result); await loadAll(); } catch (e) { setCleanupResult({ success: false, output: String(e) }); } finally { setCleanupRunning(false); } }; const handleRestart = async (name: string) => { setRestarting(prev => new Set(prev).add(name)); setRestartMsg(null); try { const result = await vmApi.restartContainer(name); setRestartMsg({ name, ok: result.success, msg: result.message }); if (result.success) { await new Promise(r => setTimeout(r, 3000)); await loadAll(); } } catch (e) { setRestartMsg({ name, ok: false, msg: String(e) }); } finally { setRestarting(prev => { const s = new Set(prev); s.delete(name); return s; }); } }; const handleUnload = async (name: string) => { setUnloading(prev => new Set(prev).add(name)); try { await vmApi.unloadOllamaModel(name); await loadAll(); } catch (e) { console.error('Failed to unload model:', e); } finally { setUnloading(prev => { const s = new Set(prev); s.delete(name); return s; }); } }; // ── Loading ─────────────────────────────────────────────────────────────── if (loading) { return (
Loading VM health…
); } const overall = health?.overall ?? 'CRIT'; const checks = health?.checks ?? {}; const sortedKeys = [ ...CHECK_ORDER.filter(k => k in checks), ...Object.keys(checks).filter(k => !CHECK_ORDER.includes(k)), ]; const warnings = sortedKeys.filter(k => checks[k]?.level === 'WARN'); const crits = sortedKeys.filter(k => checks[k]?.level === 'CRIT'); // Parse available RAM for Ollama panel RAM bar const ramAvailGb = parseFloat( (checks.ram?.value ?? '0G').match(/^([\d.]+)/)?.[1] ?? '0' ); // ── Render ──────────────────────────────────────────────────────────────── return (
{/* ── Header ── */}

VM Health

{health?.hostname ?? 'srv1491630'} ·{' '} {lastRefreshed ? `last checked ${lastRefreshed.toLocaleTimeString()}` : 'checking…'}

{/* ── Score card ── */} {/* ── Overall status banner ── */}

{overall === 'OK' ? 'All checks passing' : overall === 'WARN' ? `${warnings.length} warning${warnings.length !== 1 ? 's' : ''}` : `${crits.length} critical issue${crits.length !== 1 ? 's' : ''}`}

{health?.error && (

{health.error}

)} {(crits.length > 0 || warnings.length > 0) && !health?.error && (

{[...crits, ...warnings].map(k => checks[k]?.message).join(' · ')}

)}
{overall}
{/* ── Check cards grid ── */}
{sortedKeys.map(key => { const check = checks[key]; if (!check) return null; const meta = CHECK_META[key] ?? { label: key, icon: Activity }; const Icon = meta.icon; return (
{meta.label} {check.level}

{check.message}

{check.value}

); })}
{/* ── Restart feedback ── */} {restartMsg && (
{restartMsg.ok ? : } {restartMsg.name}: {restartMsg.msg}
)} {/* ── Unhealthy containers ── */} {/* ── Cron status ── */} {/* ── Ollama ── */} {/* ── Cleanup section ── */}

VM Cleanup

Cron runs automatically: daily build-cache prune, weekly cleanup, monthly full cleanup. Use buttons below to trigger manually.

{cleanupResult && (
{cleanupResult.success ? : } {cleanupResult.success ? 'Cleanup completed' : 'Cleanup failed'}
{cleanupResult.output && (
                    {cleanupResult.output}
                  
)}
)}
{/* ── Cleanup log ── */} {cleanupLog && (
{showLog && (
                    {cleanupLog}
                  
)}
)}
); }