Lane 5, PRs 1 and 2 (server): scenes, and the shelf's cards (#731, #689; D12, D113, D114, D119) #827

Merged
claude-bot merged 4 commits from build/lane5-p2-cards into main 2026-09-26 07:50:08 +00:00
Collaborator

This is the server half of lane 5's board: two PRs in one, because PR 2 builds on PR 1's migration. No pages; page PR 4a builds on this.

PR 1: scenes (migration b2c4d6e8f0a1)

  • The model: scenes is its own table.
    • A partial unique index stops two live scenes in a campaign sharing a name, ignoring case and spacing (D114). The SQL expression is shared by the model, the services and the migration.
    • There is no Foundry column (D113).
  • The API: CRUD, soft delete and restore under /api/campaigns/{id}/scenes. A 409 carries the existing scene.
  • Filing:
    • Cards and pins file by scene_id, and a card's PATCH takes scene_id (an explicit null unfiles).
    • The old {scene: "name"} finds or creates the scene.
    • A pin follows its card's latest filing.
  • The migration backfills from the old text column, merging by the name rule. The column stays, frozen and unmapped.
  • The demo seed is updated to create scene rows.

PR 2: the cards (migration c3d5e7f9a1b2)

  • The resurrection bug is fixed. Taking a carried card off the shelf deleted it, and reconcile_pins made it again within 20 seconds; the sweep's "Let it go" did the same. Removal is now a soft delete (removed_at) that the pin respects. POST …/shelf/{card}/restore is D12's undo.
  • Rule cards (card_kind: rule, reference_entry_id). Access is limited to the instance's packs and this campaign's own.
  • Note cards (D119): title and text.
  • Card faces: pronouns and the whole stat-block envelope on lore snapshots.
  • Filing on create: scene_id when a card is added.
  • Indexes for pack re-imports, which delete every record in a pack.

Tests:

  • New: test_scenes.py, test_scenes_migration.py, test_shelf_removal.py, test_shelf_card_kinds.py and test_shelf_cards_migration.py.
  • The full backend suite before the merges: 4477 passed.
  • Both migrations up, down and up on postgres 16.
  • ruff is clean.

Known gap: a pack re-import unlinks rule cards (reference_entry_id → NULL). The snapshot keeps pack_id and slug for a later re-link.

🤖 Generated with Claude Code

This is the server half of lane 5's board: two PRs in one, because PR 2 builds on PR 1's migration. No pages; page PR 4a builds on this. **PR 1: scenes** (migration `b2c4d6e8f0a1`) - **The model:** `scenes` is its own table. - A partial unique index stops two live scenes in a campaign sharing a name, ignoring case and spacing (D114). The SQL expression is shared by the model, the services and the migration. - There is no Foundry column (D113). - **The API:** CRUD, soft delete and restore under `/api/campaigns/{id}/scenes`. A 409 carries the existing `scene`. - **Filing:** - Cards and pins file by `scene_id`, and a card's `PATCH` takes `scene_id` (an explicit null unfiles). - The old `{scene: "name"}` finds or creates the scene. - A pin follows its card's latest filing. - **The migration** backfills from the old text column, merging by the name rule. The column stays, frozen and unmapped. - **The demo seed** is updated to create scene rows. **PR 2: the cards** (migration `c3d5e7f9a1b2`) - **The resurrection bug is fixed.** Taking a carried card off the shelf deleted it, and `reconcile_pins` made it again within 20 seconds; the sweep's "Let it go" did the same. Removal is now a soft delete (`removed_at`) that the pin respects. `POST …/shelf/{card}/restore` is D12's undo. - **Rule cards** (`card_kind: rule`, `reference_entry_id`). Access is limited to the instance's packs and this campaign's own. - **Note cards (D119):** title and text. - **Card faces:** pronouns and the whole stat-block envelope on lore snapshots. - **Filing on create:** `scene_id` when a card is added. - **Indexes for pack re-imports**, which delete every record in a pack. **Tests:** - New: `test_scenes.py`, `test_scenes_migration.py`, `test_shelf_removal.py`, `test_shelf_card_kinds.py` and `test_shelf_cards_migration.py`. - The full backend suite before the merges: 4477 passed. - Both migrations up, down and up on postgres 16. - ruff is clean. **Known gap:** a pack re-import unlinks rule cards (`reference_entry_id` → NULL). The snapshot keeps `pack_id` and `slug` for a later re-link. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A scene was a free-text tag on each shelf card. It is now a row of the
campaign (migration b2c4d6e8f0a1): made once, files cards on every
session, can be empty, renamed in one place, and deleted with an undo
(a card in a deleted scene reads as Unfiled and keeps its pointer).
Names are unique among a campaign's live scenes ignoring case and
spacing, by one SQL expression shared by the index, the service and the
migration.

- GET|POST /api/campaigns/{id}/scenes (?session_id= for tonight's
  counts), PATCH|DELETE .../scenes/{sid}, POST .../scenes/{sid}/restore;
  a taken name is a 409 carrying the scene that has it.
- PATCH /api/sessions/{id}/shelf/{card} takes scene_id (explicit null
  unfiles); ShelfCardOut gains scene_id. {scene: name} still works for
  the current page (find-or-create by name).
- Pins carry a scene_id, follow the GM's latest filing while open, and
  file the cards they materialise; copy-from keeps the scene.
- The migration merges each campaign's names ignoring case and spacing
  (D114), files cards and pins, and leaves shelf_cards.scene frozen (no
  longer mapped); the downgrade writes the filing back as names.
- No Foundry pairing column (D113). The demo seed files its cards in
  scene rows.

The one-head pin moves to test_scenes_migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A carried card taken off the shelf came back within 20 seconds: DELETE
hard-deleted it and the next GET /shelf let its pin make it again (the
sweep's "Let it go" too). Taking a card off is now a soft delete
(shelf_cards.removed_at) that reconcile_pins counts as the pin's card,
with an undo (D12).

- DELETE /api/sessions/{id}/shelf/{card} keeps the card; a second
  DELETE is a no-op. POST .../shelf/{card}/restore is the undo and
  returns the card as it was (place, notes, scene, player text, pin,
  reveal). A removed card is a 404 to PATCH, pin, unpin, reveal and
  promote, and is left out of tonight's scene counts, This week's prep
  count, the cold open's carried count and the harvest; copy-from
  neither copies one nor puts one back. Re-adding a removed wiki entry
  or rule brings the same card back. Reveals keep naming the card.
- Rule cards (IS-177): card_kind "rule" + reference_entry_id, checked
  as GET /reference/{id} checks it and narrowed to the instance's packs
  and this campaign's own. One per session, carried by pins, copied;
  no server-derived player text. The snapshot keeps pack_id + slug to
  find the record after a re-import.
- Note cards (D119, IS-439): title and text on add and on PATCH (notes
  only); the text is the snapshot's summary.
- POST /shelf takes scene_id, filing the new card as it is added.
- Wiki cards' snapshots carry pronouns and the stats envelope (IS-235),
  live.
- Migration c3d5e7f9a1b2 (down b2c4d6e8f0a1): the kind CHECK on cards
  and pins, reference_entry_id on both (SET NULL, indexed), one card per
  record per session, removed_at. The downgrade turns rule cards into
  notes and deletes removed cards. The one-head pin moves to
  test_shelf_cards_migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
Merge remote-tracking branch 'origin/main' into build/lane5-p2-cards
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 55s
CI / Bot tests and audit (pull_request) Successful in 1m54s
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Feedback site tests (pull_request) Successful in 42s
CI / Docker image build (pull_request) Successful in 19s
CI / Bot/backend version sync (pull_request) Successful in 29s
CI / Does this change touch the recording path? (pull_request) Successful in 16s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Frontend tests, audit, and build (pull_request) Successful in 17m22s
CI / Backend migration, tests, and audit (pull_request) Successful in 30m46s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 25m17s
2fb96d8934
# Conflicts:
#	CHANGELOG.md
Sign in to join this conversation.
No description provided.