In a milestone campaign the GM marks a level as waiting, and the player levels up (#1007) #1019

Merged
claude-bot merged 16 commits from feat/l13-milestone-level-waiting into main 2026-10-02 00:40:09 +00:00
Collaborator

Closes #1007. The owner's answer to Q127 (L13), over the recommendation; recorded as D255.

This branch contains #1015's commits (fix/l19-other-class-feats, #1008): both change Level up, so it was built on top. #1015's own run lost its backend job when the runner dropped it mid-run; the same code is tested here in full. Merging this brings #1008 in too.

What changes

As built in lane 11, only a GM could level a character in a milestone campaign. Now:

  • The GM says so. On a character's sheet, beside the GM's own Level up, is a quieter Let Priya level up. Pressing it shows "Level 4 is waiting for Priya" and Take it back.
  • The player does it. Their sheet shows "Level 4 is waiting", Level up and Not yet, exactly as an XP campaign's does at the threshold, and Level up runs for them. Finishing clears the mark; so does the GM levelling the character themselves.
  • The bell tells them, once: "Level 4 is waiting for Tam Ridley". One row per player and character however often it is said; it settles to "A level was waiting for …" when the level is reached or taken back. Bell only: no email, no Discord.
  • A GM lock on Level still stops the player (D256), mark or no mark.
  • With no mark, a player who opens Level up directly still gets the calm sentence ("In this campaign the GM says when a character reaches a level."), not an error.
  • An XP campaign refuses the mark ("In this campaign a level is waiting when the XP reaches it.").

How

  • Three nullable columns on character_sheets: level_waiting_to (the level handed over), level_waiting_at, level_waiting_by. The level is stored, so a mark only counts while it names the current level + 1: a level changed another way leaves it inert.
  • Migration c1007a1b2c3d on b680d1e2f3a4; it also widens the notification kind check with level_waiting. Single head; up, down and up again tested.
  • One answer, not a parallel path: sheet_levelup.waiting() is true for XP at the threshold or a live mark in a milestone campaign, and why_not, can_level, level_why, the preview and apply all read it.
  • PUT / DELETE /api/campaigns/{cid}/sheets/{sid}/level-waiting, GM only, idempotent, no sheet version. access.can_offer_level tells the page when to draw the control (a GM who plays the character themselves has nobody to hand it to).
  • The mark does not travel in an export. No bot API change.
  • Demo seed: Hollow & Ash becomes a milestone campaign (no demo campaign was one), after Tam's existing level-3 story is seeded, and Tam has Level 4 waiting. Its sheet headers stop showing XP, as a milestone campaign's do.
  • Catalogue rows IS-467 (the GM's control) and IS-468 (the player's state and the bell row).

Checked

  • Full backend suite on the merged tree: 6763 passed, 14 skipped; ruff 0.4.4 clean. Full vitest: 3602 passed, 1 skipped; eslint 0 errors. Every coverage/lane*.py passes; changelog and version checks pass.
  • Rendered in an e2e stack (a campaign switched to milestones through Settings; J7's Fighter, played by Priya) at 390×844 (touch) and 1440×900, light and dark, and looked at: the GM with nothing said; the GM after pressing it; the player's sheet and bell; the player's Level up; the player with no mark, on the sheet and at /level-up directly; the GM taking it back. Buttons are 44px high at 390, nothing overflows, focus stays on the control after each press, no console errors or failed calls. At 1440 the level-up was completed end to end: the sheet reached level 2, the cue went for both, and the bell row settled.
  • The render found that repeated marks piled up bell rows; fixed here (one row, refreshed) with tests.

For whoever edits webapp/CLAUDE.md

The migration head is now c1007a1b2c3d.

🤖 Generated with Claude Code

Closes #1007. The owner's answer to Q127 (L13), over the recommendation; recorded as D255. **This branch contains #1015's commits** (`fix/l19-other-class-feats`, #1008): both change Level up, so it was built on top. #1015's own run lost its backend job when the runner dropped it mid-run; the same code is tested here in full. Merging this brings #1008 in too. ## What changes As built in lane 11, only a GM could level a character in a milestone campaign. Now: - **The GM says so.** On a character's sheet, beside the GM's own Level up, is a quieter **Let Priya level up**. Pressing it shows "Level 4 is waiting for Priya" and **Take it back**. - **The player does it.** Their sheet shows "Level 4 is waiting", **Level up** and **Not yet**, exactly as an XP campaign's does at the threshold, and Level up runs for them. Finishing clears the mark; so does the GM levelling the character themselves. - **The bell tells them**, once: "Level 4 is waiting for Tam Ridley". One row per player and character however often it is said; it settles to "A level was waiting for …" when the level is reached or taken back. Bell only: no email, no Discord. - **A GM lock on Level still stops the player** (D256), mark or no mark. - With no mark, a player who opens Level up directly still gets the calm sentence ("In this campaign the GM says when a character reaches a level."), not an error. - An XP campaign refuses the mark ("In this campaign a level is waiting when the XP reaches it."). ## How - Three nullable columns on `character_sheets`: `level_waiting_to` (the level handed over), `level_waiting_at`, `level_waiting_by`. The level is stored, so a mark only counts while it names the current level + 1: a level changed another way leaves it inert. - **Migration `c1007a1b2c3d` on `b680d1e2f3a4`**; it also widens the notification kind check with `level_waiting`. Single head; up, down and up again tested. - One answer, not a parallel path: `sheet_levelup.waiting()` is true for XP at the threshold or a live mark in a milestone campaign, and `why_not`, `can_level`, `level_why`, the preview and apply all read it. - `PUT` / `DELETE /api/campaigns/{cid}/sheets/{sid}/level-waiting`, GM only, idempotent, no sheet version. `access.can_offer_level` tells the page when to draw the control (a GM who plays the character themselves has nobody to hand it to). - The mark does not travel in an export. No bot API change. - **Demo seed:** Hollow & Ash becomes a milestone campaign (no demo campaign was one), after Tam's existing level-3 story is seeded, and Tam has Level 4 waiting. Its sheet headers stop showing XP, as a milestone campaign's do. - Catalogue rows IS-467 (the GM's control) and IS-468 (the player's state and the bell row). ## Checked - Full backend suite on the merged tree: **6763 passed, 14 skipped**; ruff 0.4.4 clean. Full vitest: **3602 passed, 1 skipped**; eslint 0 errors. Every `coverage/lane*.py` passes; changelog and version checks pass. - Rendered in an e2e stack (a campaign switched to milestones through Settings; J7's Fighter, played by Priya) at 390×844 (touch) and 1440×900, light and dark, and looked at: the GM with nothing said; the GM after pressing it; the player's sheet and bell; the player's Level up; the player with no mark, on the sheet and at /level-up directly; the GM taking it back. Buttons are 44px high at 390, nothing overflows, focus stays on the control after each press, no console errors or failed calls. At 1440 the level-up was completed end to end: the sheet reached level 2, the cue went for both, and the bell row settled. - The render found that repeated marks piled up bell rows; fixed here (one row, refreshed) with tests. ## For whoever edits `webapp/CLAUDE.md` The migration head is now `c1007a1b2c3d`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The list's note for another class's feat is tested as it renders (#1008)
Some checks failed
CI / Backend lint (ruff) (pull_request) Successful in 38s
CI / Feedback site tests (pull_request) Successful in 51s
CI / Does this change touch the recording path? (pull_request) Successful in 24s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m6s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Bot tests and audit (pull_request) Successful in 2m1s
CI / Docker image build (pull_request) Successful in 4m15s
CI / End-to-end journeys (phone) (pull_request) Successful in 8m15s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 9m2s
CI / End-to-end journeys (desktop) (pull_request) Successful in 9m8s
CI / Frontend (shard 2/2) (pull_request) Successful in 14m48s
CI / Frontend (shard 1/2) (pull_request) Successful in 20m4s
CI / Backend migration, tests, and audit (pull_request) Failing after 42m56s
ac5e4ea8b0
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into feat/l13-milestone-level-waiting
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 29s
CI / Backend lint (ruff) (pull_request) Successful in 35s
CI / Feedback site tests (pull_request) Successful in 48s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot tests and audit (pull_request) Successful in 2m0s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 3m14s
CI / Docker image build (pull_request) Successful in 4m53s
CI / End-to-end journeys (phone) (pull_request) Successful in 8m5s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 8m36s
CI / Frontend (shard 2/2) (pull_request) Successful in 15m39s
CI / Frontend (shard 1/2) (pull_request) Successful in 20m54s
CI / End-to-end journeys (desktop) (pull_request) Successful in 20m59s
CI / Backend migration, tests, and audit (pull_request) Successful in 52m50s
e3d8042379
Sign in to join this conversation.
No description provided.