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%
6.4 KB · 112 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { RelativeTime } from "@/components/ui/freshness";4import { Empty, Page, PageHeader, Section, Stat } from "@/components/ui/section";5import { Sparkline } from "@/components/ui/sparkline";6import { StatusBadge } from "@/components/ui/status-badge";7import { api } from "@/lib/api";8import { formatDateTime, formatDuration } from "@/lib/format";9import type { DataHealth } from "@/lib/types";1011export const metadata: Metadata = { title: "Data health", description: "Live health of Market Atlas' observation network: connector states, freshness, multi-source coverage, confidence and incidents.", alternates: { canonical: "/data-health" } };12export const dynamic = "force-dynamic";1314export default async function DataHealthPage() {15  const h = await api<DataHealth>("/v1/data-health");16  const c = h.connectors;17  return (18    <Page wide>19      <PageHeader kicker="Transparency" title="Data health" lead="How well Market Atlas is observing the market right now. These are operational metrics about our own network — freshness, redundancy, agreement, incidents — not statements about market quality." actions={<Link href="/status" className="text-sm text-accent hover:underline">Public status →</Link>} />20      <div className="grid grid-cols-2 gap-4 rounded-md border border-rule bg-surface px-4 py-4 sm:grid-cols-3 lg:grid-cols-6">21        <Stat label="Connectors healthy" value={h.healthy_ratio == null ? "—" : `${Math.round(h.healthy_ratio * 100)}%`} sub={`${c.healthy} / ${c.total}`} tone={h.healthy_ratio != null && h.healthy_ratio < 0.8 ? "warn" : "pos"} />22        <Stat label="Median freshness" value={formatDuration(h.median_freshness_ms)} sub={`p95 ${formatDuration(h.p95_freshness_ms)} · live quotes`} />23        <Stat label="Public quotes" value={h.quotes_public} sub={`${h.quotes_realtime} real time`} />24        <Stat label="Multi-source" value={h.quotes_public ? `${Math.round((h.multi_source_quotes / h.quotes_public) * 100)}%` : "—"} sub={`${h.multi_source_quotes} quotes with ≥ 2 independent sources`} />25        <Stat label="Mean confidence" value={h.mean_confidence == null ? "—" : `${Math.round(h.mean_confidence * 100)}%`} sub={`dispersion ${h.mean_dispersion_bps ?? "—"} bps`} />26        <Stat label="Observations / s" value={h.observations_per_sec.toFixed(1)} sub={`queue ${h.queue_depth}`} />27      </div>28      <div className="mt-3 flex flex-wrap gap-3 text-xs text-ink-2">29        <span className="text-positive">{c.healthy} healthy</span>30        <span className="text-warning">{c.degraded} degraded / stale</span>31        <span className="text-accent">{c.recovering} recovering</span>32        <span className="text-negative">{c.failed} failed</span>33        <span className="text-stale">{c.paused} paused</span>34      </div>35      {h.history.length > 1 && (36        <Section title="Last 48 hours" hint="hourly averages from connector health snapshots">37          <div className="grid grid-cols-1 [&>*]:min-w-0 gap-4 rounded-md border border-rule bg-surface p-4 sm:grid-cols-3">38            <Trend label="Healthy ratio" values={h.history.map((x) => x.healthy_ratio)} fmt={(v) => `${Math.round(v * 100)}%`} />39            <Trend label="Messages / hour" values={h.history.map((x) => x.messages)} fmt={(v) => Math.round(v).toLocaleString("en-US")} />40            <Trend label="Median latency" values={h.history.map((x) => x.latency_ms ?? 0)} fmt={(v) => formatDuration(v)} />41          </div>42        </Section>43      )}44      <Section title="Connectors" href="/connectors">45        <div className="overflow-x-auto rounded-md border border-rule bg-surface">46          <table className="table-dense">47            <thead>48              <tr>49                <th>Connector</th>50                <th>State</th>51                <th className="text-right">Msg / min</th>52                <th className="text-right">Median latency</th>53                <th className="text-right">Instruments</th>54                <th className="text-right">Reliability</th>55                <th>Last message</th>56                <th>Last error</th>57              </tr>58            </thead>59            <tbody>60              {h.by_connector.map((r) => (61                <tr key={r.id}>62                  <td className="mono">{r.id}</td>63                  <td>64                    <StatusBadge status={r.state} />65                  </td>66                  <td className="num">{r.messages_1m}</td>67                  <td className="num">{r.median_latency_ms == null ? "—" : formatDuration(r.median_latency_ms)}</td>68                  <td className="num">{r.instruments}</td>69                  <td className="num">{r.reliability_score ?? "—"}</td>70                  <td className="text-xs text-ink-3">{r.last_message_at ? <RelativeTime value={r.last_message_at} /> : "—"}</td>71                  <td className="max-w-[320px] truncate text-xs text-ink-3" title={r.last_error ?? undefined}>72                    {r.last_error ?? "—"}73                  </td>74                </tr>75              ))}76            </tbody>77          </table>78        </div>79      </Section>80      <Section title="Incidents · 7 days" hint="source failures, recoveries, schema drift, divergence">81        {h.incidents.length ? (82          <ul className="divide-y divide-rule rounded-md border border-rule bg-surface px-3">83            {h.incidents.map((i) => (84              <li key={i.id} className="flex flex-wrap items-center gap-x-3 gap-y-1 py-2 text-sm">85                <span className="mono text-xs text-ink-3">{formatDateTime(i.ts, { seconds: true })}</span>86                <StatusBadge status={i.severity} />87                <Link href={`/events/${i.id}`} className="hover:underline">88                  {i.title}89                </Link>90                <span className="mono text-[11px] text-ink-3">{i.type}</span>91              </li>92            ))}93          </ul>94        ) : (95          <Empty>No source incidents in the last 7 days.</Empty>96        )}97      </Section>98    </Page>99  );100}101102function Trend({ label, values, fmt }: { label: string; values: number[]; fmt: (v: number) => string }) {103  const last = values[values.length - 1] ?? 0;104  return (105    <div>106      <div className="text-[11px] font-medium uppercase tracking-wide text-ink-3">{label}</div>107      <div className="mono mt-0.5 text-xl font-semibold tnum">{fmt(last)}</div>108      <Sparkline values={values} width={220} height={40} stroke="var(--accent)" className="mt-1 w-full" />109    </div>110  );111}112