feat(webapp): one screen for a live session (#386, phase 8 of v4.3.0) #532

Merged
claude-bot merged 5 commits from feat/386-one-live-session-screen into main 2026-09-06 09:29:20 +00:00
Contributor

Closes #386. Phase 8, the last item of the v4.3.0 build order (#514). Frontend only, five commits, no migration, no contract change.

SessionDetail is the one live-session screen. The Recording Dashboard's live half moves into a new components/RecordingPanel.jsx (GM-only, id="recording"): the SSE stream with polling fallback and reconnect, the #377 connection state machine (last known state kept, "Connection lost, retrying…" with staleness, "Unknown" when never answered), the elapsed clock, per-speaker capture, "Present, not captured yet", the #327 capture tripwire, and Start/Stop with "End session when stopping recording". The editable summary stays where it was; nothing read-only duplicates it. AudioStatusSection is now "Transcript & Summary".

/sessions/:id/recording redirects to /sessions/:id#recording (kept with the other legacy routes for bookmarks and older embeds); RecordingDashboard.jsx and its tests are deleted with their behaviour and tests moved. The "Recording →" link is gone; SessionShelf gains a Recording link to the anchor.

proposed: no backend change. POST /sessions/{id}/record/start gates on get_session_for_gm only; there was never a status rule, so the confirmed-only entry link hid a working capability. The panel renders from proposed onwards; where a start genuinely cannot happen (no Discord server on the campaign, the backend's own 400) the button is disabled with the reason on the page. The attendance toggle reads "Present" / "Absent" in its one place; the roster is available from in_progress, not only completed.

Forced by the move: the live status is per Discord guild, not per session, so on every session page it would light up an old session as recording; the payload's session_id is now honoured, another session's recording is reported as such, and Start is refused. Also removed the frontend's approveAudio wrapper (its only caller was the deleted page; it targeted the backend's documented deprecated shim, which is untouched).

Frontend 799 passed (64 files: −7 dashboard tests, +17 panel tests, +1 redirect, +1 shelf link), eslint clean apart from the pre-existing warning, vite build green.

🤖 Generated with Claude Code

Closes #386. Phase 8, the last item of the v4.3.0 build order (#514). Frontend only, five commits, no migration, no contract change. `SessionDetail` is the one live-session screen. The Recording Dashboard's live half moves into a new `components/RecordingPanel.jsx` (GM-only, `id="recording"`): the SSE stream with polling fallback and reconnect, the #377 connection state machine (last known state kept, "Connection lost, retrying…" with staleness, "Unknown" when never answered), the elapsed clock, per-speaker capture, "Present, not captured yet", the #327 capture tripwire, and Start/Stop with "End session when stopping recording". The editable summary stays where it was; nothing read-only duplicates it. `AudioStatusSection` is now "Transcript & Summary". `/sessions/:id/recording` redirects to `/sessions/:id#recording` (kept with the other legacy routes for bookmarks and older embeds); `RecordingDashboard.jsx` and its tests are deleted with their behaviour and tests moved. The "Recording →" link is gone; `SessionShelf` gains a **Recording** link to the anchor. `proposed`: no backend change. `POST /sessions/{id}/record/start` gates on `get_session_for_gm` only; there was never a status rule, so the confirmed-only entry link hid a working capability. The panel renders from `proposed` onwards; where a start genuinely cannot happen (no Discord server on the campaign, the backend's own 400) the button is disabled with the reason on the page. The attendance toggle reads "Present" / "Absent" in its one place; the roster is available from `in_progress`, not only `completed`. Forced by the move: the live status is per Discord guild, not per session, so on every session page it would light up an old session as recording; the payload's `session_id` is now honoured, another session's recording is reported as such, and Start is refused. Also removed the frontend's `approveAudio` wrapper (its only caller was the deleted page; it targeted the backend's documented deprecated shim, which is untouched). Frontend 799 passed (64 files: −7 dashboard tests, +17 panel tests, +1 redirect, +1 shelf link), eslint clean apart from the pre-existing warning, `vite build` green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Running a live session meant two screens describing the same recording.
The session page owned the transcript and the editable summary; a
separate Recording Dashboard owned the live picture — the clock,
per-speaker capture, who was in voice and silent, Start and Stop — plus
a read-only second copy of the summary and an attendance list whose
toggle said "✓ Here" where this page said "Present".

The live half moves here, as its own component so SessionDetail (already
1800 lines) does not grow another panel inline. It keeps everything the
dashboard had that was actually about *now*: the SSE stream with its
`record/live` fallback and 2s reconnect, the three-state connection
machine from #377 (last known state kept, "Connection lost, retrying…"
with how stale it is, "Unknown" when the server has never answered), the
elapsed clock, the #327 capture tripwire, per-speaker seconds and
"Present, not captured yet".

Two things are new, both forced by the move:

- The live status is per Discord *guild*, not per session. On a page a
  GM opened deliberately that did not matter; sitting on every session
  page of a campaign it does, or tonight's recording would light up last
  March's session as "Recording in progress". The payload carries the
  session id it belongs to, so a recording of another session is
  reported as exactly that and Start is refused instead of offered.
- Start says *why* when it cannot be used, rather than the panel hiding
  itself. A campaign with no Discord server is the backend's own 400 on
  `/record/start`, known before the click; an unloaded campaign is not
  the same as a missing one, so nothing is disabled on a guess.

The panel is there from `proposed` onwards. The backend has never had a
status rule on `/record/start` — `get_session_for_gm` and nothing else —
so the old page's confirmed-only entry link was hiding a capability that
worked, at the exact moment a GM wants to prove the bot can hear the
table. The section that stays is renamed "Transcript & Summary", which
is what it is now that the recording controls are not in it.

The attendance roster is also available from `in_progress`, not only
`completed`: the dashboard carried one during play, and marking someone
off as they arrive is the natural time to do it. It reads "Present" /
"Absent" — the one label, now that the "✓ Here" copy is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Everything RecordingDashboard did now happens on the session page, so
the page itself goes. Its address does not: it is bookmarked, and it has
gone out in Discord embeds, so `/sessions/:id/recording` becomes a
redirect to `/sessions/:id#recording` and lands on the panel by anchor.
The redirect sits with the other legacy routes, outside the app shell,
so no chrome paints for a route that immediately leaves.

`useDeclareCampaign` goes with the page and is not missed — the session
page has always declared its own campaign for the nav bar (#378), and a
redirect has nothing to declare.

`approveAudio` goes too. It wrapped `POST /sessions/:id/audio/approve`,
which the backend has documented as a deprecated shim for
`/sessions/:id/approve` since the audio-state guard was removed, and the
dashboard's "Approve transcript" button was its only caller. The session
page's own "Approve summary" is the same action against the endpoint
that is not deprecated. The shim stays on the backend for clients that
are not this one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shelf is the surface a GM actually has open during play, and it was
the one session screen with no route to the recording at all — you had
to go back to the session page and find the entry link, which on a
proposed session was not there either. It now carries a Recording link
next to Table view, straight to the panel's anchor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs(webapp): the guide's retry instruction names a page that exists (#386)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 38s
CI / Backend lint (ruff) (pull_request) Successful in 47s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m17s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m59s
CI / Bot tests and audit (pull_request) Successful in 2m23s
CI / Docker image build (pull_request) Successful in 4m34s
CI / Backend migration, tests, and audit (pull_request) Successful in 9m58s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m5s
516707d0ab
"Retry it from the recording page" outlived the recording page by one
commit. The Retry control has always been in the session page's audio
section, now named "Transcript & Summary"; the guide points there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-06 09:14:29 +00:00
claude-bot deleted branch feat/386-one-live-session-screen 2026-09-06 09:29:20 +00:00
Sign in to join this conversation.
No description provided.