Truth strip review fixes: all-offline is not Listening, hearing bounded (#232)
- Every configured node offline now degrades the Radio stage ("all N nodes
offline") so the wall no longer shows "Listening" with nothing up.
- nodes-hearing only counts configured nodes, so it can't exceed N/N.
- AI chip copy: "no incident writes in the recent window" instead of
"last incident write none in window".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f252e3eb61
commit
824b0f6f5a
@@ -182,14 +182,21 @@ export function derivePipelineHealth(
|
|||||||
// ── Radio ────────────────────────────────────────────────────────────────
|
// ── Radio ────────────────────────────────────────────────────────────────
|
||||||
const newestCallAt = maxDate(calls.map((c) => ms(c.started_at)));
|
const newestCallAt = maxDate(calls.map((c) => ms(c.started_at)));
|
||||||
const anyNodeUp = nodes.some((n) => n.status !== "offline");
|
const anyNodeUp = nodes.some((n) => n.status !== "offline");
|
||||||
|
// Every configured node offline is not "Listening" — nothing can be heard.
|
||||||
|
const allNodesOffline = nodes.length > 0 && !anyNodeUp;
|
||||||
const radioDry = anyNodeUp && (!newestCallAt || now - newestCallAt.getTime() > absenceMs);
|
const radioDry = anyNodeUp && (!newestCallAt || now - newestCallAt.getTime() > absenceMs);
|
||||||
const radio: PipelineStage = {
|
const radio: PipelineStage = {
|
||||||
key: "radio", label: "Radio", lastAt: newestCallAt,
|
key: "radio", label: "Radio", lastAt: newestCallAt,
|
||||||
state: nodes.length === 0 ? "unknown" : radioDry ? "degraded" : "ok",
|
state: nodes.length === 0 ? "unknown" : radioDry || allNodesOffline ? "degraded" : "ok",
|
||||||
detail: newestCallAt ? `last call heard ${agoText(newestCallAt, now)}` : "no calls in the recent window",
|
detail: allNodesOffline
|
||||||
|
? `all ${nodes.length} node${nodes.length === 1 ? "" : "s"} offline`
|
||||||
|
: newestCallAt ? `last call heard ${agoText(newestCallAt, now)}` : "no calls in the recent window",
|
||||||
};
|
};
|
||||||
|
// Bounded to configured nodes so a call from a since-removed node can't push
|
||||||
|
// the KPI past N/N.
|
||||||
|
const knownNodes = new Set(nodes.map((n) => n.node_id));
|
||||||
const nodesHearing = new Set(
|
const nodesHearing = new Set(
|
||||||
calls.filter((c) => now - ms(c.started_at) <= staleMs).map((c) => c.node_id),
|
calls.filter((c) => now - ms(c.started_at) <= staleMs && knownNodes.has(c.node_id)).map((c) => c.node_id),
|
||||||
).size;
|
).size;
|
||||||
|
|
||||||
// ── STT: radio heard, nothing transcribed ────────────────────────────────
|
// ── STT: radio heard, nothing transcribed ────────────────────────────────
|
||||||
@@ -225,7 +232,7 @@ export function derivePipelineHealth(
|
|||||||
state: aiDry || aiTiers.length ? "degraded" : reachable === false && !newestAiAt ? "unknown" : "ok",
|
state: aiDry || aiTiers.length ? "degraded" : reachable === false && !newestAiAt ? "unknown" : "ok",
|
||||||
detail: aiDry
|
detail: aiDry
|
||||||
? `transcribed, no incident written since ${newestAiAt ? clockTime(newestAiAt) : clockTime(new Date(oldestUncorrelated))}`
|
? `transcribed, no incident written since ${newestAiAt ? clockTime(newestAiAt) : clockTime(new Date(oldestUncorrelated))}`
|
||||||
: `last incident write ${agoText(newestAiAt, now)}${reachable === false ? " · /health/ai unreachable" : ""}`,
|
: `${newestAiAt ? `last incident write ${agoText(newestAiAt, now)}` : "no incident writes in the recent window"}${reachable === false ? " · /health/ai unreachable" : ""}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Context feeds (chip only — a dead camera feed doesn't blind the pipeline) ──
|
// ── Context feeds (chip only — a dead camera feed doesn't blind the pipeline) ──
|
||||||
@@ -244,7 +251,8 @@ export function derivePipelineHealth(
|
|||||||
|
|
||||||
// ── Verdict ─────────────────────────────────────────────────────────────
|
// ── Verdict ─────────────────────────────────────────────────────────────
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (radio.state === "degraded") parts.push(`nodes checking in, no radio heard since ${newestCallAt ? clockTime(newestCallAt) : "—"}`);
|
if (allNodesOffline) parts.push(`${radio.detail}${newestCallAt ? `, last call heard ${clockTime(newestCallAt)}` : ""}`);
|
||||||
|
else if (radioDry) parts.push(`nodes checking in, no radio heard since ${newestCallAt ? clockTime(newestCallAt) : "—"}`);
|
||||||
if (sttDry) parts.push(stt.detail);
|
if (sttDry) parts.push(stt.detail);
|
||||||
if (aiDry) parts.push(ai_.detail);
|
if (aiDry) parts.push(ai_.detail);
|
||||||
for (const t of degradedTiers) parts.push(tierText(t.tier, t.state));
|
for (const t of degradedTiers) parts.push(tierText(t.tier, t.state));
|
||||||
|
|||||||
Reference in New Issue
Block a user