Files
nxdns/admin/src/features/diagnostics/healthFacts.ts
T
mokhtar 85b8be50a0
Gates / frontend (push) Successful in 1m57s
Gates / test (push) Successful in 2m34s
Gates / test-aarch64 (push) Successful in 8m9s
Gates / package (push) Successful in 7m14s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 18m17s
Release / guard (push) Successful in 33s
Gates / test-aarch64 (push) Successful in 7m22s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m35s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m16s
Gates / package (push) Successful in 44s
Release / publish (push) Successful in 10m4s
admin: overview redesign, device scope, one formatting contract (milestone 39)
The Overview page takes the decided visual language (specs/ui-visual-redesign.md): four centred totals with their Activity links, a smoothed area chart of total and blocked queries with point hover and a tooltip centred beside the point, a stacked client chart in eight distinct hues plus one Other band that is always a series, and a card row with the cache hit rate, the query types as a single-hue ramp ring, and the upstream breakdown. The count axis grows its margin with the widest grouped tick and draws whole-number ticks only.

GET /api/overview takes a client parameter; the scoped read uses idx_query_log_ts and the cache keeps scoped slots. The device selector beside the period selector is URL state, so a scoped view is a link, and the tile links carry the scope into Activity. The route reduces a pasted IPv6 scope to the RFC 5952 spelling the logger stores, mapped addresses included, and drops anything that is not an address. A failed device list says so under the selector with a retry.

All measured quantities go through admin/src/lib/format.ts: grouped counts, two-decimal percentages, one-decimal rates, durations as the two largest nonzero units. Identifiers, configured values and preset labels render as written; the module header states that scope. A sweep test refuses toFixed, toLocaleString, Intl.NumberFormat and padStart anywhere else.

Chrome: one 4px radius from the metrics constants, shared Card with a prominent title and a one-line description on every panel, the settings form sections on the same card with a floated legend, the sidebar grouped into Monitoring and System with a status block (protection, queries per minute on Overview, uptime), keyboard-focusable table scroll wrappers, and the accent darkened to 5.43:1 on its wash.

Not built: the spec's ranked-list primitive, which has no consumer and no API rows. Codex reviewed sessions B to D over five rounds (thirty-three findings fixed, thirteen rejected as non-quantities); the owner skipped a sixth round.

Claude-Session: https://claude.ai/code/session_01VTgx3a1zz1R78o4K55kkwR
2026-09-07 23:11:50 +02:00

132 lines
4.8 KiB
TypeScript

/**
* 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, formatCount } 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;
}
/** 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 = `${formatCount(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: `${formatCount(upstreams.available)} of ${formatCount(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 }),
},
];
}