TypeScript 55.4%
Python 43.2%
SQL 1.2%
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