milestone 30: overview as a dashboard, explicit health contract, period aggregations
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s

This commit is contained in:
2026-08-22 16:45:15 +02:00
parent 17422fac21
commit 648d9b4496
89 changed files with 7222 additions and 4239 deletions
@@ -5,6 +5,7 @@ import { AuthProvider } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import { DIAGNOSTIC_CODES, type DiagnosticEvent } from "@/lib/types";
import { health } from "@/lib/healthFixture";
import { EVENT_COPY } from "./eventCopy";
const NOW_S = Math.floor(Date.now() / 1000);
@@ -35,6 +36,8 @@ beforeEach(() => {
requested = [];
responses = {
"/api/version": { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 },
// The shell reads health for the Diagnostics nav badge on every route.
"/api/health": health(),
};
vi.stubGlobal(
"fetch",
@@ -4,6 +4,7 @@ import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import { health } from "@/lib/healthFixture";
import type { DiagnosticEvent, DiagnosticsPage } from "@/lib/types";
// Ages are rendered against the wall clock, so the fixtures are anchored to it
@@ -68,6 +69,9 @@ beforeEach(() => {
requested = [];
responses = {
"/api/version": { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 },
// The health strip at the top of the page; quiet on a healthy box, which is
// what every test below wants it to be.
"/api/health": health(),
"/api/diagnostics?state=active": ACTIVE,
"/api/diagnostics?state=resolved": RESOLVED,
};
@@ -256,7 +260,7 @@ test("only the resolved history offers a purge", async () => {
// An episode still failing is the state of the box, not history: no purge
// affordance anywhere on its card.
const active = screen.getByText("Blocklist source failed to update").closest("li")!;
const active = (await screen.findByText("Blocklist source failed to update")).closest("li")!;
expect(within(active).queryByRole("button", { name: "Purge" })).toBeNull();
const row = screen.getByText("Disk space low").closest("tr")!;
@@ -17,6 +17,7 @@ import ConfirmDialog from "@/ui/ConfirmDialog";
import Select from "@/ui/Select";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
import HealthStrip from "./HealthStrip";
import SeverityBadge from "./SeverityBadge";
import { diagnosticsFilterOf } from "./filter";
import { DIAGNOSTIC_COMPONENTS, componentLabel, copyFor } from "./eventCopy";
@@ -401,6 +402,8 @@ export default function DiagnosticsPage() {
repeats, and closes when the subject recovers.
</p>
<HealthStrip />
<RangeNotice since={search.since} until={search.until} />
<div {...stylex.props(styles.filterGrid)}>
@@ -0,0 +1,195 @@
/**
* The health strip on the Diagnostics page, through the real router.
*
* Its load contract is migrated whole from the deleted Overview status section:
* a visible loading state before the first reading, an error row with Retry when
* the first read fails, and a refetch failure that marks the conditions on
* screen as the last reading rather than the current state. What is new is that
* a condition explained on this page narrows this page instead of navigating.
*/
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import { health } from "@/lib/healthFixture";
import type { Health } from "@/lib/types";
let healthBody: Health;
let healthFails: boolean;
let requested: string[];
/** Held open to keep a health request in flight while a test looks at the strip. */
let pendingHealth: Promise<void> | null;
function json(payload: unknown, status = 200): Response {
return new Response(JSON.stringify(payload), { status, headers: { "content-type": "application/json" } });
}
beforeEach(() => {
healthBody = health();
healthFails = false;
requested = [];
pendingHealth = null;
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
requested.push(url);
if (url === "/api/health") {
if (pendingHealth !== null) await pendingHealth;
return healthFails ? json({ error: "health unavailable" }, 400) : json(healthBody);
}
if (url === "/api/version")
return json({ version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 });
if (url.startsWith("/api/diagnostics"))
return json({ events: [], next_before: null, active: { warnings: 0, errors: 0 } });
return json({ error: "not stubbed" }, 404);
}),
);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.useRealTimers();
});
function renderDiagnostics(path = "/diagnostics") {
const queryClient = createQueryClient();
const defaults = queryClient.getDefaultOptions();
queryClient.setDefaultOptions({ ...defaults, queries: { ...defaults.queries, retry: false } });
const router = createAppRouter(createMemoryHistory({ initialEntries: [path] }), queryClient);
render(
<AuthProvider>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</AuthProvider>,
);
return { router, queryClient };
}
function strip(): HTMLElement {
return screen.getByRole("list", { name: "Current status" });
}
function fact(label: string): HTMLElement {
// First match, not only match: a condition's label and the link it offers can
// be the same word — Upstreams links to Upstreams — and the label comes first.
const cell = within(strip()).getAllByText(label)[0];
const item = cell.closest("li");
if (item === null) throw new Error(`no health fact for ${label}`);
return item;
}
test("the five conditions are stated in words, healthy ones without a way out", async () => {
renderDiagnostics();
await waitFor(() => expect(strip()).toBeTruthy());
for (const [label, value] of [
["Protection", "Active"],
["Upstreams", "Available"],
["Query history", "Recording"],
["Diagnostics", "Recording"],
["Storage", "OK"],
] as const) {
expect(within(fact(label)).getByText(value)).toBeTruthy();
}
expect(within(strip()).queryByRole("link")).toBeNull();
});
test("protection unavailable sends the reader to Blocklists, upstreams to Upstreams", async () => {
healthBody = health({
protection: { state: "unavailable", until: null },
upstreams: { state: "unavailable", available: 0, total: 2 },
});
renderDiagnostics();
await waitFor(() => expect(strip()).toBeTruthy());
expect(within(fact("Protection")).getByRole("link", { name: "Blocklists" }).getAttribute("href")).toBe(
"/blocklists",
);
expect(within(fact("Upstreams")).getByRole("link", { name: "Upstreams" }).getAttribute("href")).toBe("/upstreams");
});
test("a losing query log narrows this page to the disk, a failed writer to the query log", async () => {
healthBody = health({ query_history: { state: "losing", dropped_total: 4, last_drop_s: null } });
const { router } = renderDiagnostics();
await waitFor(() => expect(strip()).toBeTruthy());
expect(within(fact("Query history")).getByText("4 queries dropped")).toBeTruthy();
fireEvent.click(within(fact("Query history")).getByRole("link", { name: "Disk diagnostics" }));
await waitFor(() => expect(router.state.location.search).toEqual({ component: "disk" }));
await waitFor(() => expect(requested.some((url) => url.includes("component=disk"))).toBe(true));
});
test("a filter link drops a time window that would hide the episodes it points at", async () => {
healthBody = health({ disk: { state: "critical", free_bytes: 0 } });
const { router } = renderDiagnostics("/diagnostics?since=1000&until=2000&severity=error&state=resolved");
await waitFor(() => expect(strip()).toBeTruthy());
fireEvent.click(within(fact("Storage")).getByRole("link", { name: "Disk diagnostics" }));
// Everything that could hide the episode goes with the bounds: `state=resolved`
// would exclude the active disk episode this link exists to show, and an
// `error` severity would exclude it whenever it is a warning.
await waitFor(() => expect(router.state.location.search).toEqual({ component: "disk" }));
});
test("an unavailable diagnostics store explains itself and offers no link into itself", async () => {
healthBody = health({ diagnostics: { state: "unavailable", active_warnings: 0, active_errors: 0 } });
renderDiagnostics();
await waitFor(() => expect(strip()).toBeTruthy());
const row = fact("Diagnostics");
expect(within(row).getByText(/not being recorded/)).toBeTruthy();
expect(within(row).queryByRole("link")).toBeNull();
});
test("the strip says it is loading before the first reading, never empty conditions", async () => {
// Through the route, which is the path that matters: the loader starts the
// health request without waiting for it, so the page paints while the reading
// is still in flight and the strip has to say so.
let release = () => {};
pendingHealth = new Promise<void>((resolve) => {
release = resolve;
});
renderDiagnostics();
expect(await screen.findByText("Loading status…")).toBeTruthy();
expect(screen.queryByText("Protection")).toBeNull();
release();
await waitFor(() => expect(strip()).toBeTruthy());
expect(screen.queryByText("Loading status…")).toBeNull();
});
test("a failed first health read is an error row with Retry, not a healthy strip", async () => {
healthFails = true;
renderDiagnostics();
await screen.findByText("health unavailable");
expect(screen.queryByRole("list", { name: "Current status" })).toBeNull();
});
test("a reading that has gone stale says so rather than passing for current", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
renderDiagnostics();
await vi.waitFor(() => expect(strip()).toBeTruthy());
expect(within(fact("Storage")).getByText("OK")).toBeTruthy();
// The next poll fails. The conditions on screen are the last that arrived and
// must not keep passing for the current state.
healthFails = true;
await vi.advanceTimersByTimeAsync(11_000);
await vi.waitFor(() => expect(screen.getByText(/last reading that arrived/)).toBeTruthy());
expect(within(fact("Storage")).getByText("OK")).toBeTruthy();
// Recovery clears the caption rather than leaving the page permanently unsure.
healthFails = false;
await vi.advanceTimersByTimeAsync(11_000);
await vi.waitFor(() => expect(screen.queryByText(/last reading that arrived/)).toBeNull());
});
@@ -0,0 +1,178 @@
/**
* The five health conditions, compactly, at the top of the page that explains
* failures. Healthy conditions stay quiet; a degraded one is highlighted and
* offers the way out.
*
* A degraded condition whose explanation is on this page narrows this page
* rather than navigating away: the filter link sets `component` and drops every
* other filter. A time window, a severity or a `state=resolved` left from an
* earlier investigation would each hide the very episode the reader was sent to
* read, and a link that lands on "no events" states something false.
*
* The load contract is the one the deleted Overview status section carried: a
* visible loading state before the first reading, and a refetch failure that
* says so — the conditions on screen become the last reading that arrived, never
* a claim about the current state, until a poll succeeds again.
*/
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import * as stylex from "@stylexjs/stylex";
import InlineError from "@/lib/InlineError";
import { healthQuery } from "@/lib/queries";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
import { healthFacts, type FactLink, type FactTone, type HealthFact } from "./healthFacts";
const DARK = "@media (prefers-color-scheme: dark)";
const styles = stylex.create({
list: {
marginTop: "0.75rem",
display: "grid",
gap: "0.5rem",
// Five is prime, so every count between one and five leaves a short last
// row; three columns made it 3 then 2, which reads as a layout that ran out
// of room rather than one that chose. So the strip goes from one column
// straight to two and then to a single row of five, and is never ragged.
gridTemplateColumns: {
default: "minmax(0, 1fr)",
"@media (min-width: 640px)": "repeat(2, minmax(0, 1fr))",
"@media (min-width: 1100px)": "repeat(5, minmax(0, 1fr))",
},
listStyleType: "none",
padding: 0,
},
fact: {
display: "flex",
flexWrap: "wrap",
alignItems: "baseline",
gap: "0.375rem",
borderRadius: "0.25rem",
borderWidth: 1,
borderStyle: "solid",
paddingInline: "0.625rem",
paddingBlock: "0.5rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
/** Quiet: a healthy condition is the normal state and gets no emphasis. */
quiet: {
borderColor: colors.border,
backgroundColor: "transparent",
},
highlighted: {
borderColor: colors.borderStrong,
backgroundColor: colors.surfaceRaised,
},
label: {
color: colors.textSecondary,
},
value: {
fontWeight: 500,
},
detail: {
flexBasis: "100%",
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textMuted,
},
link: {
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.primaryOnSurface,
textDecorationLine: "none",
},
ok: {
color: { default: "oklch(43.2% 0.095 166.913)", [DARK]: "oklch(84.5% 0.143 164.978)" },
},
notice: {
color: { default: "oklch(47.3% 0.137 46.201)", [DARK]: "oklch(87.9% 0.169 91.605)" },
},
warn: {
color: { default: "oklch(47.3% 0.137 46.201)", [DARK]: "oklch(87.9% 0.169 91.605)" },
},
danger: {
color: colors.dangerText,
},
message: {
marginTop: "0.5rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
});
const TONES = { ok: styles.ok, notice: styles.notice, warn: styles.warn, danger: styles.danger } as const;
/** Text and icon carry the state; the colour only agrees with them. */
const ICONS: Record<FactTone, string> = { ok: "●", notice: "‖", warn: "!", danger: "✕" };
function FactLinkAnchor({ link }: { link: FactLink }) {
if (link.kind === "filter") {
return (
<Link
to="/diagnostics"
// Sets the component and clears every filter that could hide what it
// points at: a time window from an older investigation, and a severity
// or state — `resolved` above all — that would exclude the very episode
// explaining the condition this link came from.
search={() => ({ component: link.component })}
{...stylex.props(styles.link, shared.focusRing)}
>
{link.label}
</Link>
);
}
return (
<Link to={link.to} {...stylex.props(styles.link, shared.focusRing)}>
{link.label}
</Link>
);
}
function Fact({ fact }: { fact: HealthFact }) {
return (
<li {...stylex.props(styles.fact, fact.tone === "ok" ? styles.quiet : styles.highlighted)}>
<span aria-hidden="true" {...stylex.props(TONES[fact.tone])}>
{ICONS[fact.tone]}
</span>
<span {...stylex.props(styles.label)}>{fact.label}</span>
<span {...stylex.props(styles.value, TONES[fact.tone])}>{fact.value}</span>
{fact.link !== undefined && <FactLinkAnchor link={fact.link} />}
{fact.detail !== undefined && <span {...stylex.props(styles.detail)}>{fact.detail}</span>}
</li>
);
}
export default function HealthStrip() {
const health = useQuery(healthQuery());
if (health.data === undefined) {
return health.isError ? (
<InlineError error={health.error} onRetry={() => void health.refetch()} />
) : (
<p role="status" {...stylex.props(styles.message, shared.pulse)}>
Loading status
</p>
);
}
return (
<>
<ul aria-label="Current status" {...stylex.props(styles.list)}>
{healthFacts(health.data).map((fact) => (
<Fact key={fact.key} fact={fact} />
))}
</ul>
{health.isError && (
<>
<p role="status" {...stylex.props(styles.message)}>
This is the last reading that arrived. The current state is unknown.
</p>
<InlineError error={health.error} onRetry={() => void health.refetch()} />
</>
)}
</>
);
}
@@ -48,3 +48,20 @@ test("component labels read as prose without inventing a name", () => {
expect(componentLabel("query_log")).toBe("Query log");
expect(componentLabel("disk")).toBe("Disk");
});
test("the legacy upstream_history code keeps its copy, so a retained episode still reads as prose", () => {
// Nothing emits it any more, but stored rows outlive the subsystem and the
// list endpoint passes their codes through verbatim.
expect(DIAGNOSTIC_CODES).toContain("upstream_history.write");
const copy = EVENT_COPY["upstream_history.write"];
expect(copy.title).toBe("Upstream history write failed");
expect(copy.impact).toMatch(/no longer runs/);
});
test("the recreated-log copy covers a planned schema change as well as an unreadable file", () => {
const copy = EVENT_COPY["query_log.recreated"];
// The planned cause leads, because it is the one an upgrade produces; the
// unreadable file is the other cause and must not be dropped from the copy.
expect(copy.impact).toMatch(/schema/);
expect(copy.impact).toMatch(/could not be read/);
});
+9 -3
View File
@@ -111,14 +111,20 @@ export const EVENT_COPY: Record<DiagnosticCode, EventCopy> = {
},
"query_log.recreated": {
title: "Query log recreated",
impact: "The old log database was unreadable and was moved aside; the history it held is not in the new one.",
impact: "The old log database was moved aside — a release changed its schema, or the file could not be read — and the history it held is not in the new one.",
remediation: "Keep or delete the aside file named below. Nothing else is required — logging is running.",
link: SETTINGS,
},
/**
* Legacy. Nothing emits this code any more: milestone 30 deleted the
* upstream-minute history subsystem. Stored rows outlive it, and the list
* endpoint passes their codes through verbatim, so the copy stays — a
* retained episode must still read as prose rather than as a dotted string.
*/
"upstream_history.write": {
title: "Upstream history write failed",
impact: "Resolution is unaffected; the per-upstream success and failure aggregates lose the affected window.",
remediation: "Check free disk space and the configuration database's permissions.",
impact: "A recorded failure of a subsystem this version no longer runs. Resolution was unaffected; the per-upstream aggregates it fed are gone.",
remediation: "Nothing to do. Purge the entry once you have read it.",
link: UPSTREAMS,
},
"upstream.exchange": {
@@ -0,0 +1,97 @@
/**
* The health-fact matrix, migrated whole from the Overview status rows this
* replaces. Same states, same words, same link matrix — with the Diagnostics
* links now narrowing the page the strip sits on rather than navigating to it.
*/
import { health } from "@/lib/healthFixture";
import { healthFacts, type HealthFact } from "./healthFacts";
const LOCALE = "en-GB";
const TZ = "UTC";
function factsBy(overrides: Parameters<typeof health>[0] = {}): Record<string, HealthFact> {
return Object.fromEntries(healthFacts(health(overrides), LOCALE, TZ).map((fact) => [fact.key, fact]));
}
test("a healthy box is five quiet facts, none of them linking anywhere", () => {
const facts = healthFacts(health(), LOCALE, TZ);
expect(facts.map((fact) => fact.key)).toEqual(["protection", "upstreams", "query_history", "diagnostics", "disk"]);
expect(facts.every((fact) => fact.tone === "ok")).toBe(true);
expect(facts.every((fact) => fact.link === undefined)).toBe(true);
});
test("protection: active, paused indefinitely, paused until a time, unavailable", () => {
expect(factsBy()["protection"].value).toBe("Active");
expect(factsBy({ protection: { state: "paused", until: null } })["protection"].value).toBe("Paused");
const timed = factsBy({ protection: { state: "paused", until: Date.UTC(2026, 0, 1, 14, 5) / 1000 } })["protection"];
expect(timed.value).toBe("Paused until 14:05");
const gone = factsBy({ protection: { state: "unavailable", until: null } })["protection"];
expect(gone.value).toBe("Unavailable");
expect(gone.link).toEqual({ kind: "route", to: "/blocklists", label: "Blocklists" });
});
test("a pause never reads as a fault, and never carries a way out", () => {
const paused = factsBy({ protection: { state: "paused", until: null } })["protection"];
expect(paused.tone).toBe("notice");
expect(paused.link).toBeUndefined();
});
test("upstreams count the enabled pool, and only an empty one links out", () => {
const ok = factsBy({ upstreams: { state: "ok", available: 1, total: 3 } })["upstreams"];
expect(ok.value).toBe("Available");
expect(ok.detail).toBe("1 of 3 enabled");
expect(ok.link).toBeUndefined();
const none = factsBy({ upstreams: { state: "unavailable", available: 0, total: 3 } })["upstreams"];
expect(none.value).toBe("None reachable");
expect(none.link).toEqual({ kind: "route", to: "/upstreams", label: "Upstreams" });
});
test("query history: losing blames the disk gate, a failed writer blames the query log", () => {
const losing = factsBy({ query_history: { state: "losing", dropped_total: 0, last_drop_s: null } })[
"query_history"
];
expect(losing.value).toBe("Losing rows");
expect(losing.link).toEqual({ kind: "filter", component: "disk", label: "Disk diagnostics" });
const failed = factsBy({ query_history: { state: "failed", dropped_total: 0, last_drop_s: null } })[
"query_history"
];
expect(failed.value).toBe("Writer failed");
expect(failed.link).toEqual({ kind: "filter", component: "query_log", label: "Query log diagnostics" });
});
test("drops are reported while recording, with or without a stamp on the last one", () => {
const stamped = factsBy({
query_history: { state: "recording", dropped_total: 5, last_drop_s: Date.UTC(2026, 0, 1, 9, 30) / 1000 },
})["query_history"];
expect(stamped.tone).toBe("ok");
expect(stamped.detail).toBe("5 queries dropped, last at 09:30");
const unstamped = factsBy({ query_history: { state: "recording", dropped_total: 1, last_drop_s: null } })[
"query_history"
];
expect(unstamped.detail).toBe("1 query dropped");
expect(factsBy()["query_history"].detail).toBeUndefined();
});
test("an unavailable diagnostics store explains itself and links nowhere", () => {
const fact = factsBy({ diagnostics: { state: "unavailable", active_warnings: 0, active_errors: 0 } })[
"diagnostics"
];
expect(fact.value).toBe("Unavailable");
expect(fact.detail).toContain("not being recorded");
// The page a link would filter is the thing that is broken.
expect(fact.link).toBeUndefined();
});
test("storage names the three disk states and always shows what is free", () => {
expect(factsBy()["disk"].value).toBe("OK");
expect(factsBy()["disk"].link).toBeUndefined();
const low = factsBy({ disk: { state: "low", free_bytes: 1024 } })["disk"];
expect(low.value).toBe("Low");
expect(low.tone).toBe("warn");
expect(low.link).toEqual({ kind: "filter", component: "disk", label: "Disk diagnostics" });
const critical = factsBy({ disk: { state: "critical", free_bytes: 0 } })["disk"];
expect(critical.value).toBe("Critical");
expect(critical.tone).toBe("danger");
expect(critical.detail).toBe("0 B free");
});
@@ -0,0 +1,133 @@
/**
* 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: "/blocklists" | "/upstreams"; 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: "/blocklists", label: "Blocklists" },
};
}
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: "/upstreams", 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 }),
},
];
}