Live wall: truth strip keyed to the pipeline, not node heartbeats (#232) #252

Merged
logan merged 2 commits from feat/232-truth-strip into main 2026-10-05 02:47:01 -04:00
Owner

Replaces the heartbeat-driven '● Live' pill on the live wall with a truth strip keyed to pipeline output.

What changed

  • lib/usePipelineHealth.ts (new) polls GET /health/ai every 60 s with plain fetch (it is auth-free; the response type is defined locally, so c2api.ts/types.ts are untouched). Because /health/ai resets on restart, staleness also comes from the data the wall already loads:
    • Radio: newest call started_at. Counts as dry when nodes are up but nothing has been heard for 3N.
    • STT: calls heard after the newest transcript. Counts as dry when at least 3 are waiting and the oldest is more than N old. Duplicates and in-flight calls are excluded.
    • AI: newest incident updated_at/last_thin_at or linked call. Counts as dry after 3N of transcribed but unlinked calls. Any degraded /health/ai tier also makes this stage degraded.
    • Feeds: 511 error or staleness, and camera provider errors. These show on the chip only and do not trigger the bar.
    • N = NEXT_PUBLIC_WALL_STALE_MIN, default 10.
  • components/live/TruthStrip.tsx (new) shows a chip per stage when healthy. When degraded it shows a full-width amber bar, e.g. DEGRADED — radio heard, not transcribed since 08:15 (7 calls waiting) · correlation_cheap tier degraded (gemini …): credits depleted. It uses theme tokens only (sev-moderate is the amber).
  • LiveView.tsx:
    • The heartbeat lastUpdated effect is removed and the prop is no longer passed to MapView, which hides the old pill. MapView itself is not edited (#233 owns it).
    • 'Listening' now shows only when every stage is healthy, and reads "last call heard" instead of "last check-in".
    • When degraded, the map and rail are greyed by a wrapper div.
    • The nodes KPI is now nodes hearing (Nm): nodes with a call in the last N min.

Judgment calls to review

  • Radio silence and AI absence use 3N rather than N. A quiet county at 4am, or chatter the correlator orphans, is weaker evidence than radio that was heard but not transcribed.
  • STT needs at least 3 untranscribed calls, because Whisper writes no transcript for silence or kerchunks by design.
  • NEXT_PUBLIC_WALL_STALE_MIN is baked in at build time. The Dockerfile and CI do not pass it as a build arg yet (not in this batch's file set), so prod gets the default of 10.

Verification

  • tsc --noEmit passes (exit 0).
  • The stack was not run, so STT was not switched off via the feature flag. Instead, the pure derivePipelineHealth() was compiled and run against synthetic calls:
    • Healthy: no bar.
    • STT off for 15 min: bar shows "radio heard, not transcribed since …".
    • STT off for 5 min: no bar.
    • 2 blank calls: no bar.
    • Degraded /health/ai tier: the tier is named.
    • Radio silent for 45 min: bar shows.
  • Manual check still to do: set stt_enabled=false on a system, let calls arrive for more than 10 min, and confirm the amber bar appears, the map greys, and no 'Listening' shows.

Closes #232

🤖 Generated with Claude Code

Replaces the heartbeat-driven '● Live' pill on the live wall with a truth strip keyed to pipeline output. **What changed** - `lib/usePipelineHealth.ts` (new) polls `GET /health/ai` every 60 s with plain fetch (it is auth-free; the response type is defined locally, so `c2api.ts`/`types.ts` are untouched). Because `/health/ai` resets on restart, staleness also comes from the data the wall already loads: - **Radio:** newest call `started_at`. Counts as dry when nodes are up but nothing has been heard for 3N. - **STT:** calls heard after the newest transcript. Counts as dry when at least 3 are waiting and the oldest is more than N old. Duplicates and in-flight calls are excluded. - **AI:** newest incident `updated_at`/`last_thin_at` or linked call. Counts as dry after 3N of transcribed but unlinked calls. Any degraded `/health/ai` tier also makes this stage degraded. - **Feeds:** 511 error or staleness, and camera provider errors. These show on the chip only and do not trigger the bar. - N = `NEXT_PUBLIC_WALL_STALE_MIN`, default 10. - `components/live/TruthStrip.tsx` (new) shows a chip per stage when healthy. When degraded it shows a full-width amber bar, e.g. `DEGRADED — radio heard, not transcribed since 08:15 (7 calls waiting) · correlation_cheap tier degraded (gemini …): credits depleted`. It uses theme tokens only (`sev-moderate` is the amber). - `LiveView.tsx`: - The heartbeat `lastUpdated` effect is removed and the prop is no longer passed to MapView, which hides the old pill. MapView itself is not edited (#233 owns it). - 'Listening' now shows only when every stage is healthy, and reads "last call heard" instead of "last check-in". - When degraded, the map and rail are greyed by a wrapper div. - The nodes KPI is now `nodes hearing (Nm)`: nodes with a call in the last N min. **Judgment calls to review** - Radio silence and AI absence use 3N rather than N. A quiet county at 4am, or chatter the correlator orphans, is weaker evidence than radio that was heard but not transcribed. - STT needs at least 3 untranscribed calls, because Whisper writes no transcript for silence or kerchunks by design. - `NEXT_PUBLIC_WALL_STALE_MIN` is baked in at build time. The Dockerfile and CI do not pass it as a build arg yet (not in this batch's file set), so prod gets the default of 10. **Verification** - `tsc --noEmit` passes (exit 0). - The stack was not run, so STT was not switched off via the feature flag. Instead, the pure `derivePipelineHealth()` was compiled and run against synthetic calls: - Healthy: no bar. - STT off for 15 min: bar shows "radio heard, not transcribed since …". - STT off for 5 min: no bar. - 2 blank calls: no bar. - Degraded `/health/ai` tier: the tier is named. - Radio silent for 45 min: bar shows. - Manual check still to do: set `stt_enabled=false` on a system, let calls arrive for more than 10 min, and confirm the amber bar appears, the map greys, and no 'Listening' shows. Closes #232 🤖 Generated with [Claude Code](https://claude.com/claude-code)
logan added 1 commit 2026-10-05 02:32:32 -04:00
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>
logan added 1 commit 2026-10-05 02:37:53 -04:00
- 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>
logan merged commit b615cce30c into main 2026-10-05 02:47:01 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/server-26#252