Owner: TBD • Branch: followup/frontend-polish
Make the dashboard reflect what's actually happening. On the last gen 2 run, the LiveBoard component stayed on "Waiting for first game…" despite the backend firing game.move events during the tournament. The generation also finished quickly (all games timed out after 0–1 moves), and the GenerationTimeline just showed "KEPT" / "PROMOTED" badges with no indication that every single game had terminated on time because of a bug — the UI happily declared gen 2 a success story.
Two gaps:
- Live board isn't rendering the move stream. Either events are being filtered out before they reach
LiveBoard, or the component'suseMemothat reduces moves into a board state is stale aftergame.finishedfires. - Termination reasons are invisible.
game.finishedcarries aterminationfield (checkmate/stalemate/time/max_moves/illegal_move/error) but the UI never displays it. A tournament where every game ended ontimeshould look alarming on the dashboard, not triumphant.
frontend/src/components/LiveBoard.tsxfrontend/src/components/GenerationTimeline.tsxfrontend/src/components/Bracket.tsxfrontend/src/components/StrategistFeed.tsxfrontend/src/hooks/useEventStream.ts(only if a filter bug is found there)
None. frontend/src/api/events.ts stays untouched (mirror of backend/api/websocket.py); the backend already supplies all needed data in game.finished.
Open frontend/src/components/LiveBoard.tsx. Check the selector that picks the "current" game — it likely keys on the most recent game.move, but if no game.move events are arriving (round-robin completes too fast, or events are getting filtered), the component never leaves the empty state.
Add a debug log (console.debug("LiveBoard events:", events.filter(e => e.type.startsWith("game.")))) and run with the live backend. Confirm whether game.move events reach the component; if not, inspect useEventStream and App.tsx for filtering. If the events are arriving, the bug is in the move→board-state reducer.
The GenerationTimeline row shows PROMOTED / KEPT. Add a termination-breakdown subline underneath: "6 games: 5 time, 1 error". Add it to Bracket too — each pairing cell should tint red when any game in it ended on time, error, or illegal_move.
New helper in GenerationTimeline.tsx:
function terminationSummary(events: DarwinEvent[], gen: number): string {
const finishes = events.filter(
(e): e is GameFinished =>
e.type === "game.finished" && /* filter by gen */ true
);
const counts: Record<string, number> = {};
for (const f of finishes) counts[f.termination] = (counts[f.termination] ?? 0) + 1;
return Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
}builder.completed events with ok: false currently show an "✗" in the feed but hide the error string. Render it under the question text, maybe truncated to 2 lines with full text on hover.
-
LiveBoardrenders moves in real time during a live generation. - Each
GenerationTimelinerow shows a termination breakdown. -
Bracketpairings with non-natural terminations are visually flagged. - Builder error messages surface in
StrategistFeedtooltips.
- Event-volume regression. A real generation can emit ~2400
game.moveevents; rendering every one will tank perf. Mitigation: keepLiveBoardkeyed on the most recently active game, drop the rest into a counter. - Stale memoization.
useMemoreducers can become stale aftergame.finished; verify reducer keys include thegame_idso finished games don't poison the live view.
Merged. LiveBoards (now grid) tracks termination per game in frontend/src/components/LiveBoards.tsx; termination data also flows into Bracket and GenerationTimeline.