/** * The five conditions `GET /api/health` reports, as facts. * * A pure projection of `Health` so the whole matrix — every state of every * condition, and every way out a degraded one offers — is testable without a * router or a fetch. `HealthStrip` only paints what this returns. * * A healthy fact is quiet: no badge, no panel, no green reassurance, so the one * condition that is not healthy is the thing the eye lands on. */ import { formatBytes, formatClock } from "@/lib/format"; import type { Health } from "@/lib/types"; export type FactTone = "ok" | "notice" | "warn" | "danger"; /** * Where a degraded fact sends the reader. A `filter` link stays on this page and * narrows it to the component that failed; a `route` link leaves for the surface * that can fix the condition. */ export type FactLink = | { kind: "route"; to: "/configuration/protection" | "/configuration/resolution"; label: string } | { kind: "filter"; component: string; label: string }; export interface HealthFact { key: "protection" | "upstreams" | "query_history" | "diagnostics" | "disk"; label: string; tone: FactTone; /** The state, in the reader's words. Never colour alone. */ value: string; detail?: string; link?: FactLink; } const numberFormat = new Intl.NumberFormat(); /** Kept out of the state rule: rows are lost whether or not the box is losing them now. */ function dropText(dropped: number, lastDrop: number | null, locale?: string, timeZone?: string): string | undefined { if (dropped <= 0) return undefined; const count = `${numberFormat.format(dropped)} ${dropped === 1 ? "query" : "queries"} dropped`; return lastDrop === null ? count : `${count}, last at ${formatClock(lastDrop, locale, timeZone)}`; } function protectionFact(protection: Health["protection"], locale?: string, timeZone?: string): HealthFact { if (protection.state === "unavailable") { return { key: "protection", label: "Protection", tone: "danger", value: "Unavailable", detail: "No filter snapshot is published, so queries are not being filtered.", link: { kind: "route", to: "/configuration/protection", label: "Blocklist sources" }, }; } if (protection.state === "paused") { return { key: "protection", label: "Protection", tone: "notice", value: protection.until === null ? "Paused" : `Paused until ${formatClock(protection.until, locale, timeZone)}`, }; } return { key: "protection", label: "Protection", tone: "ok", value: "Active" }; } function queryHistoryFact(history: Health["query_history"], locale?: string, timeZone?: string): HealthFact { const detail = dropText(history.dropped_total, history.last_drop_s, locale, timeZone); if (history.state === "failed") { return { key: "query_history", label: "Query history", tone: "danger", value: "Writer failed", detail, link: { kind: "filter", component: "query_log", label: "Query log diagnostics" }, }; } if (history.state === "losing") { // The disk gate is what is holding the writes back, and it may be the only // thing that has reported: query_log itself need not have an episode open. return { key: "query_history", label: "Query history", tone: "danger", value: "Losing rows", detail, link: { kind: "filter", component: "disk", label: "Disk diagnostics" }, }; } return { key: "query_history", label: "Query history", tone: "ok", value: "Recording", detail }; } export function healthFacts(health: Health, locale?: string, timeZone?: string): HealthFact[] { const { upstreams, diagnostics, disk } = health; return [ protectionFact(health.protection, locale, timeZone), { key: "upstreams", label: "Upstreams", tone: upstreams.state === "unavailable" ? "danger" : "ok", value: upstreams.state === "unavailable" ? "None reachable" : "Available", detail: `${upstreams.available} of ${upstreams.total} enabled`, ...(upstreams.state === "unavailable" ? { link: { kind: "route", to: "/configuration/resolution", label: "Upstreams" } as FactLink } : {}), }, queryHistoryFact(health.query_history, locale, timeZone), diagnostics.state === "unavailable" ? { key: "diagnostics", label: "Diagnostics", tone: "danger", value: "Unavailable", // No link: the page it would filter is the thing that is broken. detail: "Diagnostics are not being recorded. Check free disk space and the configuration database's permissions.", } : { key: "diagnostics", label: "Diagnostics", tone: "ok", value: "Recording" }, { key: "disk", label: "Storage", tone: disk.state === "critical" ? "danger" : disk.state === "low" ? "warn" : "ok", value: disk.state === "critical" ? "Critical" : disk.state === "low" ? "Low" : "OK", detail: `${formatBytes(disk.free_bytes)} free`, ...(disk.state === "ok" ? {} : { link: { kind: "filter", component: "disk", label: "Disk diagnostics" } as FactLink }), }, ]; }