SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
4.0 KB · 77 lines tsx
Raw Blame History
1"use client";23import Link from "next/link";4import { useEffect, useRef, useState } from "react";5import type { Stats } from "@/lib/api";6import { fmtInt, relTime } from "@/lib/format";7import { publicFetch } from "@/lib/owner";89/**10 * Live system strip (spec §65): SOURCES · SENSORS · CHECKS/MIN · EVENTS 24H · BREAKING · SILENT · STATUS.11 * Server-rendered with real values, then refreshed every 10 s. Numbers tick when they change.12 */13export function LiveStrip({ initial }: { initial: Stats }) {14  const [s, setS] = useState<Stats>(initial);15  const [now, setNow] = useState(() => Date.now());16  useEffect(() => {17    const tick = (): void => {18      publicFetch<Stats>("/api/v1/stats").then(setS).catch(() => undefined);19      setNow(Date.now());20    };21    const t = setInterval(tick, 10_000);22    const n = setInterval(() => setNow(Date.now()), 1000);23    return () => {24      clearInterval(t);25      clearInterval(n);26    };27  }, []);28  const lastCheckAge = s.last_check_at ? now - new Date(s.last_check_at).getTime() : Infinity;29  const live = lastCheckAge < 120_000;30  // `now` differs between server and client renders; the status word is re-rendered right after mount.31  const items: { label: string; value: number | undefined; href?: string; tone?: string; hint?: string }[] = [32    { label: "Sources", value: s.sources, href: "/sources", hint: `${fmtInt(s.sources_first_party)} first-party` },33    { label: "Sensors", value: s.sensors, href: "/health", hint: s.sensors_degraded ? `${fmtInt(s.sensors_degraded)} degraded` : "all healthy" },34    { label: "Checks / min", value: s.checks_per_min, href: "/health", hint: s.not_modified_ratio_5m !== null && s.not_modified_ratio_5m !== undefined ? `${Math.round(s.not_modified_ratio_5m * 100)}% 304` : undefined },35    { label: "Events 24h", value: s.events_24h, href: "/live", hint: s.events_per_min !== undefined ? `${s.events_per_min}/min` : undefined },36    { label: "Breaking", value: s.breaking_now ?? s.breaking_24h, href: "/breaking", tone: "text-hot", hint: s.developing_now ? `${s.developing_now} developing` : undefined },37    { label: "Silent 24h", value: s.silent_24h, href: "/silent", tone: "text-silent" },38  ];39  return (40    <div className="panel mb-4 flex items-stretch overflow-x-auto no-scrollbar" role="region" aria-label="Live system status">41      {items.map((it) => (42        <Cell key={it.label} {...it} />43      ))}44      <div className="ml-auto flex min-w-[7.5rem] flex-col justify-center gap-0.5 border-l border-line px-3 py-2">45        <span className="label">Status</span>46        <span className="inline-flex items-center gap-1.5 font-mono text-[12px] font-semibold tracking-wider" suppressHydrationWarning>47          <span className={`inline-block size-2 rounded-full ${live ? "bg-signal animate-pulse-dot" : "bg-warn"}`} />48          {live ? "LIVE" : "STALE"}49        </span>50        <span className="truncate text-[10.5px] text-fg-subtle" suppressHydrationWarning>{s.last_check_at ? `last check ${relTime(s.last_check_at, now)}` : "—"}</span>51      </div>52    </div>53  );54}5556function Cell({ label, value, href, tone, hint }: { label: string; value: number | undefined; href?: string; tone?: string; hint?: string }) {57  const prev = useRef(value);58  const [tick, setTick] = useState(false);59  useEffect(() => {60    if (prev.current !== value) {61      prev.current = value;62      setTick(true);63      const t = setTimeout(() => setTick(false), 600);64      return () => clearTimeout(t);65    }66  }, [value]);67  const body = (68    <>69      <span className="label">{label}</span>70      <span className={`font-mono text-lg font-semibold leading-tight tabular ${tone ?? ""} ${tick ? "animate-tick" : ""}`}>{fmtInt(value)}</span>71      {hint && <span className="truncate text-[10.5px] text-fg-subtle">{hint}</span>}72    </>73  );74  const cls = "flex min-w-[7.25rem] flex-col gap-0.5 border-r border-line px-3 py-2 last:border-r-0";75  return href ? <Link href={href} className={`${cls} hover:bg-panel-2/60`}>{body}</Link> : <div className={cls}>{body}</div>;76}77