Shelf: reveal-to-table #245

Closed
opened 2026-07-22 02:59:04 +00:00 by claude-bot · 1 comment
Contributor

Flick a ShelfCard to the players - post it to the Discord channel or a player-facing screen - with a GM-only vs player-safe layer, so a handout or read-aloud can be shown without manual redaction. (#235)

Flick a ShelfCard to the players - post it to the Discord channel or a player-facing screen - with a GM-only vs player-safe layer, so a handout or read-aloud can be shown without manual redaction. (#235)
Author
Contributor

Done — merged in PR #265 (43143d1), CI green.

Scope grew from the original issue on your call: rather than picking Discord or a player screen, this ships both plus a registry so Foundry can be a third surface later, and the player screen targets logged-in members rather than being public.

The player-safe layer. New shelf_cards.player_text — what the table may see, as against the rest of the card, which stays GM-only. When the GM hasn't authored one, the server derives a conservative default: a wiki entry's public summary (never gm_notes), or a generation card's read-aloud fields only. That derivation is a strict allowlist, not a denylist — an unknown tool's output contributes nothing rather than leaking whatever it happens to call its secret — and when it comes out empty the reveal is refused rather than guessed at. Whatever does get shown is saved back onto the card, so you see exactly what the table saw and can edit it instead of revealing blind each time.

Reveals are rows, not a flag on the card, for three reasons: the shown text must be frozen against later edits (a handout the table has read shouldn't retroactively change), a card can be shown more than once, and the record has to outlive the card being discarded in the sweep (#247) — hence ON DELETE SET NULL.

Surfaces are a registry. _REVEAL_DISPATCH maps a name to a dispatcher, so the Foundry pillar's "push into the live scene" is one entry plus one vocabulary value — no schema change, no migration, no new column. That's the extensibility you asked for. Dispatch runs before the row is persisted and is all-or-nothing, so a surface that refuses (Discord with no channel configured) never leaves a reveal claiming the table saw something it didn't.

Authorization. Reading reveals is open to every campaign member — it's the players' surface. Revealing and revoking stay GM-only, and the board itself is unchanged.

Cross-component. shelf_reveal is an additive event on the durable bot stream, so BOT_CONTRACT_VERSION stays 1; backend and bot moved in one commit. The bot posts player_text verbatim, never enriches it, never logs it.

Migration d9e0f1a2b3c4 is the new head. New page at /sessions/:id/table, linked from SessionDetail for every member.

Tested: backend 790, bot 194, frontend 421 + production build; ruff + eslint clean. Coverage includes gm_notes/secret never leaking, the unknown-target and failed-dispatch-reveals-nothing cases, the member-read/GM-write boundary, and the reveal surviving its card.

🤖 Generated with Claude Code

Done — merged in **PR #265** (`43143d1`), CI green. Scope grew from the original issue on your call: rather than picking Discord *or* a player screen, this ships **both** plus a registry so Foundry can be a third surface later, and the player screen targets **logged-in members** rather than being public. **The player-safe layer.** New `shelf_cards.player_text` — what the table may see, as against the rest of the card, which stays GM-only. When the GM hasn't authored one, the server derives a conservative default: a wiki entry's public `summary` (**never** `gm_notes`), or a generation card's read-aloud fields only. That derivation is a strict **allowlist, not a denylist** — an unknown tool's output contributes nothing rather than leaking whatever it happens to call its secret — and when it comes out empty the reveal is **refused** rather than guessed at. Whatever does get shown is saved back onto the card, so you see exactly what the table saw and can edit it instead of revealing blind each time. **Reveals are rows, not a flag on the card**, for three reasons: the shown text must be frozen against later edits (a handout the table has read shouldn't retroactively change), a card can be shown more than once, and the record has to outlive the card being discarded in the sweep (#247) — hence `ON DELETE SET NULL`. **Surfaces are a registry.** `_REVEAL_DISPATCH` maps a name to a dispatcher, so the Foundry pillar's "push into the live scene" is one entry plus one vocabulary value — no schema change, no migration, no new column. That's the extensibility you asked for. Dispatch runs *before* the row is persisted and is all-or-nothing, so a surface that refuses (Discord with no channel configured) never leaves a reveal claiming the table saw something it didn't. **Authorization.** Reading reveals is open to every campaign member — it's the players' surface. Revealing and revoking stay GM-only, and the board itself is unchanged. **Cross-component.** `shelf_reveal` is an additive event on the durable bot stream, so `BOT_CONTRACT_VERSION` stays **1**; backend and bot moved in one commit. The bot posts `player_text` verbatim, never enriches it, never logs it. Migration `d9e0f1a2b3c4` is the new head. New page at `/sessions/:id/table`, linked from SessionDetail for every member. Tested: backend 790, bot 194, frontend 421 + production build; ruff + eslint clean. Coverage includes `gm_notes`/`secret` never leaking, the unknown-target and failed-dispatch-reveals-nothing cases, the member-read/GM-write boundary, and the reveal surviving its card. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#245
No description provided.