SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
3.0 KB · 62 lines tsx
Raw Blame History
1"use client";23import { useEffect, useRef, useState } from "react";4import { clientApi } from "@/lib/client-api";5import { cx, formatCompact, formatDuration, formatInt } from "@/lib/format";6import type { Stats } from "@/lib/types";78function Counter({ value, format }: { value: number; format: (n: number) => string }) {9  const [display, setDisplay] = useState(value);10  const from = useRef(value);11  useEffect(() => {12    const start = from.current;13    const delta = value - start;14    if (!delta) return;15    const t0 = performance.now();16    let raf = 0;17    const step = (t: number) => {18      const k = Math.min(1, (t - t0) / 600);19      setDisplay(start + delta * (1 - (1 - k) ** 3));20      if (k < 1) raf = requestAnimationFrame(step);21      else from.current = value;22    };23    raf = requestAnimationFrame(step);24    return () => cancelAnimationFrame(raf);25  }, [value]);26  return <span suppressHydrationWarning>{format(display)}</span>;27}2829/** Homepage telemetry — every number comes from GET /v1/stats (refreshed every 5 s). */30export function TelemetryStrip({ initial, className }: { initial: Stats; className?: string }) {31  const [s, setS] = useState(initial);32  useEffect(() => {33    let alive = true;34    const t = setInterval(() => clientApi<Stats>("/v1/stats").then((x) => alive && setS(x)).catch(() => {}), 5000);35    return () => {36      alive = false;37      clearInterval(t);38    };39  }, []);40  const items: Array<{ label: string; value: number; format: (n: number) => string; sub?: string }> = [41    { label: "Instruments", value: s.instruments_total, format: formatInt, sub: `${formatInt(s.instruments_quoted)} quoted · ${formatInt(s.instruments_live)} live` },42    { label: "Exchanges", value: s.exchanges_total, format: formatInt, sub: `${s.exchanges_open} open now` },43    { label: "Active connectors", value: s.connectors_healthy, format: formatInt, sub: `${s.connectors_total} connectors · ${s.sources_total} sources` },44    { label: "Observations today", value: s.observations_today, format: (n) => formatCompact(n, 2), sub: `${s.observations_per_sec.toFixed(1)} / sec` },45    { label: "Events · 24h", value: s.events_24h, format: formatInt, sub: `${s.filings_24h} filings` },46    { label: "Median freshness", value: s.median_freshness_ms ?? 0, format: (n) => (s.median_freshness_ms == null ? "—" : formatDuration(n)), sub: "real-time quotes" },47  ];48  return (49    <div className={cx("grid grid-cols-2 gap-x-4 gap-y-4 rounded-md border border-rule bg-surface px-4 py-4 sm:grid-cols-3 lg:grid-cols-6", className)}>50      {items.map((it) => (51        <div key={it.label} className="min-w-0">52          <div className="text-[10.5px] font-medium uppercase tracking-wide text-ink-3">{it.label}</div>53          <div className="mono mt-0.5 text-xl font-semibold tracking-tight tnum sm:text-2xl">54            <Counter value={it.value} format={it.format} />55          </div>56          {it.sub && <div className="mt-0.5 truncate text-[11px] text-ink-3">{it.sub}</div>}57        </div>58      ))}59    </div>60  );61}62