import { useState, useEffect, useMemo } from 'react'; import { useNodes } from '@/context/NodeContext'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from './ui/card'; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'; import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog'; import { Activity, Square, ArrowRight, Plus, Cpu, HardDrive, MemoryStick, Network } from 'lucide-react'; import { apiFetch } from '@/lib/api'; import { toast } from 'sonner'; import { Label } from './ui/label'; interface Stats { active: number; managed: number; unmanaged: number; exited: number; total: number; } interface MetricPoint { timestamp: number; cpu_percent: number; memory_mb: number; } interface SystemStats { cpu: { usage: string; cores: number; }; memory: { total: number; used: number; free: number; usagePercent: string; }; disk: { fs: string; mount: string; total: number; used: number; free: number; usagePercent: string; } | null; network?: { rxBytes: number; txBytes: number; rxSec: number; txSec: number; }; } const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; export default function HomeDashboard() { const { activeNode } = useNodes(); const [dockerRunInput, setDockerRunInput] = useState(''); const [isConverting, setIsConverting] = useState(false); const [convertedYaml, setConvertedYaml] = useState(''); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [newStackName, setNewStackName] = useState(''); const [stats, setStats] = useState({ active: 0, managed: 0, unmanaged: 0, exited: 0, total: 0 }); const [systemStats, setSystemStats] = useState(null); const [metrics, setMetrics] = useState([]); // Fetch container stats - re-runs when active node changes so stale data is cleared immediately useEffect(() => { setStats({ active: 0, managed: 0, unmanaged: 0, exited: 0, total: 0 }); const fetchStats = async () => { try { const res = await apiFetch('/stats'); if (!res.ok) return; const data = await res.json(); setStats(data); } catch (error) { console.error('Failed to fetch stats:', error); } }; fetchStats(); const interval = setInterval(fetchStats, 5000); return () => clearInterval(interval); }, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps // Fetch system stats (CPU/RAM/Disk/Network) - 5s polling, re-runs on node switch useEffect(() => { setSystemStats(null); const fetchSystemStats = async () => { try { const res = await apiFetch('/system/stats'); if (res.ok) setSystemStats(await res.json()); } catch (error) { console.error('Failed to fetch system stats:', error); } }; fetchSystemStats(); const interval = setInterval(fetchSystemStats, 5000); return () => clearInterval(interval); }, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps // Fetch historical metrics - intentionally slow 60s poll to prevent OOM. // The backend now returns 1-minute buckets (down from raw 5s points), so // re-fetching more often than 60s would return the same data anyway. useEffect(() => { setMetrics([]); const fetchMetrics = async () => { try { const res = await apiFetch('/metrics/historical'); if (res.ok) setMetrics(await res.json()); } catch (error) { console.error('Failed to fetch historical metrics:', error); } }; fetchMetrics(); const interval = setInterval(fetchMetrics, 60000); return () => clearInterval(interval); }, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps const chartData = useMemo(() => { const buckets: Record = {}; const cores = systemStats?.cpu.cores || 1; metrics.forEach(m => { const date = new Date(m.timestamp); date.setSeconds(0, 0); const key = date.getTime() + ''; if (!buckets[key]) { buckets[key] = { time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), timestamp: date.getTime(), cpu: 0, ram: 0 }; } buckets[key].cpu += (m.cpu_percent / cores); buckets[key].ram += (m.memory_mb / 1024); }); return Object.values(buckets).sort((a, b) => a.timestamp - b.timestamp); }, [metrics, systemStats]); const chartConfig = { cpu: { label: 'CPU Usage (%)', color: 'var(--chart-1)' }, ram: { label: 'RAM Usage (GB)', color: 'var(--chart-2)' }, }; const handleConvert = async () => { if (!dockerRunInput.trim()) return; setIsConverting(true); try { const response = await apiFetch('/convert', { method: 'POST', body: JSON.stringify({ dockerRun: dockerRunInput }), }); if (!response.ok) throw new Error('Conversion failed'); const data = await response.json(); setConvertedYaml(data.yaml); } catch (error) { console.error('Conversion error:', error); toast.error('Failed to convert docker run command'); } finally { setIsConverting(false); } }; const handleCreateStack = async () => { if (!newStackName.trim() || !convertedYaml) return; // Send stackName directly (no .yml extension - backend creates directory) const stackName = newStackName.trim(); try { // Create the stack const createResponse = await apiFetch('/stacks', { method: 'POST', body: JSON.stringify({ stackName }), }); if (!createResponse.ok) { const err = await createResponse.json().catch(() => ({})); throw new Error(err.error || 'Failed to create stack'); } // Save the converted YAML content const saveResponse = await apiFetch(`/stacks/${stackName}`, { method: 'PUT', body: JSON.stringify({ content: convertedYaml }), }); if (!saveResponse.ok) { const err = await saveResponse.json().catch(() => ({})); throw new Error(err.error || 'Failed to save stack content'); } setCreateDialogOpen(false); setNewStackName(''); setConvertedYaml(''); setDockerRunInput(''); window.location.reload(); // Refresh to show new stack } catch (error) { console.error('Failed to create stack:', error); toast.error((error as Error).message || 'Failed to create stack'); } }; const handleUseConvertedYaml = () => { setCreateDialogOpen(true); }; return (
{/* Container Stats Row */}
Active Containers
{stats.active}

{stats.managed} managed · {stats.unmanaged} external

Exited Containers
{stats.exited}

Stopped or crashed

Docker Network
{systemStats?.network ? `${formatBytes(systemStats.network.rxSec)}/s ↓` : '...'}

{systemStats?.network ? `${formatBytes(systemStats.network.txSec)}/s ↑` : 'Loading...'}

{/* Host System Stats Row */}
Host CPU
{systemStats ? `${systemStats.cpu.usage}%` : '...'}

{systemStats ? `${systemStats.cpu.cores} cores` : 'Loading...'}

Host RAM
{systemStats ? `${systemStats.memory.usagePercent}%` : '...'}

{systemStats ? `${formatBytes(systemStats.memory.used)} / ${formatBytes(systemStats.memory.total)}` : 'Loading...'}

Host Disk
{systemStats?.disk ? `${systemStats.disk.usagePercent}%` : '...'}

{systemStats?.disk ? `${formatBytes(systemStats.disk.used)} / ${formatBytes(systemStats.disk.total)}` : 'Loading...'}

{/* Historical Charts */}
Normalized CPU Usage Total CPU percentage over total host cores. {chartData.length > 0 ? ( `${Number(val).toFixed(0)}%`} domain={[0, 100]} /> } /> ) : (
No historical CPU data.
)}
Normalized RAM Usage Total RAM allocation in GB. {chartData.length > 0 ? ( `${Number(val).toFixed(1)} GB`} /> } /> ) : (
No historical RAM data.
)}
{/* Docker Run Converter */} Convert Docker Run to Compose

Paste your docker run command below to convert it to a Docker Compose YAML file.