Skip to the content

Capsid menu

src/health.ts

105 lines and 6 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.

Functions

The functions of health.ts
FunctionLinesComplexityCoveredCRAPMutation
backupFreshness11 to 386100%WatchCRAP 6No data: mutation testing has not run on this repo yet
probeError40 to 422100%OkCRAP 2No data: mutation testing has not run on this repo yet
probeBindings46 to 665100%OkCRAP 5No data: mutation testing has not run on this repo yet
healthReport76 to 1111294%WatchCRAP 12No data: mutation testing has not run on this repo yet
<anonymous:106>106 to 1061100%OkCRAP 1No data: mutation testing has not run on this repo yet
handleHealth113 to 1162100%OkCRAP 2No 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 });
}