SPB Git forge
2commits 1branches 0releases
492.0 KBsize
maindefault branch
2 h agolast push
TypeScript 96.1% CSS 3.1% JavaScript 0.8%
4.0 KB · 111 lines tsx
Raw Blame History
1"use client";23import { ClusterNode, TIER_COLORS } from "@/lib/cluster";4import type { NodeMetrics } from "@/lib/ssh";56interface TailscaleStatus {7  online: boolean;8  lastSeen: string;9  ip: string;10}1112export default function NodeGrid({13  nodes,14  statusMap,15  metricsMap,16  selectedNode,17  onSelect,18}: {19  nodes: ClusterNode[];20  statusMap: Record<string, TailscaleStatus>;21  metricsMap: Record<string, NodeMetrics>;22  selectedNode: string | null;23  onSelect: (id: string) => void;24}) {25  return (26    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">27      {nodes.map((node) => {28        const ts = statusMap[node.id];29        const m = metricsMap[node.id];30        const online = ts?.online ?? m?.online ?? false;31        const selected = selectedNode === node.id;32        const color = TIER_COLORS[node.tier];33        const cpu = m ? Math.round(m.cpuUsage) : null;34        const mem = m && m.memoryTotal > 0 ? Math.round((m.memoryUsed / m.memoryTotal) * 100) : null;35        const dsk = m && m.diskTotal > 0 ? Math.round((m.diskUsed / m.diskTotal) * 100) : null;36        const ext = m && (m.extDiskTotal ?? 0) > 0 ? Math.round((m.extDiskUsed / m.extDiskTotal) * 100) : null;3738        return (39          <button40            key={node.id}41            onClick={() => onSelect(node.id)}42            className={`glass glass-hover text-left rounded-2xl p-4 ${43              selected ? "ring-2 ring-violet-500/50 border-violet-300" : ""44            } ${online ? "" : "opacity-60"}`}45          >46            <div className="mb-3 flex items-center justify-between">47              <div className="flex items-center gap-2">48                <span className="text-base font-semibold text-zinc-900">{node.name}</span>49                <span50                  className="rounded-full border px-1.5 py-0.5 text-[10px] font-semibold uppercase"51                  style={{ color, borderColor: color + "55", backgroundColor: color + "18" }}52                >53                  {node.tier}54                </span>55              </div>56              <span className={`h-2.5 w-2.5 rounded-full ${online ? "dot-online" : "dot-offline"}`} />57            </div>5859            <div className="mb-3 text-xs text-zinc-500">60              {node.chip} · {node.modelName}61            </div>6263            {/* Usage bars */}64            <div className="space-y-2">65              <UsageBar label="CPU" value={cpu} color="#2563eb" />66              <UsageBar label="MEM" value={mem} color="#7c3aed" />67              <UsageBar label="DSK" value={dsk} color="#d97706" />68              {ext !== null && <UsageBar label="EXT" value={ext} color="#ca8a04" />}69            </div>7071            <div className="mt-3 flex items-center justify-between text-[10px] text-zinc-500">72              <span>73                {node.cpuCores} cores · {node.memoryGB} GB · {node.gpuCores} GPU74                {ext !== null && (75                  <span className="text-yellow-600 font-medium"> · +{fmtStorage(m!.extDiskTotal)} ext</span>76                )}77              </span>78              {m && <span className="font-mono">load {m.loadAverage[0].toFixed(1)}</span>}79            </div>80          </button>81        );82      })}83    </div>84  );85}8687function fmtStorage(bytes: number): string {88  const gb = bytes / (1024 * 1024 * 1024);89  if (gb >= 1024) return `${(gb / 1024).toFixed(1)} TB`;90  return `${Math.round(gb)} GB`;91}9293function UsageBar({ label, value, color }: { label: string; value: number | null; color: string }) {94  const v = value ?? 0;95  const hot = v >= 85;96  return (97    <div>98      <div className="mb-1 flex items-center justify-between text-[11px]">99        <span className="font-medium text-zinc-500">{label}</span>100        <span className="font-mono tabular-nums text-zinc-700">{value === null ? "—" : `${v}%`}</span>101      </div>102      <div className="h-1.5 w-full overflow-hidden rounded-full bg-zinc-200">103        <div104          className="h-full rounded-full transition-all"105          style={{ width: `${v}%`, backgroundColor: hot ? "#dc2626" : color }}106        />107      </div>108    </div>109  );110}111