SPB Git forge
2commits 1branches 0releases
492.0 KBsize
maindefault branch
2 h agolast push
TypeScript 96.1% CSS 3.1% JavaScript 0.8%
6.1 KB · 119 lines tsx
Raw Blame History
1"use client";23import { CLUSTER_NODES, TOTAL_CORES, TOTAL_MEMORY, TOTAL_GPU, TIER_COLORS } from "@/lib/cluster";4import type { NodeMetrics } from "@/lib/ssh";56interface TailscaleStatus {7  online: boolean;8  lastSeen: string;9  ip: string;10}1112/** Format a byte count, auto-switching GB -> TB above 1024 GB. */13function formatStorage(bytes: number): string {14  const gb = bytes / (1024 * 1024 * 1024);15  if (gb >= 1024) return `${(gb / 1024).toFixed(2)} TB`;16  return `${Math.round(gb)} GB`;17}1819export default function ClusterOverview({20  statusMap,21  metricsMap = {},22}: {23  statusMap: Record<string, TailscaleStatus>;24  metricsMap?: Record<string, NodeMetrics>;25}) {26  const onlineCount = Object.values(statusMap).filter((s) => s.online).length;27  const total = CLUSTER_NODES.length;28  const pct = total ? Math.round((onlineCount / total) * 100) : 0;29  const tiers = ["ultra", "max", "pro", "base"] as const;3031  // Cluster-wide storage totals (internal SSD + external drives) across reporting nodes.32  const liveMetrics = Object.values(metricsMap).filter((m) => m.online);33  const storageUsed = liveMetrics.reduce((a, m) => a + m.diskUsed + (m.extDiskUsed || 0), 0);34  const storageTotal = liveMetrics.reduce((a, m) => a + m.diskTotal + (m.extDiskTotal || 0), 0);35  const extTotal = liveMetrics.reduce((a, m) => a + (m.extDiskTotal || 0), 0);3637  return (38    <div className="space-y-5 fade-in">39      {/* Hero stat cards */}40      <div className="grid grid-cols-2 md:grid-cols-5 gap-4">41        <StatCard label="Nodes Online" value={`${onlineCount}/${total}`} sub={`${pct}% reachable`} accent="#16a34a" icon={<ServerIcon />}>42          <div className="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-zinc-200">43            <div className="h-full rounded-full" style={{ width: `${pct}%`, background: "linear-gradient(90deg,#16a34a,#4ade80)" }} />44          </div>45        </StatCard>46        <StatCard label="CPU Cores" value={TOTAL_CORES.toString()} sub="across cluster" accent="#2563eb" icon={<CpuIcon />} />47        <StatCard label="Memory" value={`${TOTAL_MEMORY}`} unit="GB" sub="unified" accent="#7c3aed" icon={<MemIcon />} />48        <StatCard label="GPU Cores" value={TOTAL_GPU.toString()} sub="Metal" accent="#d97706" icon={<GpuIcon />} />49        <StatCard50          label="Storage"51          value={storageTotal > 0 ? formatStorage(storageUsed) : "—"}52          sub={storageTotal > 0 ? `of ${formatStorage(storageTotal)}${extTotal > 0 ? ` · +${formatStorage(extTotal)} ext` : ""}` : "internal + external"}53          accent="#0891b2"54          icon={<DiskIcon />}55        />56      </div>5758      {/* Tier breakdown */}59      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">60        {tiers.map((tier) => {61          const nodes = CLUSTER_NODES.filter((n) => n.tier === tier);62          const online = nodes.filter((n) => statusMap[n.id]?.online).length;63          const tpct = nodes.length ? Math.round((online / nodes.length) * 100) : 0;64          const color = TIER_COLORS[tier];65          return (66            <div key={tier} className="glass glass-hover rounded-2xl p-4">67              <div className="mb-2 flex items-center justify-between">68                <div className="flex items-center gap-2">69                  <span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color, boxShadow: `0 0 8px ${color}66` }} />70                  <span className="text-xs font-semibold uppercase tracking-wider text-zinc-600">{tier}</span>71                </div>72                <span className="text-xs text-zinc-500">{online}/{nodes.length}</span>73              </div>74              <div className="text-2xl font-bold text-zinc-900 tabular-nums">75                {nodes.reduce((a, n) => a + n.cpuCores, 0)}76                <span className="ml-1 text-sm font-normal text-zinc-500">cores</span>77              </div>78              <div className="mb-2 text-xs text-zinc-500">{nodes.reduce((a, n) => a + n.memoryGB, 0)} GB · {nodes.reduce((a, n) => a + n.gpuCores, 0)} GPU</div>79              <div className="h-1 w-full overflow-hidden rounded-full bg-zinc-200">80                <div className="h-full rounded-full" style={{ width: `${tpct}%`, backgroundColor: color }} />81              </div>82            </div>83          );84        })}85      </div>86    </div>87  );88}8990function StatCard({91  label, value, unit, sub, accent, icon, children,92}: {93  label: string; value: string; unit?: string; sub: string; accent: string; icon: React.ReactNode; children?: React.ReactNode;94}) {95  return (96    <div className="glass glass-hover relative overflow-hidden rounded-2xl p-4">97      <div className="pointer-events-none absolute -right-6 -top-6 h-20 w-20 rounded-full opacity-25 blur-2xl" style={{ background: accent }} />98      <div className="flex items-start justify-between">99        <div className="text-xs font-medium uppercase tracking-wide text-zinc-500">{label}</div>100        <div style={{ color: accent }}>{icon}</div>101      </div>102      <div className="mt-2 text-3xl font-bold tabular-nums text-zinc-900">103        {value}104        {unit && <span className="ml-1 text-base font-medium text-zinc-500">{unit}</span>}105      </div>106      <div className="mt-0.5 text-xs text-zinc-500">{sub}</div>107      {children}108    </div>109  );110}111112/* Minimal inline icons */113const 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 };114const ServerIcon = () => (<svg {...S}><rect x="3" y="4" width="18" height="6" rx="2" /><rect x="3" y="14" width="18" height="6" rx="2" /><path d="M7 7h.01M7 17h.01" /></svg>);115const CpuIcon = () => (<svg {...S}><rect x="6" y="6" width="12" height="12" rx="2" /><path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" /></svg>);116const MemIcon = () => (<svg {...S}><rect x="3" y="7" width="18" height="10" rx="2" /><path d="M7 7v10M12 7v10M17 7v10" /></svg>);117const GpuIcon = () => (<svg {...S}><rect x="2" y="6" width="20" height="12" rx="2" /><circle cx="9" cy="12" r="2.5" /><path d="M15 10h4M15 14h4" /></svg>);118const DiskIcon = () => (<svg {...S}><rect x="3" y="4" width="18" height="16" rx="2" /><path d="M7 8h10M7 12h10" /><circle cx="8" cy="16" r="1" /></svg>);119