fix(tui): load health findings for focused summaries (#1986)

A focused TUI view that shows a health summary (the instance Table, the rig and pod Table, and Overview) now loads the existing health projection that summary displays. Served findings and coverage appear after hydration instead of staying at "Not assessed". Other focused views keep the findings read lazy.

An empty projection still reads as "not a healthy verdict", and a 503, an unrequested read or a remote-host summary stays honest. Pod summaries keep the rig scope. The still-resolving `pendingDrill` first frame isn't covered.

This carries @Kero-66's original fix and report forward, with their commits and authorship kept, through @Aschenbath's resubmission. Thank you both.
This commit is contained in:
Nash Aschenbath authored and GitHub committed 2026-10-25 02:44:15 -07:00
1 parent bff6dba887
commit cb8373cc07
4 files changed
+87 -2

No files matched your search

+13
View File
@@ -390,6 +390,19 @@ ID reads retain them while the source can still project them. A recurrence after
clear receives a new episode ID. This is an on-demand projection, not a history
store; a detail 404 does not prove resolution.
In the TUI, the instance Table and the rig/pod Table and Overview load canonical
findings for their health summaries. These summaries previously could show `Not assessed`
even when the daemon served findings because they did not request the projection.
Once the topology drill is resolved, these pages use the existing bounded
`GET /api/health?limit=200` read; pod summaries continue to cover their rig.
Other topology views using focused reads keep this projection lazy; the Health
tab requests it explicitly.
This findings read is separate from the control-plane `GET /healthz` read.
If `GET /api/health?limit=200` fails, including HTTP 503, the summary shows
`Unknown · Unavailable`. A projection that was not requested remains
`Unknown · Not assessed`, and a loaded empty result is not a healthy verdict.
Lists default to 100 and cap at 200, with `total` and `truncated` explicit.
Ceremony findings take precedence before the cap so context pressure cannot
hide the primary signal. Narrow a truncated query; do not certify the unseen
+4 -1
View File
@@ -497,7 +497,10 @@ export async function hydrateSnapshot(
const broadReads = !readingOnly && !focusedTopology && !wantsConnections;
// Seat rows (Q / WORK / NOW) join these four lists: only a seat's page and the drilled table tab show them.
const seatQueueReads = broadReads || (focusedTopology && (topologyLeaf?.kind === "agent" || (!!topologyLeaf && viewContext?.viewTab === "table")));
const healthRequested = broadReads || viewContext?.viewTab === "health";
// render.ts draws healthSummaryLine on the instance Table and on the rig/pod Table and Overview; those focused pages must load health too.
const rendersHealthSummary = (topologyLeaf?.kind === "host" && viewContext?.viewTab === "table")
|| ((topologyLeaf?.kind === "rig" || topologyLeaf?.kind === "pod") && (viewContext?.viewTab === "table" || viewContext?.viewTab === "overview"));
const healthRequested = broadReads || viewContext?.viewTab === "health" || rendersHealthSummary;
const wantsGraph = wantsTopologyScope && (!viewContext || viewContext.viewTab === "graph");
// A pending pod/agent address needs every rig's seats to resolve.
const pendingInventory = viewContext?.pendingDrill?.resource === "pod" || viewContext?.pendingDrill?.resource === "agent";
+64
View File
@@ -246,6 +246,70 @@ describe("snapshot hydration over the §4.A reads (Phase 2)", () => {
const snap = await hydrateSnapshot(fixtureClient(), undefined, undefined, undefined, undefined, context);
expect(snap.health).toBeUndefined();
});
// The focused views that render the HEALTH summary line must hydrate the projection they read.
const focusedHealthViews = [
["instance Table", { resource: "host", name: "mm2-openrig1" }, "table"],
["rig Table", { resource: "rig", name: "myrig", target: { host: "mm2-openrig1" } }, "table"],
["rig Overview", { resource: "rig", name: "myrig", target: { host: "mm2-openrig1" } }, "overview"],
["pod Table", { resource: "pod", name: "dev", target: { host: "mm2-openrig1", rig: "myrig" } }, "table"],
["pod Overview", { resource: "pod", name: "dev", target: { host: "mm2-openrig1", rig: "myrig" } }, "overview"],
] as const;
it.each(focusedHealthViews)("loads health for the focused %s so its summary reports the served findings", async (_label, drill, tab) => {
const calls: string[] = [];
const coverage = [{ source: "passive-ceremony", unit: "handoff families", limit: 2, total: 3,
evaluated: 2, omitted: 1, partial: true, order: "fixture order" }];
const health = { ...(FIXTURES["/api/health?limit=200"] as Record<string, unknown>), coverage };
const full = await hydrateSnapshot(fixtureClient());
const view = createViewState({ instanceId: "t", getSnapshot: () => full });
view.dispatch({ type: "drill", ...drill } as never);
view.dispatch({ type: "tab", tab });
const { section, viewTab, drill: segments } = view.get();
const snap = await hydrateSnapshot(fixtureClient({}, { "/api/health?limit=200": health }, calls), undefined, undefined, undefined, segments.find((part) => part.kind === "rig")?.name, { section, viewTab, drill: segments });
expect(calls.filter(route => route === "/api/health?limit=200")).toEqual(["/api/health?limit=200"]);
expect(calls.filter(route => route === "/healthz")).toEqual(["/healthz"]);
expect(snap.health).toMatchObject({ availability: "loaded", total: 1, truncated: false, coverage,
records: [{ id: "health-context-dev-impl", scope: { rigId: "01JRIG", seatId: "01JNODEIMPL" }, severity: "warning" }] });
const text = renderScreen(view.get(), snap, { cols: 220, rows: 60 }).lines.join("\n");
expect(text).toMatch(/HEALTH\s+ACTIVE.*WARN 1.*PARTIAL.*context 1/);
expect(text).toContain("Seat context utilization reached 96%.");
expect(text).not.toContain("Not assessed");
});
it.each(focusedHealthViews)("marks the focused %s summary unavailable when findings fail despite a healthy control plane", async (_label, drill, tab) => {
const calls: string[] = [];
const full = await hydrateSnapshot(fixtureClient());
const view = createViewState({ instanceId: "t", getSnapshot: () => full });
view.dispatch({ type: "drill", ...drill } as never);
view.dispatch({ type: "tab", tab });
const { section, viewTab, drill: segments } = view.get();
const snap = await hydrateSnapshot(fixtureClient({ "/api/health?limit=200": { status: 503 } }, {}, calls), undefined, undefined, undefined, segments.find((part) => part.kind === "rig")?.name, { section, viewTab, drill: segments });
expect(calls.filter(route => route === "/api/health?limit=200")).toEqual(["/api/health?limit=200"]);
expect(snap.controlPlane).toMatchObject({ ok: true, selfHostId: "mm2-openrig1" });
expect(snap.health).toMatchObject({ availability: "unavailable", records: [] });
expect(snap.readErrors.join("\n")).toMatch(/health-findings:.*503/);
const text = renderScreen(view.get(), snap, { cols: 220, rows: 60 }).lines.join("\n");
expect(text).toMatch(/HEALTH\s+Unknown.*Unavailable.*canonical health records could not be read/);
expect(text).not.toContain("Not assessed");
expect(text).not.toContain("Seat context utilization reached 96%.");
});
// Views that do not render the summary stay lazy: no health request, nothing to assess.
it.each([
["instance Overview", { resource: "host", name: "mm2-openrig1" }, "overview"],
["instance Graph", { resource: "host", name: "mm2-openrig1" }, "graph"],
["rig Graph", { resource: "rig", name: "myrig", target: { host: "mm2-openrig1" } }, "graph"],
["rig Recent", { resource: "rig", name: "myrig", target: { host: "mm2-openrig1" } }, "recent"],
] as const)("does not read health for the focused %s", async (_label, drill, tab) => {
const calls: string[] = [];
const full = await hydrateSnapshot(fixtureClient());
const view = createViewState({ instanceId: "t", getSnapshot: () => full });
view.dispatch({ type: "drill", ...drill } as never);
view.dispatch({ type: "tab", tab });
const { section, viewTab, drill: segments } = view.get();
const snap = await hydrateSnapshot(fixtureClient({}, {}, calls), undefined, undefined, undefined, segments.find((part) => part.kind === "rig")?.name, { section, viewTab, drill: segments });
expect(calls).not.toContain("/api/health?limit=200");
expect(calls.filter(route => route === "/healthz")).toEqual(["/healthz"]);
expect(snap.controlPlane).toMatchObject({ ok: true });
expect(snap.health).toBeUndefined();
});
it("hydrates the seat queue on the focused topology table so Q / WORK / NOW can fill", async () => {
const row = (qitemId: string, state: string, destinationSession: string) => ({ qitemId, state, destinationSession, blockedOn: null, handedOffTo: null, tier: null, tags: ["slice:OPR.1.0.2.7"], summary: `work ${qitemId}`, body: "", claimedAt: null, tsUpdated: "2026-09-12T10:00:00.000Z" });
const context = { section: "topology" as const, viewTab: "table" as const, drill: [{ kind: "rig" as const, name: "myrig" }] };
@@ -46,6 +46,7 @@ it("a 24-rig landing fetches names only; a selected rig never reads unrelated in
if (u.pathname === "/healthz") return Response.json({ selfHostId: "fixture" });
if (u.pathname === "/api/rigs/summary") return Response.json(rigs);
if (u.pathname === "/api/rigs/r17/nodes" || u.pathname === "/api/queue/recent-transitions" || u.pathname === "/api/queue/list") return Response.json([]);
if (u.pathname === "/api/health") return Response.json({ schema: "openrig.health-list/v0alpha1", evaluatedAt: "2026-09-05T12:00:00.000Z", total: 0, limit: 200, truncated: false, records: [] });
throw Error(`Unrelated request ${u.pathname}`);
}) as typeof fetch });
let snap = emptySnapshot();
@@ -61,7 +62,11 @@ it("a 24-rig landing fetches names only; a selected rig never reads unrelated in
expect(calls.filter(c => c.startsWith("/api/queue/list")).sort()).toEqual([
"/api/queue/list?attention=1", "/api/queue/list?state=blocked", "/api/queue/list?state=in-progress", "/api/queue/list?state=pending&limit=50",
]);
expect(calls).toHaveLength(8);
// The rig Table reads health once without fetching other rigs' node inventories.
expect(calls.filter(c => c.startsWith("/api/health"))).toEqual(["/api/health?limit=200"]);
expect(calls).toHaveLength(9);
expect(snap.health).toMatchObject({ availability: "loaded", total: 0, records: [] });
expect(renderScreen(view.get(), snap, { cols: 220, rows: 60 }).lines.join("\n")).toContain("no findings served; not a healthy verdict");
});
it.each(["overview", "graph", "health"] as const)("a drilled rig on the %s tab reads no seat queue lists", async (viewTab) => {