src/health.ts
105 lines and 6 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| backupFreshness | 11 to 38 | 6 | 100% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
| probeError | 40 to 42 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| probeBindings | 46 to 66 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
| healthReport | 76 to 111 | 12 | 94% | WatchCRAP 12 | No data: mutation testing has not run on this repo yet |
| <anonymous:106> | 106 to 106 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| handleHealth | 113 to 116 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
Imports
It imports src/doc-index.ts, src/env.ts, src/ops-types/sites.ts, src/store-probe.ts, src/time.ts. Imported by src/backup.ts, src/ops-snapshot.ts, src/routes.ts, src/site-check.ts, src/watcher-detect.ts, src/watcher-gather.ts, src/watcher.ts.
Source
import type { Env } from "./env";
import type { HealthSnapshot } from "./ops-types/sites";
import { probeFts } from "./store-probe";
import { aiSearchUsage } from "./doc-index";
import { HOUR_MS } from "./time";
export const BACKUP_LAST_OK_KEY = "backup:last-ok";
// Daily cron is 24h; 26h is that plus a 2h grace so one slow run does not warn.
export const BACKUP_STALE_HOURS = 26;
async function backupFreshness(
env: Env
): Promise<{ last_ok: string | null; age_hours: number | null; warning?: string }> {
let lastOk: string | null = null;
try {
lastOk = await env.APP_KV.get(BACKUP_LAST_OK_KEY);
} catch (err) {
return {
last_ok: null,
age_hours: null,
warning: `backup freshness unreadable: ${(err instanceof Error ? err.message : String(err)).slice(0, 80)}`,
};
}
if (!lastOk) return { last_ok: null, age_hours: null, warning: "no successful backup recorded" };
const stamped = Date.parse(lastOk);
// NaN compares false against the threshold, so without this an unreadable stamp
// would pass as fresh with no warning.
if (Number.isNaN(stamped)) {
return { last_ok: lastOk, age_hours: null, warning: `${BACKUP_LAST_OK_KEY} is not a parseable time: ${lastOk.slice(0, 40)}` };
}
const ageMs = Date.now() - stamped;
const ageHours = Math.round((ageMs / HOUR_MS) * 10) / 10;
const result: { last_ok: string; age_hours: number; warning?: string } = { last_ok: lastOk, age_hours: ageHours };
if (ageMs > BACKUP_STALE_HOURS * HOUR_MS) {
result.warning = `last successful backup was ${ageHours}h ago, over the ${BACKUP_STALE_HOURS}h threshold`;
}
return result;
}
function probeError(err: unknown): string {
return `error: ${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`;
}
// The two bindings beside the store. Each probe reads a key that need not exist: a
// null answer proves the binding resolves. Reported as fields, left out of status.
async function probeBindings(env: Env): Promise<{ media: string; app_kv: string }> {
let media = "unbound";
if (env.MEDIA) {
try {
await env.MEDIA.head("health-probe");
media = "ok";
} catch (err) {
media = probeError(err);
}
}
let app_kv = "unbound";
if (env.APP_KV) {
try {
await env.APP_KV.get("health-probe");
app_kv = "ok";
} catch (err) {
app_kv = probeError(err);
}
}
return { media, app_kv };
}
export interface HealthReport extends HealthSnapshot {
// Capsid's own hybrid searches this month against its cap (design-hybrid-search 3.5).
// Informational; null when KV cannot be read, absent from snapshots taken before it.
ai_search?: { month: string; hybrid: number; cap: number } | null;
}
// /health serializes this and the watcher's pass records it for Capsid Portal, so the
// Portal shows the same values the live gate asserts.
export async function healthReport(env: Env): Promise<HealthReport> {
const provenance = {
sha: env.BUILD_SHA ?? "unknown",
dirty: env.BUILD_DIRTY === "true",
builtAt: env.BUILT_AT ?? null,
};
let d1 = "unbound";
let fts = "skipped";
try {
const one = await env.DB.prepare("SELECT 1 AS ok").first<{ ok: number }>();
d1 = one?.ok === 1 ? "ok" : `unexpected: ${JSON.stringify(one)}`;
} catch (err) {
d1 = `error: ${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`;
}
if (d1 === "ok") fts = await probeFts(env.DB);
// Informational. A store without d1_migrations is null, not a health failure.
let schema_version: string | null = null;
if (d1 === "ok") {
try {
const row = await env.DB.prepare("SELECT name FROM d1_migrations ORDER BY id DESC LIMIT 1").first<{ name: string }>();
schema_version = row?.name ?? null;
} catch {
schema_version = null;
}
}
const backup = await backupFreshness(env);
const bindings = await probeBindings(env);
const ai_search = await aiSearchUsage(env).catch(() => null);
// Status is the store only: the live gate polls it after a deploy, and a 503 fails it.
const healthy = d1 === "ok" && fts === "ok";
return { status: healthy ? "ok" : "degraded", ...provenance, schema_version, store: { d1, fts }, bindings, backup, ai_search };
}
export async function handleHealth(env: Env): Promise<Response> {
const report = await healthReport(env);
return Response.json(report, { status: report.status === "ok" ? 200 : 503 });
}