feat(webapp): the Prep lane: pickers, notes, re-open, a steerable sheet, a front door, and clocks (phase 3 of v4.3.0) #528

Merged
claude-bot merged 6 commits from feat/phase3-prep-workbench into main 2026-09-06 06:05:18 +00:00
Contributor

Closes #302, closes #303, closes #304, closes #305, closes #374, closes #381. Phase 3 of the v4.3.0 build order (#514): the Prep workbench lane, six commits in dependency order. No migration, no contract change.

#302 One ordering rule (utils/sessionOrder.js, soonestFirst): dated sessions ascending, undated last. The Session Prep picker, Beat Planner and the Name Generator (same bug, third picker) all default through AddToShelf.defaultSessionId, which is re-expressed on the shared sort so the two rules cannot drift. API ordering unchanged. No existing test pinned the old behaviour, because the fixture sessions had no dates; the new ones would have caught it.

#303 #454 had already put beat_notes on the GM-gated session response (tested), so the issue's premise had moved. This adds the read the issue asks for, GET …/planning/sessions/{sid}/beat-notes mirroring the PATCH, with a test that the two reads agree, and keeps the response field (the transcript precedent). The client marks the read uncached; one BeatNotesEditor is used by Beat Planner and the prep panel; Save is disabled while the draft matches the server (the untouched-empty-draft guard); a failed read disables the box and the button, since an unseen empty draft is the worst thing to write back; Append reads at click time and fails closed. The test that pinned the broken append now asserts real concatenation.

#304 Configs gain restore: { toValues, matches }. Re-open hands the row to the panel keyed by run id; on mount a panel restores its tool's newest ready run only when matches says it is still about the current selection. matches exists only for session-keyed tools; wiki-article grounding is not restored (a run stores ids, the picker holds entry objects), documented in the file.

#305 The notes editor lives inside the Session Prep panel; a one-shot direction steer is a new SessionPrepParams field with its own context block registered last; the prompt names what the recent summaries are for and makes loose_ends the only backward section. Tests assert on the assembled prompt and system prompt, including that the steer block comes last.

#374 "Prep this session" on the GM session page and on every upcoming row of the campaign page, via ?tool=session_prep&session=…, read once into state; Session Prep is the default tool; untitled sessions read as their date everywhere through one sessionPickerLabel; "Save to this session" and "Add to shelf" each report where the output went; Generation History refetches on a refreshKey. Palette 13 → 6 jobs with the tools as leaves (Session Prep: prep, Beat Planner, series titles; Story Arcs: board, suggester; Quests: tracker, rumor mill; NPCs: instant NPC, voice cues; Names; At the Table: description, random table, loot parcel); nothing removed and activeToolId stays a leaf id, so links, Re-open and history labels are untouched. Switching sessions inside the panel now confirms before discarding unsaved notes (#410's guard, in its new location).

#381 One ElapsedTimer on the session page, the recording page and the Prep button, each with a typical-duration hint. The audio pipeline records no start time, so it anchors on updated_at, which can under-report but never over-report; an exact start column would be a migration and is noted as a follow-up. "history rail" → "Generation History".

Frontend 667 passed (57 files, +51 on this lane), backend full 2476 passed / 13 skipped, ruff and eslint clean apart from the pre-existing warning, vite build green. Two follow-ups worth a decision rather than silent action: Beat Planner is now redundant with the in-panel editor, and an audio_processing_started_at column would make the timer exact.

🤖 Generated with Claude Code

Closes #302, closes #303, closes #304, closes #305, closes #374, closes #381. Phase 3 of the v4.3.0 build order (#514): the Prep workbench lane, six commits in dependency order. No migration, no contract change. **#302** One ordering rule (`utils/sessionOrder.js`, `soonestFirst`): dated sessions ascending, undated last. The Session Prep picker, Beat Planner and the Name Generator (same bug, third picker) all default through `AddToShelf.defaultSessionId`, which is re-expressed on the shared sort so the two rules cannot drift. API ordering unchanged. No existing test pinned the old behaviour, because the fixture sessions had no dates; the new ones would have caught it. **#303** #454 had already put `beat_notes` on the GM-gated session response (tested), so the issue's premise had moved. This adds the read the issue asks for, `GET …/planning/sessions/{sid}/beat-notes` mirroring the PATCH, with a test that the two reads agree, and keeps the response field (the `transcript` precedent). The client marks the read uncached; one `BeatNotesEditor` is used by Beat Planner and the prep panel; Save is disabled while the draft matches the server (the untouched-empty-draft guard); a failed read disables the box and the button, since an unseen empty draft is the worst thing to write back; Append reads at click time and fails closed. The test that pinned the broken append now asserts real concatenation. **#304** Configs gain `restore: { toValues, matches }`. Re-open hands the row to the panel keyed by run id; on mount a panel restores its tool's newest ready run only when `matches` says it is still about the current selection. `matches` exists only for session-keyed tools; wiki-article grounding is not restored (a run stores ids, the picker holds entry objects), documented in the file. **#305** The notes editor lives inside the Session Prep panel; a one-shot `direction` steer is a new `SessionPrepParams` field with its own context block registered last; the prompt names what the recent summaries are for and makes `loose_ends` the only backward section. Tests assert on the assembled prompt and system prompt, including that the steer block comes last. **#374** "Prep this session" on the GM session page and on every upcoming row of the campaign page, via `?tool=session_prep&session=…`, read once into state; Session Prep is the default tool; untitled sessions read as their date everywhere through one `sessionPickerLabel`; "Save to this session" and "Add to shelf" each report where the output went; Generation History refetches on a `refreshKey`. Palette 13 → 6 jobs with the tools as leaves (Session Prep: prep, Beat Planner, series titles; Story Arcs: board, suggester; Quests: tracker, rumor mill; NPCs: instant NPC, voice cues; Names; At the Table: description, random table, loot parcel); nothing removed and `activeToolId` stays a leaf id, so links, Re-open and history labels are untouched. Switching sessions inside the panel now confirms before discarding unsaved notes (#410's guard, in its new location). **#381** One `ElapsedTimer` on the session page, the recording page and the Prep button, each with a typical-duration hint. The audio pipeline records no start time, so it anchors on `updated_at`, which can under-report but never over-report; an exact start column would be a migration and is noted as a follow-up. "history rail" → "Generation History". Frontend 667 passed (57 files, +51 on this lane), backend full 2476 passed / 13 skipped, ruff and eslint clean apart from the pre-existing warning, `vite build` green. Two follow-ups worth a decision rather than silent action: Beat Planner is now redundant with the in-panel editor, and an `audio_processing_started_at` column would make the timer exact. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
`GET /api/campaigns/{id}/sessions` returns sessions newest-first by
`created_at`, and a recurring series materialises its occurrences in date
order — so `sessions[0]` is the occurrence furthest in the future. Every
picker that seeded from index 0 therefore defaulted the GM to the session
they will prep last.

One rule, in one place: `utils/sessionOrder.soonestFirst` orders dated
sessions ascending and sends undated ones to the back. `GeneratorPanel`'s
`sessionOptions` sorts with it (so `session_prep` and `series_titles` both
list and default correctly), `autoSelectFirst` now means "the session
`AddToShelf.defaultSessionId` would pick", and the Beat Planner and Name
Generator seeds in `CampaignPlanning` use the same call. `AddToShelf`'s
own rule is re-expressed on top of the shared sort rather than
re-implementing it.

The API ordering is unchanged — other screens depend on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`beat_notes` is a full-replace field whose only endpoint was a PATCH, so
anything wanting to *add* to it had to guess at the current value. #454
opened one read path (GM-gated on `SessionResponse`); this adds the one
that pairs with the write: `GET /api/campaigns/{id}/planning/sessions/
{sid}/beat-notes`, campaign-scoped, GM-only at the routing layer, same
`{beat_notes, updated_at}` body. `SessionResponse` keeps the field —
`transcript` sets that precedent for the session page and its gate is
pinned by tests — and a new test asserts the two reads agree so they
cannot drift.

On the front end the client marks the read `cache: false` (the shared
15s GET cache would defeat the entire point of a read-before-write), and
the editor moves into one `BeatNotesEditor` used by the Beat Planner and,
next, by the Session Prep panel. It holds the rules that have each been
got wrong once: load before you can save, refuse to save at all if the
read failed, and disable Save while the draft still matches the server —
which is what stops an untouched empty editor PATCHing null over stored
notes. `refreshToken` lets a write from elsewhere in Prep reload it, and
declines to when that would discard unsaved typing.

"Append to prep notes" now reads through the same endpoint immediately
before writing, and is disabled outright until it knows those notes are
readable. A button that appends to a value it cannot read is the bug.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`GeneratorPanel` holds its result in local state and the shell unmounts it
on every tool switch, so a generated sheet was gone the moment the GM
looked at anything else — while the run itself sat persisted in
`generation_results` and listed in Generation History. "Re-open" switched
the active tool and stopped there, which is the least useful half of what
the word promises.

Restoring needs the inverse of `buildParams`, which only the tool knows,
so configs gain `restore`:

  toValues(params, ctx) -> values | null   rebuild the inputs; null = "I
                                           cannot show this faithfully"
  matches(params, values) -> boolean       is this run about what the
                                           panel is currently pointed at?

Re-open passes the row to the panel as `initialRun` (keyed by run id, so
a second Re-open remounts) and uses `toValues`. On mount, a panel with
`matches` asks history for its tool's newest ready run and restores it
only if it still matches the current selection — the session-keyed tools
declare that; the free-text ones do not, because "the last thing you
generated" is not the same claim as "the sheet for this session".

`session_prep`/`series_titles` return null from `toValues` when the run's
session is no longer one the picker offers: every action on a prep sheet
writes to the session it names, so a sheet shown against the wrong one is
worse than no sheet. Wiki-article pickers are not restored — a run
records ids and the picker holds entry objects — which is documented at
the top of workbenchTools.jsx.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The prep prompt's only forward-looking block was `beat_notes`, which was
almost always empty because nothing in Prep could read it (#302, #303).
Handed a stack of recent session summaries and asked for a prep sheet,
the model did the reasonable thing and summarised them again.

Three changes, in the order they matter:

1. The notes editor is now *in* the Session Prep panel, bound to the
   selected session through #303's GET/PATCH — a new `beatNotes` field
   control that holds no value and never reaches `buildParams`. Notes get
   written when writing them is part of asking for the sheet, not when
   they live behind a different palette entry.
2. `direction` on `SessionPrepParams`: a one-shot steer for this run,
   folded in by its own context builder, registered last so it lands
   closest to the closing instruction rather than behind the wiki dump.
   Whitespace normalises to None, so a blank box adds no empty block.
3. The prompt says what the recent summaries are for. `scenes` and
   `secrets_and_clues` must advance past them; `loose_ends` is named as
   the only backward-looking section.

Per this project's testing feedback, a mocked `generate_structured_text`
cannot tell a good prompt from a bad one — so the tests assert on the
assembled prompt and the `system_prompt` kwarg: the framing sentence is
exported as `SESSION_PREP_FRAMING` and asserted by identity, and the
steer is checked both for presence and for being the last block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Prep was reachable only from the campaign page and the nav bar, opened on
the Beat Planner, and presented a campaign-wide session picker that had
already chosen for you. A GM looking at Tuesday had to find Tuesday again.

- **Entry point.** "Prep this session" on the GM's session page and on
  every upcoming row of the campaign page, linking to
  `?tool=session_prep&session=<id>`. `CampaignPlanning` reads both params
  once into state (the link is an entry point, not a live binding, and
  the URL stays shareable), and `?session=` seeds any session picker that
  would have offered that session anyway.
- **Default tool** is Session Prep. Prep exists to answer "what am I
  running on Tuesday".
- **Palette: 13 → 6.** Each top-level entry is a job, with the tools that
  serve it behind sub-tabs: Session Prep (+ Beat Planner, Series Titles),
  Story Arcs (+ Arc Suggester), Quests (+ Rumor Mill, whose only action
  is "add as a plot thread"), NPCs (Instant NPC + Voice Cues), Names, At
  the Table (Description, Random Table, Loot Parcel). Nothing is removed
  and `activeToolId` stays a leaf id, so `?tool=`, "Re-open" and the
  history labels are untouched.
- **Untitled sessions read as their date** everywhere — one
  `sessionPickerLabel` shared by the Prep pickers, AddToShelf and the
  tool configs. Four rows all saying "Untitled session" is not a picker.
- **Two save destinations, both named.** "Save to this session" (prep
  notes) and "Add to shelf" (this session's shelf), each reporting where
  it went with a link. "Replace prep notes" is gone: it wrote the same
  field destructively and sat next to a button that did not.
- **Generation History refetches** on a `refreshKey` the page bumps when
  a run starts or finishes.

Also carried over from #410: the prep panel's session picker now confirms
before discarding unsaved notes, since #305 put that editor inside it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(frontend): put a clock and a typical duration on every long wait (#381)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 1m0s
CI / Backend lint (ruff) (pull_request) Successful in 1m3s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m44s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m21s
CI / Bot tests and audit (pull_request) Successful in 2m51s
CI / Docker image build (pull_request) Successful in 4m15s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m32s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m2s
568b82cce8
"Processing…" and "Generating…" said nothing else, indefinitely. After
about ninety seconds that is indistinguishable from a hang, which is
where the Retry that adds a second job to the queue comes from.

One `ElapsedTimer` for all three surfaces — the session page's audio
status, the recording page, and the GeneratorPanel button — because two
screens describing the same pipeline must not quote different numbers.
It shows time elapsed plus a typical duration, and the typical duration
is where the information actually is: a rising number alone does not tell
a GM whether four minutes is normal.

The audio pipeline records no start time, so the timer anchors on the
session's `updated_at`, which the pipeline sets when it takes the job and
nothing else touches while it runs. An unrelated edit resets it, so this
can under-report and never over-report — the safe direction for a number
read as "should I retry yet?". Prep tools anchor on the click, which is
exact.

Hints are typical, not measured, and say so: the real per-install figure
is on Admin → AI (extrapolated from that install's own last ten runs) and
per-run queue estimates already arrive as `processing_wait`. What belongs
beside a spinner is an honest order of magnitude.

Also: the overrun message names "Generation History" instead of "the
history rail", which is not a thing on that page; and the prep sheet's
save confirmation falls back to "this session" rather than an empty
string if its session somehow is not in the loaded list.

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 05:51:21 +00:00
claude-bot deleted branch feat/phase3-prep-workbench 2026-09-06 06:05:19 +00:00
Sign in to join this conversation.
No description provided.