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%
12.6 KB · 216 lines tsx
Raw Blame History
1import Link from "next/link";2import type { Metadata } from "next";3import { Chip, Empty, HealthPill, PageHeader, Panel, Stat, Table, Td } from "@/components/ui";4import { api } from "@/lib/api";5import { fmtBytes, fmtInt, fmtMs, fmtPct, relTime, utcDateTime, utcTime } from "@/lib/format";67export const dynamic = "force-dynamic";8export const metadata: Metadata = { title: "Connector health", description: "Live health of every connector family and sensor: success rate, latency, HTTP codes, noise ratios, queue depth and engine heartbeat." };910/** A heartbeat older than two minutes means the scheduler stopped publishing. */11function heartbeatStale(at: string | undefined): boolean {12  return Boolean(at) && Date.now() - new Date(at as string).getTime() > 120_000;13}1415export default async function HealthPage() {16  const h = await api.health();17  const byHealth = Object.fromEntries((h?.sensors_by_health ?? []).map((x) => [x.health, x.n]));18  const tp = h?.throughput;19  const eng = h?.engine ?? null;20  const engStale = heartbeatStale(eng?.at);21  const healthy = byHealth.UP ?? 0;22  const degraded = (byHealth.DEGRADED ?? 0) + (byHealth.RATE_LIMITED ?? 0);23  const failed = byHealth.ERROR ?? 0;24  return (25    <>26      <PageHeader compact kicker="Public observability" title="Connector health" description="Every connector exposes UP / DEGRADED / ERROR / RATE_LIMITED with 24 h success rate, latency, changes and events. Noisy sensors point to canonicalizer work, not more LLM calls." />2728      <div className="panel mb-4 grid grid-cols-2 divide-x divide-y divide-line sm:grid-cols-4 xl:grid-cols-8 xl:divide-y-0">29        <Stat label="Healthy" value={fmtInt(healthy)} tone="signal" hint="sensors UP" />30        <Stat label="Degraded" value={fmtInt(degraded)} tone={degraded ? "warn" : undefined} hint={`${fmtInt(byHealth.RATE_LIMITED ?? 0)} rate-limited`} />31        <Stat label="Failed" value={fmtInt(failed)} tone={failed ? "hot" : undefined} hint="consecutive errors" />32        <Stat label="Checks / min" value={tp ? tp.checks_per_min.toFixed(1) : "—"} hint={tp ? `${fmtInt(tp.checks_5m)} in 5 min` : undefined} />33        <Stat label="Events / min" value={tp ? tp.events_per_min.toFixed(2) : "—"} tone="signal" hint={tp ? `${fmtInt(tp.events_5m)} in 5 min · ${fmtInt(tp.changes_5m)} raw` : undefined} />34        <Stat label="304 ratio" value={fmtPct(tp?.not_modified_ratio ?? null)} hint="conditional GET hits · 5 min" />35        <Stat label="Queue depth" value={fmtInt(tp?.queue_due ?? 0)} tone={(tp?.queue_due ?? 0) > 500 ? "warn" : undefined} hint="sensors due now" />36        <Stat label="Avg latency" value={fmtMs(tp?.avg_latency_5m_ms)} hint={`${fmtInt(tp?.noise_filtered_24h ?? 0)} noise filtered · 24 h`} />37      </div>3839      <div className="mb-4 flex flex-wrap items-center gap-x-3 gap-y-1 rounded-md border border-line bg-panel px-3 py-2 font-mono text-[11.5px] text-fg-muted tabular">40        <span className="flex items-center gap-1.5">41          <span className={`inline-block size-2 rounded-full ${eng && !engStale ? "bg-signal animate-pulse-dot" : "bg-warn"}`} />42          <span className="label !text-fg-subtle">engine</span>43          <span className={eng && !engStale ? "text-signal" : "text-warn"}>{eng ? (engStale ? "STALE" : "LIVE") : "NO HEARTBEAT"}</span>44        </span>45        {eng ? (46          <>47            <span>in flight {eng.inflight ?? 0} / {eng.concurrency ?? "—"}</span>48            <span>due {fmtInt(eng.due)}</span>49            <span>busy hosts {eng.busyHosts?.length ?? 0}</span>50            <span className={eng.circuitOpen?.length ? "text-warn" : ""}>circuit open {eng.circuitOpen?.length ?? 0}</span>51            {eng.version && <span>v{eng.version}</span>}52            {eng.at && <span className="text-fg-subtle" title={utcDateTime(eng.at)}>heartbeat {utcTime(eng.at)} UTC · {relTime(eng.at)}</span>}53          </>54        ) : (55          <span className="text-fg-subtle">the scheduler publishes a heartbeat to Redis every few seconds; none is visible right now.</span>56        )}57        <span className="ml-auto text-fg-subtle">WS clients {fmtInt(h?.live.clients ?? 0)} · {fmtInt(h?.live.published ?? 0)} published since start</span>58      </div>5960      <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">61        {(h?.connectors ?? []).map((c) => (62          <Panel key={c.connector} title={<span className="flex items-center gap-2 normal-case tracking-normal"><span className="font-mono text-[13px] text-fg">{c.connector}</span><HealthPill health={c.status} /></span>}>63            <dl className="grid grid-cols-2 gap-y-1 text-[12.5px]">64              <dt className="text-fg-subtle">Success rate</dt><dd className="text-right font-mono tabular">{fmtPct(c.success_rate ?? null, 1)}</dd>65              <dt className="text-fg-subtle">Runs 24 h</dt><dd className="text-right font-mono tabular">{fmtInt(c.runs_24h)}</dd>66              <dt className="text-fg-subtle">Errors 24 h</dt><dd className="text-right font-mono tabular">{fmtInt(c.errors_24h)}</dd>67              <dt className="text-fg-subtle">Avg latency</dt><dd className="text-right font-mono tabular">{fmtMs(c.avg_latency_ms)}</dd>68              <dt className="text-fg-subtle">Changes / events 24 h</dt><dd className="text-right font-mono tabular">{fmtInt(c.changes_24h)} / {fmtInt(c.events_24h)}</dd>69              <dt className="text-fg-subtle">Last success</dt><dd className="text-right font-mono tabular">{c.last_success_at ? relTime(c.last_success_at) : "—"}</dd>70            </dl>71            <div className="mt-2 flex flex-wrap gap-1">72              {Object.entries(c.http_codes ?? {}).sort((a, b) => b[1] - a[1]).map(([code, n]) => (73                <Chip key={code} tone={code.startsWith("2") || code === "304" ? "ok" : code.startsWith("4") ? "warn" : code === "0" || code.startsWith("5") ? "danger" : "default"}>{code} · {n}</Chip>74              ))}75            </div>76            {c.last_error && <p className="mt-2 truncate font-mono text-[11px] text-danger" title={c.last_error}>{c.last_error}</p>}77          </Panel>78        ))}79        {!h?.connectors.length && <Panel dense><Empty>Health rollups appear after the first minute of engine activity.</Empty></Panel>}80      </div>8182      <div className="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">83        <Panel title="Sensors by connector" dense>84          {h?.sensors_by_connector?.length ? (85            <Table head={["Connector", "Sensors", "Up", "Up ratio", "Avg latency"]}>86              {h.sensors_by_connector.map((c) => (87                <tr key={c.connector}>88                  <Td mono>{c.connector}</Td>89                  <Td mono>{fmtInt(c.sensors)}</Td>90                  <Td mono>{fmtInt(c.up)}</Td>91                  <Td mono className={c.sensors && c.up / c.sensors < 0.9 ? "text-warn" : ""}>{fmtPct(c.sensors ? c.up / c.sensors : null)}</Td>92                  <Td mono>{fmtMs(c.avg_latency_ms)}</Td>93                </tr>94              ))}95            </Table>96          ) : (97            <Empty />98          )}99        </Panel>100        <Panel title="Sensors by status" dense>101          {h?.sensors_by_status?.length ? (102            <Table head={["Status", "Sensors"]}>103              {h.sensors_by_status.map((s) => (104                <tr key={s.status}>105                  <Td><HealthPill health={s.status} /></Td>106                  <Td mono>{fmtInt(s.n)}</Td>107                </tr>108              ))}109            </Table>110          ) : (111            <Empty />112          )}113          <p className="border-t border-line px-3 py-2 text-[11px] text-fg-subtle">PENDING → VALIDATED → ACTIVE is the sensor lifecycle; DISABLED sensors are kept for evidence but never scheduled.</p>114        </Panel>115      </div>116117      <div className="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2">118        <Panel title="Degraded sensors" dense>119          {h?.degraded_sensors.length ? (120            <Table head={["Sensor", "Source", "Health", "Errors", "HTTP", "Last error", "Last check"]}>121              {h.degraded_sensors.map((s) => (122                <tr key={s.id}>123                  <Td><Link href={`/sensor/${s.id}`} className="hover:underline">{s.name}</Link><div className="max-w-[18rem] truncate font-mono text-[10.5px] text-fg-subtle">{s.url}</div></Td>124                  <Td><Link href={`/source/${s.source_id}`} className="hover:underline">{s.source_id}</Link></Td>125                  <Td><HealthPill health={s.health} /></Td>126                  <Td mono>{s.consecutive_errors}</Td>127                  <Td mono>{s.last_status ?? "—"}</Td>128                  <Td className="max-w-[16rem] truncate text-danger"><span title={s.last_error ?? ""}>{s.last_error}</span></Td>129                  <Td mono className="whitespace-nowrap text-fg-subtle">{s.last_check_at ? relTime(s.last_check_at) : "—"}</Td>130                </tr>131              ))}132            </Table>133          ) : (134            <Empty>All sensors healthy.</Empty>135          )}136        </Panel>137        <Panel title="Noisy sensors (raw vs meaningful)" dense>138          {h?.noisy_sensors.length ? (139            <Table head={["Sensor", "Source", "Raw", "Meaningful", "Noise ratio"]}>140              {h.noisy_sensors.map((s) => (141                <tr key={s.id}>142                  <Td><Link href={`/sensor/${s.id}`} className="hover:underline">{s.name}</Link></Td>143                  <Td><Link href={`/source/${s.source_id}`} className="hover:underline">{s.source_id}</Link></Td>144                  <Td mono>{s.raw_changes}</Td>145                  <Td mono>{s.meaningful_changes}</Td>146                  <Td mono className={(s.noise_ratio ?? 0) > 0.9 ? "text-warn" : ""}>{s.noise_ratio !== null ? fmtPct(Number(s.noise_ratio), 1) : "—"}</Td>147                </tr>148              ))}149            </Table>150          ) : (151            <Empty>Noise ratios appear once sensors have ≥ 5 raw changes.</Empty>152          )}153        </Panel>154      </div>155156      <div className="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2">157        <Panel title="Top failing domains" dense>158          {h?.top_failing_domains?.length ? (159            <Table head={["Host", "Failures", "Last error"]}>160              {h.top_failing_domains.map((f) => (161                <tr key={f.host}>162                  <Td mono><Link href={`/domain/${f.host}`} className="hover:underline">{f.host}</Link></Td>163                  <Td mono className="text-danger">{fmtInt(f.failures)}</Td>164                  <Td className="max-w-[20rem] truncate font-mono text-[11px] text-fg-muted"><span title={f.last_error ?? ""}>{f.last_error ?? "—"}</span></Td>165                </tr>166              ))}167            </Table>168          ) : (169            <Empty>No failing domains right now.</Empty>170          )}171        </Panel>172        <Panel title="Slowest sensors" dense>173          {h?.slowest_sensors?.length ? (174            <Table head={["Sensor", "Source", "Connector", "Avg latency"]}>175              {h.slowest_sensors.map((s) => (176                <tr key={s.id}>177                  <Td><Link href={`/sensor/${s.id}`} className="hover:underline">{s.name}</Link></Td>178                  <Td><Link href={`/source/${s.source_id}`} className="hover:underline">{s.source_id}</Link></Td>179                  <Td mono>{s.connector}</Td>180                  <Td mono className={s.avg_latency_ms > 10_000 ? "text-warn" : ""}>{fmtMs(s.avg_latency_ms)}</Td>181                </tr>182              ))}183            </Table>184          ) : (185            <Empty />186          )}187        </Panel>188      </div>189190      <Panel title="Daily metrics" dense className="mt-4">191        {h?.daily.length ? (192          <Table head={["Day", "Checks", "304", "Bytes", "Raw changes", "Events", "Silent", "Errors", "LLM calls", "LLM tokens in/out"]}>193            {h.daily.map((d) => (194              <tr key={String(d.day)}>195                <Td mono>{String(d.day)}</Td>196                <Td mono>{fmtInt(Number(d.checks))}</Td>197                <Td mono>{fmtInt(Number(d.not_modified))}</Td>198                <Td mono>{fmtBytes(Number(d.bytes))}</Td>199                <Td mono>{fmtInt(Number(d.raw_changes))}</Td>200                <Td mono>{fmtInt(Number(d.events))}</Td>201                <Td mono>{fmtInt(Number(d.silent_events))}</Td>202                <Td mono>{fmtInt(Number(d.errors))}</Td>203                <Td mono>{fmtInt(Number(d.llm_calls))}</Td>204                <Td mono>{fmtInt(Number(d.llm_input_tokens))} / {fmtInt(Number(d.llm_output_tokens))}</Td>205              </tr>206            ))}207          </Table>208        ) : (209          <Empty />210        )}211      </Panel>212      <p className="mt-3 text-[11px] text-fg-subtle">Rollups refresh every minute from sensor runs; raw counters are in <code>/api/v1/health/connectors</code> and <code>/api/metrics</code> (Prometheus). Operators: <Link href="/ops" className="hover:text-fg">/ops</Link>.</p>213    </>214  );215}216