spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
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