SPB Git

Repository Public

LiveOverview.tsx 3.5 KB 76 lines tsx

1"use client";23import Link from "next/link";4import { getDict, type Locale } from "@/lib/i18n";5import { StatusDot } from "./bits";6import { fmtPct } from "./format";7import { Kpis, type BoardNode } from "./LiveBoard";8import { useLive } from "./store";910/** Sequential blue ramp (light to dark) for CPU load; ink flips to white on the two darkest steps. */11const STEPS = [12  { max: 10, bg: "#cde2fb", fg: "#0c1322" },13  { max: 25, bg: "#86b6ef", fg: "#0c1322" },14  { max: 50, bg: "#3987e5", fg: "#0c1322" },15  { max: 75, bg: "#1c5cab", fg: "#ffffff" },16  { max: 101, bg: "#0d366b", fg: "#ffffff" },17];1819/** The "wall": one cell per machine, shaded by CPU load, each linking to its record. */20export function NodeWall({ nodes, locale }: { nodes: BoardNode[]; locale: Locale }) {21  const t = getDict(locale).live;22  const { snap } = useLive();23  const byId = new Map((snap?.nodes ?? []).map((n) => [n.id, n]));24  return (25    <div className="card p-5 md:p-6">26      <div className="flex flex-wrap items-center justify-between gap-3">27        <h3 className="text-[0.9375rem] font-semibold">{t.home.wall}</h3>28        <p className="flex items-center gap-2 text-[0.75rem] text-mute" aria-hidden="true">29          030          <span className="flex gap-0.5">{STEPS.map((s) => <span key={s.bg} className="h-2.5 w-5 rounded-[3px]" style={{ background: s.bg }} />)}</span>31          {fmtPct(100, locale)}32        </p>33      </div>34      <ul className="mt-4 grid grid-cols-6 gap-1.5 sm:grid-cols-10 lg:grid-cols-[repeat(15,minmax(0,1fr))]" data-testid="node-wall">35        {nodes.map((n) => {36          const l = byId.get(n.id);37          const on = l?.online ?? false;38          const step = on ? STEPS.find((s) => l!.cpu < s.max)! : null;39          const label = `${n.name} — ${on ? `${t.chart.cpu} ${fmtPct(l!.cpu, locale)}` : snap ? t.nodes.offline : t.nodes.noReading}`;40          return (41            <li key={n.id}>42              <Link href={`/${locale}/nodes/${n.id}`} title={label} aria-label={label} className={`num flex aspect-square items-center justify-center rounded-lg font-mono text-[0.6875rem] font-medium transition-transform hover:scale-110 ${step ? "" : "border border-dashed border-line-strong bg-surface-2 text-mute"}`} style={step ? { background: step.bg, color: step.fg } : undefined}>43                {String(n.number).padStart(2, "0")}44              </Link>45            </li>46          );47        })}48      </ul>49      <p className="mt-3 text-[0.75rem] text-mute">{t.home.wallNote}</p>50    </div>51  );52}5354export function LiveOverview({ nodes, locale }: { nodes: BoardNode[]; locale: Locale }) {55  return (56    <div className="space-y-3">57      <Kpis locale={locale} />58      <NodeWall nodes={nodes} locale={locale} />59    </div>60  );61}6263/** Compact live summary used in the hero and the header. */64export function LivePill({ locale, className = "" }: { locale: Locale; className?: string }) {65  const t = getDict(locale).live;66  const { ready, snap } = useLive();67  const c = snap?.cluster;68  return (69    <Link href={`/${locale}/live`} className={`inline-flex max-w-full items-center gap-2.5 rounded-full border border-line bg-surface py-1.5 pl-3 pr-4 text-[0.8125rem] text-body shadow-card transition-colors hover:border-accent ${className}`} data-testid="live-pill">70      <StatusDot state={c ? "on" : "off"} />71      <span className="font-medium text-fg">{t.badge}</span>72      <span className="num truncate">{c ? t.pill(c.online, c.total, fmtPct(c.cpu, locale)) : ready ? t.unavailable.short : t.loading}</span>73    </Link>74  );75}76
Files main
Menu

Theme