feat(webapp): the recap for players, three missing controls, and explained gates (phase 7b of v4.3.0) #531

Merged
claude-bot merged 3 commits from feat/phase7b-self-service into main 2026-09-06 08:53:06 +00:00
Contributor

Closes #388, closes #389, closes #382. Phase 7b of the v4.3.0 build order (#514). No migration, no contract change.

#388 recap for players. GET /api/sessions/{id}/shelf/cold-open moves from the GM gate to the member gate, and SessionTable and SessionDetail mount the existing ColdOpenPanel. The payload was not entirely player-safe: open_loops included plot threads with player_visible: false, an invariant three other places enforce, so the widening is a narrowing on the way out in the same service call (gm_view flag): previously_on unchanged (the summary is already member-readable), open_loops filtered to visible threads for non-GMs, carried_count reported as 0 (it counts cards on the GM's private shelf). The panel gains a collapsible mode on its own local-storage key for the two player surfaces; SessionTable fetches its session once and declares the campaign so the nav bar gets its links there (#378's gap).

#389 three controls. Un-reveal is a "Shown to the table" strip on the shelf rather than a per-card button, since a reveal can outlive its card. XP awards are now listed on the ledger (they were totals only, which is why deleteXp had no caller) with a delete that names the award. The .ics endpoint already existed (GET /api/sessions/{id}/calendar.ics, shared code with the feed and invites) with tests and no caller; it is now a download link beside Google Calendar, and its eligibility is widened from "status is confirmed" to "has a scheduled time" so a completed session no longer offers a download that answers 400; an unscheduled session stays 400, documented and tested.

#382 explained gates. A shared BlockedState component (the CampaignHighlights pattern) on the four wiki hard-blocks and WikiArticle's error state; CampaignPlanning explains instead of redirecting; the article edit URL for a non-editor renders the read view with a note; AddToShelf explains its empty state with a link; the highlights draft badge says it publishes when the session summary is approved (there is no per-highlight approve endpoint and none was added); the archived banner names the instance admin as the restore path.

Backend full 2504 passed / 13 skipped (+10); frontend 787 passed (64 files, +42); ruff and eslint clean apart from the pre-existing warning; vite build green.

🤖 Generated with Claude Code

Closes #388, closes #389, closes #382. Phase 7b of the v4.3.0 build order (#514). No migration, no contract change. **#388 recap for players.** `GET /api/sessions/{id}/shelf/cold-open` moves from the GM gate to the member gate, and `SessionTable` and `SessionDetail` mount the existing `ColdOpenPanel`. The payload was not entirely player-safe: `open_loops` included plot threads with `player_visible: false`, an invariant three other places enforce, so the widening is a narrowing on the way out in the same service call (`gm_view` flag): `previously_on` unchanged (the summary is already member-readable), `open_loops` filtered to visible threads for non-GMs, `carried_count` reported as 0 (it counts cards on the GM's private shelf). The panel gains a collapsible mode on its own local-storage key for the two player surfaces; `SessionTable` fetches its session once and declares the campaign so the nav bar gets its links there (#378's gap). **#389 three controls.** Un-reveal is a "Shown to the table" strip on the shelf rather than a per-card button, since a reveal can outlive its card. XP awards are now listed on the ledger (they were totals only, which is why `deleteXp` had no caller) with a delete that names the award. The `.ics` endpoint already existed (`GET /api/sessions/{id}/calendar.ics`, shared code with the feed and invites) with tests and no caller; it is now a download link beside Google Calendar, and its eligibility is widened from "status is confirmed" to "has a scheduled time" so a completed session no longer offers a download that answers 400; an unscheduled session stays 400, documented and tested. **#382 explained gates.** A shared `BlockedState` component (the `CampaignHighlights` pattern) on the four wiki hard-blocks and `WikiArticle`'s error state; `CampaignPlanning` explains instead of redirecting; the article edit URL for a non-editor renders the read view with a note; `AddToShelf` explains its empty state with a link; the highlights draft badge says it publishes when the session summary is approved (there is no per-highlight approve endpoint and none was added); the archived banner names the instance admin as the restore path. Backend full 2504 passed / 13 skipped (+10); frontend 787 passed (64 files, +42); ruff and eslint clean apart from the pre-existing warning; `vite build` green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The cold-open strip — last session's recap, the quests still open — has
been mounted only by SessionShelf, which is GM-only, so the one piece of
catch-up a player most wants was the one surface they could not reach.

`GET /sessions/{id}/shelf/cold-open` moves from `get_session_for_gm` to
`get_session_for_member`, with the players' cut computed by the same
service call rather than a second endpoint:

  - the recap is unchanged; `Session.summary` is already returned to every
    member by the session detail endpoint, so nothing new is exposed;
  - `open_loops` drops threads that are not `player_visible`, the same
    filter the Discord pre-session brief and the bot's thread endpoint
    already apply — that flag is exactly for the rumour the party has not
    heard;
  - `carried_count` reports 0, because it counts cards on the GM's private
    shelf.

On the front end SessionTable and SessionDetail mount the existing
ColdOpenPanel — no copy — in a new `collapsible` mode: a fold/unfold
toggle on its own localStorage key instead of the shelf's permanent ×,
because a player who puts the recap aside before the game wants it back
during it. SessionTable also fetches its session for the campaign id and
declares it, so the nav bar gains that campaign's links there (#378).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three backend capabilities with no way to reach them.

`revokeSessionReveal` had no caller, so revealing a card was one-way: it
stayed on the players' table view for the rest of the session. The shelf
now carries a "Shown to the table" strip listing the live reveals with an
un-reveal on each — a list rather than a control on the card, because a
reveal outlives its card and an orphan would otherwise be unreachable.
The player table drops it on its own poll, exactly as it picks up a
reveal; nothing here talks to it.

`fetchXp`/`deleteXp` had no caller either, and for the same underlying
reason: the ledger rendered XP totals only, so an individual award was
never on screen to act on. Awards are now listed like loot and gold, each
with a delete whose confirm names the award ("Delete 250 XP to Alice —
for the dragon?").

"Add to Calendar" offered Google alone. `GET /api/sessions/{id}/calendar.ics`
already existed, member-gated and unused by the front end, so this is a
link rather than a new endpoint — no second copy of the date arithmetic,
and none of it client-side. Its eligibility widens from "status is
confirmed" to "has a confirmed_time", which is the condition the calendar
section itself renders on; gating the two differently meant a completed
session showed a download that answered 400. A session with no scheduled
time stays a 400, not a 404 — it exists, there is just nothing to put in a
calendar yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(webapp): explain gated and blocked states instead of hiding or redirecting (#382)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 43s
CI / Backend lint (ruff) (pull_request) Successful in 45s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m16s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m0s
CI / Bot tests and audit (pull_request) Successful in 2m21s
CI / Docker image build (pull_request) Successful in 4m33s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m4s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m9s
7e9bec798f
Six places where the app answered "you may not do that here" by removing
the evidence that there was anything to do, which is indistinguishable
from a broken link:

  - CampaignPlanning `navigate(..., {replace: true})`d a non-GM with no
    message. Prep is hidden from players in the nav bar, so anyone landing
    here followed a link a GM shared — it now says Prep is the GM's
    workspace and offers the way back.
  - WikiProposals, WikiNewEntry and WikiDraftReview ended navigation with
    a bare sentence on an empty page. They now use a shared BlockedState
    (the pattern CampaignHighlights already had), which says what the page
    is for and links back to the article, the wiki, the campaign and the
    dashboard. WikiArticle's full-page error state gets the same links.
  - WikiArticle's `/edit` URL for a non-editor silently rendered the read
    view. It still renders the read view — that is the useful thing to
    show — but says why.
  - AddToShelf `return null`ed with no upcoming session; it now says there
    is none and links to where you schedule one.
  - SessionHighlights' draft badge named the state without naming what
    changes it. There is no per-highlight approve endpoint and this does
    not invent one: approving the session summary publishes every
    highlight in one UPDATE, and that control is already on this page, so
    the badge and a count line say so.
  - The archived-campaign banner named no restore path. Archiving is a GM
    action, restoring is an instance admin's, so the banner now says so to
    the GM who cannot — and says scheduled sessions stay put and stop
    sending reminders. The count of those sessions is left to the
    post-archive confirmation, since `silenced_future_sessions` (#479) is
    on CampaignArchiveResponse only, not CampaignResponse.

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 08:38:22 +00:00
claude-bot deleted branch feat/phase7b-self-service 2026-09-06 08:53:07 +00:00
Sign in to join this conversation.
No description provided.