From 5639d57128ebecf87449cdd95511e207541bc417 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 12:42:43 +0000 Subject: [PATCH] Patch dashboard runs from SSE payloads instead of full refetch (BEN-49) The dashboard refetched `/api/runs` and `/api/events/recent` on every SSE event except usage/tick/settings, so a live run with frequent turn events caused per-event table rebuilds, flicker, and wasted bandwidth. Add a small pure reducer in `src/web/dashboardEvents.ts` (`applyTurnEvent` / `applyRunFinishedEvent` / `replaceRun` / `hasRun`) and rewire `Dashboard.tsx` so: - `turn` increments `turnCount` on the matching row in-memory; only falls back to a full `/api/runs` if the runId is unknown (stale tab). - `runFinished` stamps `status` + `finishedAt` immediately for instant feedback, then a single `/api/runs/:id` fills in the authoritative token + cost totals via `replaceRun`. Also refreshes the recent-events feed once. - `runStarted` falls back to `/api/runs` (rare, payload is `Issue`-shaped not `ApiRun`-shaped). Adds 11 unit tests in `dashboardEvents.test.ts` and documents the SSE-to-state convention in `docs/FRONTEND.md`. `pnpm all` green (131 unit + 5 eval); `pnpm build:web` 184ms. --- docs/FRONTEND.md | 17 +++++ src/web/Dashboard.tsx | 46 ++++++++++++- src/web/dashboardEvents.test.ts | 113 ++++++++++++++++++++++++++++++++ src/web/dashboardEvents.ts | 45 +++++++++++++ 4 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 src/web/dashboardEvents.test.ts create mode 100644 src/web/dashboardEvents.ts diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 7acbc13..bb60235 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -36,6 +36,23 @@ file. Don't install a router. All fetches live in [`src/web/api.ts`](../src/web/api.ts). Never call `fetch` from a component. +### SSE → state updates + +The dashboard does **not** refetch `/api/runs` on every `turn` event. The +in-memory runs list is patched from the SSE payload via the pure helpers in +[`dashboardEvents.ts`](../src/web/dashboardEvents.ts): + +- `turn` → `applyTurnEvent` increments `turnCount` on the matching row. +- `runFinished` → `applyRunFinishedEvent` stamps `status` + `finishedAt` + immediately, then a single `GET /api/runs/:id` (`replaceRun`) fills in + authoritative token totals + cost. +- `runStarted` → falls back to `GET /api/runs` (rare event, payload is + `Issue`-shaped not `ApiRun`-shaped). +- A `turn` event for an unknown `runId` (stale tab, missed `runStarted`) + falls back to a full `GET /api/runs`. + +This keeps a live run from triggering a full table refetch per turn. + ## Conventions - **Accessibility first.** Every interactive element is a real `