import { useState } from 'react'; import { Server, Cpu, MemoryStick, HardDrive, ChevronDown, ChevronRight, Layers, Wifi, WifiOff, AlertTriangle, Download, Loader2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { formatBytes } from '@/lib/utils'; import { apiFetch } from '@/lib/api'; import { useLicense } from '@/context/LicenseContext'; import { toast } from '@/components/ui/toast-store'; import { formatVersion } from '@/lib/version'; import { UpdateStatusBadge } from './UpdateStatusBadge'; import { StackSection } from './NodeCardStackList'; import type { Label as StackLabel } from '../label-types'; import type { FleetNode, NodeUpdateStatus } from './types'; import { getNodeCpu, getNodeMem, getNodeDisk, isCritical } from './nodeUtils'; // --- Types --- export interface NodeCardProps { node: FleetNode; onNavigate: (nodeId: number, stackName: string) => void; labelMap?: Record; updateStatus?: NodeUpdateStatus; onUpdate?: (nodeId: number) => void; updatingNodeId?: number | null; onRetryUpdate?: (nodeId: number) => void; onDismissUpdate?: (nodeId: number) => void; } // --- Sub-Components --- function UsageBar({ percent, color }: { percent: number; color: string }) { return (
); } // --- Main Export --- export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updatingNodeId, onRetryUpdate, onDismissUpdate }: NodeCardProps) { const { isPaid } = useLicense(); const [expanded, setExpanded] = useState(false); const [stacks, setStacks] = useState(node.stacks); const [loadingStacks, setLoadingStacks] = useState(false); const isOnline = node.status === 'online'; const isLocal = node.type === 'local'; const formattedVersion = formatVersion(updateStatus?.version); const formattedLatest = formatVersion(updateStatus?.latestVersion); const cpuPercent = getNodeCpu(node); const memPercent = getNodeMem(node); const diskPercent = getNodeDisk(node); const handleExpand = async () => { if (!isPaid) return; const next = !expanded; setExpanded(next); if (next && stacks === null) { setLoadingStacks(true); try { const res = await apiFetch(`/fleet/node/${node.id}/stacks`, { localOnly: true }); if (res.ok) { setStacks(await res.json()); } else { toast.error('Failed to load stacks for ' + node.name); } } catch (error) { console.error('Failed to load stacks for', node.name, error); toast.error('Failed to load stacks for ' + node.name); setExpanded(false); } finally { setLoadingStacks(false); } } }; const localRailClasses = isLocal ? 'relative overflow-hidden ring-1 ring-brand/30 before:absolute before:inset-y-0 before:left-0 before:w-[2px] before:bg-brand before:rounded-l-xl after:pointer-events-none after:absolute after:inset-0 after:bg-gradient-to-r after:from-brand/[0.06] after:via-transparent after:to-transparent' : ''; return (
{/* Card Header */}
{isLocal && ( ★ Local )}

{node.name}

{isOnline ? ( <> Online ) : ( <> Offline )} {node.type} {formattedVersion && ( {formattedVersion} )} {updateStatus?.updateStatus && ( onRetryUpdate(node.id) : undefined} onDismiss={onDismissUpdate ? () => onDismissUpdate(node.id) : undefined} /> )} {updateStatus?.updateAvailable && !updateStatus.updateStatus && ( Update available )} {isOnline && isCritical(node) && ( Critical )}
{/* Container Stats */} {isOnline && node.stats && (
{node.stats.active}
Running
{node.stats.exited}
Stopped
{node.stacks?.length ?? '-'}
Stacks
)} {/* Resource Usage Bars */} {isOnline && node.systemStats && (
CPU {node.systemStats.cpu.usage}%
80 ? 'bg-destructive/80' : cpuPercent > 60 ? 'bg-warning' : 'bg-success'} />
RAM {formatBytes(node.systemStats.memory.used, 1)} / {formatBytes(node.systemStats.memory.total, 1)}
80 ? 'bg-destructive/80' : memPercent > 60 ? 'bg-warning' : 'bg-brand/60'} />
{node.systemStats.disk && (
Disk {formatBytes(node.systemStats.disk.used, 1)} / {formatBytes(node.systemStats.disk.total, 1)}
90 ? 'bg-destructive/80' : diskPercent > 75 ? 'bg-warning' : 'bg-brand'} />
)}
)} {/* Update button */} {isOnline && updateStatus?.updateAvailable && !updateStatus.updateStatus && onUpdate && (
)} {/* Offline placeholder */} {!isOnline && (
Node unreachable
)}
{/* Paid: Expandable Stack List with Container Drill-Down */} {isOnline && isPaid && (
{expanded && (
{loadingStacks ? (
) : stacks && stacks.length > 0 ? (
{stacks.map(stack => ( ))}
) : (

No stacks found

)}
)}
)}
); }