Skip to content

Show pictures from inside Chronicle page text in Foundry journals - #138

Merged
keyxmakerx merged 8 commits into
mainfrom
claude/project-thread-5q4j0i
Oct 4, 2026
Merged

keyxmakerx merged 8 commits into
mainfrom
claude/project-thread-5q4j0i

Conversation

@keyxmakerx

@keyxmakerx keyxmakerx commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Requested by Key Maker · project thread

Fixes #127
Security implication: GM-only pictures must never reach players. They are never copied. On pull they go into a secret block that the GM-only text fix (#147) then stores as a placeholder, so the picture is not in the saved journal at all; the GM's screen fills it in. Any Chronicle picture inside a secret block is pushed back GM-only whatever shape the editor leaves it in. Copies are fetched only from the apiUrl host, raster types only, with no redirects.
Consumer-verified: Chronicle internal/plugins/syncapi/media_api_handler.go toAPIResponse (signed url, mime_type, file_size; SignedURLTTL = 15 min in internal/plugins/media/signed_url.go). The figure shape and the ce-img--gm strip come from Chronicle#1014, now merged (static/js/widgets/editor_image.js, internal/sanitize/sanitize.go gmPictureRe).
Foundry compatibility: FilePicker is resolved for v12 (global) and v13/v14 (foundry.applications.apps.FilePicker.implementation). Not checked in a live Foundry world yet; see the test plan.
Mockup: Sign-offs card foundry-pictures-127 (before/after), signed "Yes, as shown".
Merge order: after #147 (merged). Test-merged with #153 and #159: no conflicts.

What this changes

Before: Chronicle pages can now hold pictures inside their text (Chronicle#1014). Journal sync copied each picture's /media/<id> path into Foundry unchanged. That path resolves against Foundry's own address, so every picture showed broken.

After:

  • Each shared picture is copied once into worlds/<world>/chronicle-media/<id>.<ext>, and the journal points at the copy. It keeps Chronicle's size, side and caption.
  • A GM-only picture shows only to the GM. The saved journal holds only a placeholder, like GM-only text.
  • Editing the page in Foundry sends the plain /media/<id> paths back, with GM-only intact.

Why

#127 planned to resolve each /media/<id> to a link the way map sync does. That can't work for journals. Chronicle's signed links expire after 15 minutes, so a saved link would break for players soon after every pull. The module keeps its own copy instead.

#127 also said GM-only pictures never arrive because Chronicle strips them. That is true only for keys below Scribe. The module uses the owner's key, so they do arrive, and the module now handles them itself. The same gap for GM secret text is tracked privately.

How

  • scripts/_inline-pictures.mjs (pure) translates the HTML both ways.
    • toFoundryPictures swaps shared srcs for local copies and wraps GM-only figures in <section class="secret chronicle-gm-picture">. It leaves an existing secret block alone.
    • toChroniclePictures restores /media/<uuid>, including from full or signed Chronicle links. Inside any secret block it marks every Chronicle picture ce-img--gm: a figure without the class, a bare picture, or a picture alone in a paragraph. It removes only the block sync itself added.
  • scripts/picture-store.mjs makes the copies.
    • It reads GET /media/:id, fetches cookieless on the apiUrl host with no redirects, and uploads with FilePicker.upload.
    • Only PNG, JPEG, GIF, WebP and AVIF are copied. The declared and served types must match, and the cap is 25 MB.
    • The id must be a UUID, so it never becomes a path.
    • Each id is copied once; existing copies are listed with browse.
    • watchGMPictures puts a fresh signed link into GM-only pictures on the GM's screen. It skips editors, so the link is never saved.
  • scripts/journal-sync.mjs:
    • _pullHtml runs _withPictures before hideSecrets, so GM-only pictures are hidden with the GM-only text. _loadSecretPieces builds the same blocks (without copying), so placeholder ids match.
    • _forPush runs toChroniclePictures after restoreSecrets and before toChronicleSecrets, so a GM-only picture in restored content or in a block the GM typed leaves GM-only.
    • watchGMPictures signs pictures as gm-secret-view.mjs fills placeholders on the GM's screen.
    • A failed copy never aborts a pull. The picture keeps its Chronicle path and is retried on the next pull.
  • styles/chronicle-sync.css mirrors Chronicle's .ce-img width, side and caption rules.
  • Bench:
    • The fake Foundry gains game.world and an in-memory FilePicker.
    • A new journal scenario covers the copy, the secret block, reuse on a second pull, and the round trip.
    • It skips on a Chronicle without pictures.

Honest deviations:

  • A copy made while a picture was shared stays in the world's files if the picture later turns GM-only. It is no longer shown, but modules have no way to delete files. This is noted in .ai.md and on the card.
  • Whether Foundry's ProseMirror editor keeps the figure's class and caption is unverified. The push side is built so that losing them can't expose a GM-only picture.

Test plan

Tenet self-check

  • T-B1 security: signed links are never stored, the fetch stays on the apiUrl host, GM-only pictures are never copied, and a Foundry edit can't drop the GM-only mark
  • T-B2 plugin isolation: module-only change
  • T-B3 production UI: no new controls; the look matches the signed Sign-offs card
  • T-B4 docs: .ai.md file index and trust table, API-CONTRACT.md GET /media/:mediaId, CLAUDE.md file list

Generated by Claude Code

Chronicle serves media only through signed links that expire in about
15 minutes, so a picture link saved into a journal breaks. Journal sync
now copies each shared picture into the world's files once and points
the journal at the copy. GM-only pictures are never copied; they go
inside Foundry's own secret block, and the GM's screen shows them from
a fresh signed link. On push every picture goes back as the plain
/media/<id> path, and every Chronicle picture inside a secret block goes
back GM-only, so a Foundry edit can't make it visible to players.

Fixes #127

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MfPNYnQNHxBmUgRgU4VyHh
@keyxmakerx keyxmakerx self-assigned this Oct 3, 2026
…-5q4j0i

# Conflicts:
#	.ai.md
#	bench/journals.bench.mjs
#	styles/chronicle-sync.css
…-5q4j0i

# Conflicts:
#	CLAUDE.md
#	scripts/journal-sync.mjs
@keyxmakerx
keyxmakerx marked this pull request as ready for review October 4, 2026 01:02
@keyxmakerx
keyxmakerx merged commit 76d42d9 into main Oct 4, 2026
3 checks passed
@keyxmakerx
keyxmakerx deleted the claude/project-thread-5q4j0i branch October 4, 2026 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Journal sync: show pictures from inside Chronicle page text

2 participants