Skip to content

POST /_screenshot-card persists captures to MediaCache and returns served URLs - #5844

Open
lukemelia wants to merge 1 commit into
cs-12560-get-screenshot-url-query-param-dsl-canonicalize-persistedfrom
cs-12561-post-screenshot-card-persists-captures-to-mediacache
Open

POST /_screenshot-card persists captures to MediaCache and returns served URLs#5844
lukemelia wants to merge 1 commit into
cs-12560-get-screenshot-url-query-param-dsl-canonicalize-persistedfrom
cs-12561-post-screenshot-card-persists-captures-to-mediacache

Conversation

@lukemelia

Copy link
Copy Markdown
Contributor

Stacked on #5843 (GET DSL) → #5840 (serving) → #5838 (store). This PR completes M1's write side: the POST endpoint stops being ephemeral-only.

What this adds

Persistence under the DSL's canonical identity. A capture of an indexed card enqueues its screenshot-card job with a real persist target — instance URL (extensionless), canonical spec hash, and the instance's current index generation — the exact key the GET _screenshot/ route resolves. A POST-published capture therefore serves on the durable URL immediately, including on realms whose allowArbitraryScreenshots gate is closed: publish-by-POST, now actually wired (the base-card gate doc regains that claim here, where it's true). The endpoint itself stays deliberately ungated — an authenticated surface under realm-read trust.

Response shape, byte-compatible. data.attributes keeps the raw status/base64/width/height/contentType fields exactly as before, and gains captures: [{name, url, width, height, deviceScaleFactor, base64?}] when the capture persisted — url is the durable _screenshot/ form with the spec's canonical query (?format=embedded appears; the default spec elides). name/deviceScaleFactor are null for ad-hoc captures (they populate with declared batches and the M2 engine). includeBase64: false omits the bytes everywhere, ahead of the default flipping once callers migrate to URLs. Cards the index doesn't know (or a server without a store) still capture with the legacy shape — no captures, no persist.

Store-first reads and bounded waits, mirroring the GET DSL. A request whose canonical identity already has a ledger entry answers with zero render work — the ledger now records the capture's width/height (additive migration) so the mirror never decodes image bytes, and the object streams back as base64 only when requested. An edited card misses its stale entry and re-captures at the current generation. The HTTP wait is bounded (same 25s budget, injectable for tests); over budget answers 503 + Retry-After while the job persists its capture regardless, so the client's retry is a pure ledger hit — never a second render. A new cardId must be within realmURL 400 protects the persist identity.

Threading: the MediaCacheAdapter now reaches the realm-server's route layer (RealmServerCreateRoutesArgs), from the same env-configured instance the realms and workers share.

Test plan

Seven new handler tests against real Postgres with the in-memory store fake and a scriptable job queue: the persist identity published for the DSL to hit (hash-equal to the GET's key), the served-URL shapes for default and non-default specs, includeBase64: false, ledger hits with zero jobs enqueued, generation-bump re-capture, 503 + Retry-After on a stalled job, and the unindexed-card legacy shape. The pre-existing byte-compat test passes untouched, and the full media-cache/DSL/serving/GC sweep is green (66 tests). Typechecks clean across runtime-common, realm-server, host.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±0      1 suites  ±0   2h 39m 27s ⏱️ + 1m 19s
4 368 tests ±0  4 354 ✅ ±0  14 💤 ±0  0 ❌ ±0 
4 387 runs  ±0  4 373 ✅ ±0  14 💤 ±0  0 ❌ ±0 

Results for commit 61abdb9. ± Comparison against earlier commit 7fa6b92.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   16m 21s ⏱️ + 1m 37s
2 267 tests ±0  2 267 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 350 runs  ±0  2 350 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 61abdb9. ± Comparison against earlier commit 7fa6b92.

@lukemelia
lukemelia force-pushed the cs-12561-post-screenshot-card-persists-captures-to-mediacache branch from 8186a3f to 7fa6b92 Compare August 21, 2026 04:00
…rved URLs

The endpoint stops being ephemeral-only. A capture of an indexed card
persists under the same canonical identity the GET _screenshot/ DSL
resolves — instance URL, canonical spec hash, the instance's current
index generation — so a POST-published capture serves on the durable URL
immediately, gated realms included (publish-by-POST). The response gains
captures: [{name, url, width, height, deviceScaleFactor, base64?}]
while the top-level base64/width/height keep mirroring captures[0], so
current-shape requests stay byte-compatible; includeBase64: false opts
out of the bytes ahead of the default flipping once callers migrate to
URLs.

A request whose identity already has a ledger entry answers from the
store with zero render work (dimensions now recorded on the ledger so
the mirror never decodes bytes), an edited card misses its stale entry
and re-captures at the current generation, and the wait is bounded like
the GET DSL: over budget answers 503 + Retry-After while the job
persists its capture anyway, so the retry is a pure ledger hit. Cards
the index doesn't know still capture with the legacy response shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lukemelia
lukemelia force-pushed the cs-12561-post-screenshot-card-persists-captures-to-mediacache branch from 7fa6b92 to 61abdb9 Compare August 22, 2026 16:08
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.

1 participant