Files
server-26/drb-frontend/components/live/TruthStrip.tsx
T
Logan CusanoandClaude Opus 5.5 f252e3eb61 Live wall: truth strip keyed to the pipeline, not node heartbeats (#232)
The '● Live · Ns ago' pill was reset by any node/activeCalls/incident change,
so heartbeats kept it green while STT/correlation were dead (#196).

- lib/usePipelineHealth.ts: polls /health/ai (auth-free) every 60 s and
  derives stage staleness from data too, since /health/ai resets on restart.
  Radio = newest call; STT = calls heard after the newest transcript, dry when
  >=3 are waiting and the oldest is > N min; AI = newest incident write /
  linked call + any degraded /health/ai tier. N = NEXT_PUBLIC_WALL_STALE_MIN
  (default 10); radio silence and AI absence get 3N (weaker evidence).
- components/live/TruthStrip.tsx: per-stage chips; full-width amber
  DEGRADED bar naming the dry stage and any degraded tier.
- LiveView: heartbeat lastUpdated removed and no longer passed to MapView
  (hides the old pill without touching MapView); 'Listening' only when every
  stage is healthy; map + rail greyed when degraded; nodes KPI is now
  'nodes hearing' (a call in the last N min).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:32:00 -04:00

54 lines
2.3 KiB
TypeScript

"use client";
/**
* Truth strip (server-26#232) — replaces the heartbeat-driven '● Live' pill.
*
* Healthy: compact per-stage chips (Radio · Transcripts · AI · Feeds), each
* with its last-output age. Any core stage dry past the threshold, or any
* /health/ai tier degraded: a full-width amber bar that says what broke and
* since when — "DEGRADED — radio heard, not transcribed since 12:15".
* Theme tokens only (sev-moderate is the amber), so both themes follow.
*/
import type { PipelineHealth, PipelineStage } from "@/lib/usePipelineHealth";
function dotClass(s: PipelineStage["state"]): string {
if (s === "degraded") return "bg-sev-moderate";
if (s === "unknown") return "bg-ink-muted";
return "bg-ink-2";
}
export function TruthChips({ health }: { health: PipelineHealth }) {
return (
<div className="flex items-center gap-1.5 flex-wrap" role="status" aria-label="Pipeline health">
{health.stages.map((s) => (
<span key={s.key} title={s.detail}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded border text-[11px] whitespace-nowrap ${
s.state === "degraded" ? "border-sev-moderate text-ink bg-sev-moderate/10" : "border-line text-ink-2"}`}>
<span className={`w-1.5 h-1.5 rounded-full ${dotClass(s.state)}`} aria-hidden="true" />
{s.label}
<span className="sr-only">: {s.state === "ok" ? "healthy" : s.state}, {s.detail}</span>
</span>
))}
</div>
);
}
export function TruthBar({ health }: { health: PipelineHealth }) {
if (!health.degraded || !health.message) return null;
return (
<div role="alert"
className="shrink-0 w-full px-3 py-1.5 border-b border-sev-moderate bg-sev-moderate/15 text-xs text-ink flex flex-wrap items-baseline gap-x-2">
<b className="font-bold tracking-wide">DEGRADED</b>
<span>— {health.message}</span>
<span className="ml-auto text-ink-muted whitespace-nowrap">
stale after {health.staleMin} min · map shows last-known state
</span>
</div>
);
}
/** Chips for the wall bar, plus the bar itself directly below it when degraded. */
export function TruthStrip({ health, placement }: { health: PipelineHealth; placement: "chips" | "bar" }) {
return placement === "chips" ? <TruthChips health={health} /> : <TruthBar health={health} />;
}