import Link from "next/link";
import type { Metadata } from "next";
import { Chip, Empty, HealthPill, PageHeader, Panel, Stat, Table, Td } from "@/components/ui";
import { api } from "@/lib/api";
import { fmtBytes, fmtInt, fmtMs, fmtPct, relTime, utcDateTime, utcTime } from "@/lib/format";
export const dynamic = "force-dynamic";
export 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." };
/** A heartbeat older than two minutes means the scheduler stopped publishing. */
function heartbeatStale(at: string | undefined): boolean {
return Boolean(at) && Date.now() - new Date(at as string).getTime() > 120_000;
}
export default async function HealthPage() {
const h = await api.health();
const byHealth = Object.fromEntries((h?.sensors_by_health ?? []).map((x) => [x.health, x.n]));
const tp = h?.throughput;
const eng = h?.engine ?? null;
const engStale = heartbeatStale(eng?.at);
const healthy = byHealth.UP ?? 0;
const degraded = (byHealth.DEGRADED ?? 0) + (byHealth.RATE_LIMITED ?? 0);
const failed = byHealth.ERROR ?? 0;
return (
<>
500 ? "warn" : undefined} hint="sensors due now" />
engine
{eng ? (engStale ? "STALE" : "LIVE") : "NO HEARTBEAT"}
{eng ? (
<>
in flight {eng.inflight ?? 0} / {eng.concurrency ?? "—"}
due {fmtInt(eng.due)}
busy hosts {eng.busyHosts?.length ?? 0}
circuit open {eng.circuitOpen?.length ?? 0}
{eng.version && v{eng.version}}
{eng.at && heartbeat {utcTime(eng.at)} UTC · {relTime(eng.at)}}
>
) : (
the scheduler publishes a heartbeat to Redis every few seconds; none is visible right now.
)}
WS clients {fmtInt(h?.live.clients ?? 0)} · {fmtInt(h?.live.published ?? 0)} published since start
{(h?.connectors ?? []).map((c) => (
{c.connector}}>
- Success rate
- {fmtPct(c.success_rate ?? null, 1)}
- Runs 24 h
- {fmtInt(c.runs_24h)}
- Errors 24 h
- {fmtInt(c.errors_24h)}
- Avg latency
- {fmtMs(c.avg_latency_ms)}
- Changes / events 24 h
- {fmtInt(c.changes_24h)} / {fmtInt(c.events_24h)}
- Last success
- {c.last_success_at ? relTime(c.last_success_at) : "—"}
{Object.entries(c.http_codes ?? {}).sort((a, b) => b[1] - a[1]).map(([code, n]) => (
{code} · {n}
))}
{c.last_error && {c.last_error}
}
))}
{!h?.connectors.length &&
Health rollups appear after the first minute of engine activity.}
{h?.sensors_by_connector?.length ? (
{h.sensors_by_connector.map((c) => (
| {c.connector} |
{fmtInt(c.sensors)} |
{fmtInt(c.up)} |
{fmtPct(c.sensors ? c.up / c.sensors : null)} |
{fmtMs(c.avg_latency_ms)} |
))}
) : (
)}
{h?.sensors_by_status?.length ? (
{h.sensors_by_status.map((s) => (
|
{fmtInt(s.n)} |
))}
) : (
)}
PENDING → VALIDATED → ACTIVE is the sensor lifecycle; DISABLED sensors are kept for evidence but never scheduled.
{h?.degraded_sensors.length ? (
{h.degraded_sensors.map((s) => (
| {s.name} {s.url} |
{s.source_id} |
|
{s.consecutive_errors} |
{s.last_status ?? "—"} |
{s.last_error} |
{s.last_check_at ? relTime(s.last_check_at) : "—"} |
))}
) : (
All sensors healthy.
)}
{h?.noisy_sensors.length ? (
{h.noisy_sensors.map((s) => (
| {s.name} |
{s.source_id} |
{s.raw_changes} |
{s.meaningful_changes} |
0.9 ? "text-warn" : ""}>{s.noise_ratio !== null ? fmtPct(Number(s.noise_ratio), 1) : "—"} |
))}
) : (
Noise ratios appear once sensors have ≥ 5 raw changes.
)}
{h?.top_failing_domains?.length ? (
{h.top_failing_domains.map((f) => (
| {f.host} |
{fmtInt(f.failures)} |
{f.last_error ?? "—"} |
))}
) : (
No failing domains right now.
)}
{h?.slowest_sensors?.length ? (
{h.slowest_sensors.map((s) => (
| {s.name} |
{s.source_id} |
{s.connector} |
10_000 ? "text-warn" : ""}>{fmtMs(s.avg_latency_ms)} |
))}
) : (
)}
{h?.daily.length ? (
{h.daily.map((d) => (
| {String(d.day)} |
{fmtInt(Number(d.checks))} |
{fmtInt(Number(d.not_modified))} |
{fmtBytes(Number(d.bytes))} |
{fmtInt(Number(d.raw_changes))} |
{fmtInt(Number(d.events))} |
{fmtInt(Number(d.silent_events))} |
{fmtInt(Number(d.errors))} |
{fmtInt(Number(d.llm_calls))} |
{fmtInt(Number(d.llm_input_tokens))} / {fmtInt(Number(d.llm_output_tokens))} |
))}
) : (
)}
Rollups refresh every minute from sensor runs; raw counters are in /api/v1/health/connectors and /api/metrics (Prometheus). Operators: /ops.
>
);
}