[Backlog] GM handouts with reveal control #123

Open
opened 2026-07-14 19:56:00 +00:00 by claude-bot · 1 comment
Contributor

Context / Motivation

Research/product brief. GMs prep maps, letters, and portraits before a session and want to reveal them at the dramatic moment — currently that means pasting into Discord and losing them. Image upload infrastructure already exists: webapp/backend/app/services/image_service.py (JPEG/PNG/WebP/GIF, python-magic validation, Pillow re-encode + EXIF strip, storage under media_root/campaigns/{campaign_id}/lore/{entry_id}), size-capped by media_max_upload_bytes (config.py:76, default 8 MB) — but it is lore-entry-scoped and one-image-per-entry.

Spec (starting sketch)

ModelHandout: id, campaign_id (FK CASCADE), session_id (nullable FK — session handouts vs campaign handouts), title, media_path (nullable) or body_text (nullable Text, for text cards) — exactly one required, content_type (image | pdf | text), revealed_at (nullable timestamp — NULL = hidden), revealed_by_id (nullable FK users), created_by_id, created_at.

Flow

  • GM attaches handouts to a session or the campaign (upload via a generalized image_service path, e.g. media_root/campaigns/{id}/handouts/{handout_id}; extend allowed types with PDF for this feature only).
  • Hidden handouts: visible only to GM (API filters on revealed_at for player tokens).
  • Reveal action: sets revealed_at/revealed_by_id AND posts the handout to the campaign's Discord channel via the bot (new event type through services/bot_pubsub.py publish_bot_event; bot uploads the file or embeds the image). Un-reveal (re-hide) supported but does not delete the Discord message (note in UI).
  • Revealed handouts collect on the session page and a campaign "Handouts" gallery.

Out of scope

  • Collaborative annotation, fog-of-war, or map tooling.
  • Player-uploaded handouts.

Open questions

  • PDF handling: inline preview (pdf.js dependency) vs download-only link — leaning download-only initially.
  • Size limits: reuse media_max_upload_bytes (8 MB) or a separate, larger handout cap (PDFs run bigger)? Discord's own upload limit (8-25 MB depending on boost) constrains the reveal-post path — decide fallback (post link instead of file).
  • GIF animation preservation matters here (already handled by image_service).

Acceptance criteria (for when this is pulled)

  • Hidden handout invisible to player tokens (API test), visible to GM.
  • Reveal flips visibility and produces exactly one Discord post via the bot.
  • Gallery lists revealed handouts; session page shows that session's.
  • Upload validation matches image_service rigor (magic-byte check, size cap) including the PDF path.

References

  • webapp/backend/app/services/image_service.py (validation/storage pattern; _media_dir at :80, _validate_and_save at :97)
  • webapp/backend/app/config.py:76 (media_max_upload_bytes)
  • webapp/backend/app/services/bot_pubsub.py:29 (publish_bot_event), bot/questboard_bot/cogs/notifications.py:169 (dispatch map for a new handout_revealed event)

Filed from the July 2026 full-project review.

## Context / Motivation Research/product brief. GMs prep maps, letters, and portraits before a session and want to reveal them at the dramatic moment — currently that means pasting into Discord and losing them. Image upload infrastructure already exists: `webapp/backend/app/services/image_service.py` (JPEG/PNG/WebP/GIF, python-magic validation, Pillow re-encode + EXIF strip, storage under `media_root/campaigns/{campaign_id}/lore/{entry_id}`), size-capped by `media_max_upload_bytes` (`config.py:76`, default 8 MB) — but it is lore-entry-scoped and one-image-per-entry. ## Spec (starting sketch) **Model** — `Handout`: `id`, `campaign_id` (FK CASCADE), `session_id` (nullable FK — session handouts vs campaign handouts), `title`, `media_path` (nullable) **or** `body_text` (nullable Text, for text cards) — exactly one required, `content_type` (`image` | `pdf` | `text`), `revealed_at` (nullable timestamp — NULL = hidden), `revealed_by_id` (nullable FK users), `created_by_id`, `created_at`. **Flow** - GM attaches handouts to a session or the campaign (upload via a generalized `image_service` path, e.g. `media_root/campaigns/{id}/handouts/{handout_id}`; extend allowed types with PDF for this feature only). - Hidden handouts: visible only to GM (API filters on `revealed_at` for player tokens). - **Reveal** action: sets `revealed_at`/`revealed_by_id` AND posts the handout to the campaign's Discord channel via the bot (new event type through `services/bot_pubsub.py` `publish_bot_event`; bot uploads the file or embeds the image). Un-reveal (re-hide) supported but does not delete the Discord message (note in UI). - Revealed handouts collect on the session page and a campaign "Handouts" gallery. ## Out of scope - Collaborative annotation, fog-of-war, or map tooling. - Player-uploaded handouts. ## Open questions - **PDF handling**: inline preview (pdf.js dependency) vs download-only link — leaning download-only initially. - Size limits: reuse `media_max_upload_bytes` (8 MB) or a separate, larger handout cap (PDFs run bigger)? Discord's own upload limit (8-25 MB depending on boost) constrains the reveal-post path — decide fallback (post link instead of file). - GIF animation preservation matters here (already handled by `image_service`). ## Acceptance criteria (for when this is pulled) - Hidden handout invisible to player tokens (API test), visible to GM. - Reveal flips visibility and produces exactly one Discord post via the bot. - Gallery lists revealed handouts; session page shows that session's. - Upload validation matches `image_service` rigor (magic-byte check, size cap) including the PDF path. ## References - `webapp/backend/app/services/image_service.py` (validation/storage pattern; `_media_dir` at `:80`, `_validate_and_save` at `:97`) - `webapp/backend/app/config.py:76` (`media_max_upload_bytes`) - `webapp/backend/app/services/bot_pubsub.py:29` (`publish_bot_event`), `bot/questboard_bot/cogs/notifications.py:169` (dispatch map for a new `handout_revealed` event) _Filed from the July 2026 full-project review._
Author
Contributor

The wiki design record #593 reshapes this issue (owner decisions, 2026-09-10).

  • A handout is a document article with a file attached (image, PDF or text), not a new Handout table with its own revealed_at.
  • It's revealed through the existing reveal feature (shelf_reveals). That feature already supports choosing players, withdrawing a reveal, and keeping a copy of what was shown.
  • Players get a Handouts collection sorted by session, with their personal handouts marked.

Two scope changes from the body above:

  • Handouts can go to one player. The owner does this in play, and the reveal feature already supports it.
  • Handouts uploaded by players are no longer out of scope. They follow the campaign's edit-permission setting in #593.

Still open, and should carry into whichever build issue #593 is split into:

  • the Discord post on reveal
  • PDF preview versus download-only
  • size limits

Leaving this open until then.

**The wiki design record #593 reshapes this issue** (owner decisions, 2026-09-10). - **A handout is a document article with a file attached** (image, PDF or text), not a new `Handout` table with its own `revealed_at`. - **It's revealed through the existing reveal feature** (`shelf_reveals`). That feature already supports choosing players, withdrawing a reveal, and keeping a copy of what was shown. - **Players get a Handouts collection** sorted by session, with their personal handouts marked. Two scope changes from the body above: - **Handouts can go to one player.** The owner does this in play, and the reveal feature already supports it. - **Handouts uploaded by players are no longer out of scope.** They follow the campaign's edit-permission setting in #593. Still open, and should carry into whichever build issue #593 is split into: - the Discord post on reveal - PDF preview versus download-only - size limits Leaving this open until then.
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#123
No description provided.