"use client"; import { CLUSTER_NODES, TOTAL_CORES, TOTAL_MEMORY, TOTAL_GPU, TIER_COLORS } from "@/lib/cluster"; import type { NodeMetrics } from "@/lib/ssh"; interface TailscaleStatus { online: boolean; lastSeen: string; ip: string; } /** Format a byte count, auto-switching GB -> TB above 1024 GB. */ function formatStorage(bytes: number): string { const gb = bytes / (1024 * 1024 * 1024); if (gb >= 1024) return `${(gb / 1024).toFixed(2)} TB`; return `${Math.round(gb)} GB`; } export default function ClusterOverview({ statusMap, metricsMap = {}, }: { statusMap: Record; metricsMap?: Record; }) { const onlineCount = Object.values(statusMap).filter((s) => s.online).length; const total = CLUSTER_NODES.length; const pct = total ? Math.round((onlineCount / total) * 100) : 0; const tiers = ["ultra", "max", "pro", "base"] as const; // Cluster-wide storage totals (internal SSD + external drives) across reporting nodes. const liveMetrics = Object.values(metricsMap).filter((m) => m.online); const storageUsed = liveMetrics.reduce((a, m) => a + m.diskUsed + (m.extDiskUsed || 0), 0); const storageTotal = liveMetrics.reduce((a, m) => a + m.diskTotal + (m.extDiskTotal || 0), 0); const extTotal = liveMetrics.reduce((a, m) => a + (m.extDiskTotal || 0), 0); return (
{/* Hero stat cards */}
}>
} /> } /> } /> 0 ? formatStorage(storageUsed) : "—"} sub={storageTotal > 0 ? `of ${formatStorage(storageTotal)}${extTotal > 0 ? ` · +${formatStorage(extTotal)} ext` : ""}` : "internal + external"} accent="#0891b2" icon={} />
{/* Tier breakdown */}
{tiers.map((tier) => { const nodes = CLUSTER_NODES.filter((n) => n.tier === tier); const online = nodes.filter((n) => statusMap[n.id]?.online).length; const tpct = nodes.length ? Math.round((online / nodes.length) * 100) : 0; const color = TIER_COLORS[tier]; return (
{tier}
{online}/{nodes.length}
{nodes.reduce((a, n) => a + n.cpuCores, 0)} cores
{nodes.reduce((a, n) => a + n.memoryGB, 0)} GB · {nodes.reduce((a, n) => a + n.gpuCores, 0)} GPU
); })}
); } function StatCard({ label, value, unit, sub, accent, icon, children, }: { label: string; value: string; unit?: string; sub: string; accent: string; icon: React.ReactNode; children?: React.ReactNode; }) { return (
{label}
{icon}
{value} {unit && {unit}}
{sub}
{children}
); } /* Minimal inline icons */ const S = { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round" as const, strokeLinejoin: "round" as const }; const ServerIcon = () => (); const CpuIcon = () => (); const MemIcon = () => (); const GpuIcon = () => (); const DiskIcon = () => ();