ARCHITECTURE: the time zone rides the URL too; 'only degraded platform'
(legacy TG) instead of 'only remaining branch' — the delivery is
per-platform by design now. CLAUDE.md: the renderer joins the build/test
commands and the Dockerfile list.
Round-5 on-device findings:
- VK Android's DownloadFile hangs regardless of Content-Length and
Range/206 (PNG stalls ~80%, GCG at 0%) — the download route is
abandoned there: the PNG opens in VK's native image viewer
(VKWebAppShowImages — its save works on-device) and the GCG returns
to the clipboard copy (the pre-URL route, always solid). VK iOS keeps
VKWebAppDownloadFile (verified perfect); the desktop iframe keeps
plain anchor downloads.
- The iOS share sheet no longer carries a title: iOS pasted the
'game-<hash>.png' filename as accompanying text next to the image.
- TG iOS: the OS share sheet with the fetched file (WKWebView has
navigator.share — the долгоживущий debug-overlay Share path proves
it). The chooser stays the app modal on this one platform: a native
popup callback cannot supply the user activation the sheet needs.
TG Android/desktop keep the native popup + downloadFile (verified
perfect, untouched).
- VK mobile: the PNG now rides VKWebAppDownloadFile like the GCG — the
owner-verified path that lands in VK's native share flow; the
ShowImages viewer is dropped (its preview offered save only, no
share).
- VK desktop iframe: both formats are plain anchor downloads (it is an
ordinary browser; the viewer/bridge path made no sense there).
route.fulfill silently never resolves a FETCH interception given a
Uint8Array body (a navigation download tolerates it), deadlocking the
new share-sheet test — the fetch promise hung, no share, no toast. A
node Buffer unblocks it. Also refreshes the spec header to the final
per-platform delivery matrix.
Per the on-device review, the last hop is now the most native affordance
per platform, all fed by the same signed URL:
- Telegram: the native showPopup chooser returns (safe here — the whole
TG chain is bridge calls, which need no user activation) and both
formats go to the native downloadFile dialog on iOS and Android alike.
- VK: the PNG opens in VK's native photo viewer (VKWebAppShowImages) —
on screen at once, saved/shared from the viewer's own controls; the
GCG keeps VKWebAppDownloadFile; either falls back to a plain anchor
download (the desktop iframe). The gateway now serves /dl/* via
http.ServeContent (Content-Length + Range/206) — another swing at the
VK Android DownloadManager hang; if it persists, the next step is the
clipboard fallback for the VK-Android GCG.
- Mobile browsers: the OS share sheet with the fetched file (the proven
fetch-then-share pattern) — nothing lands in Downloads first; desktop
keeps the anchor download.
Legacy Telegram (< 8.0, no downloadFile) keeps the app modal + GCG
clipboard copy and hides the image option.
Both hops (backend attachment + gateway /dl forward) wrote the body
through net/http's buffered writer, so anything over the write buffer
went out Transfer-Encoding: chunked. Android's system DownloadManager —
the executor behind VKWebAppDownloadFile — hangs indefinitely on a
download of unknown length (observed on-device: VK Android stuck in
'downloading'); Telegram's downloader tolerates chunked, which is why
only VK broke. The artifact is fully buffered anyway, so the length is
known — set it on both responses.
The finished-game export now works identically on every platform: the
client mints a signed relative URL (game.export_url) carrying its date
locale, IANA time zone and localized non-play labels, resolves it
against its own origin and hands it to the platform's native download —
Telegram downloadFile, VKWebAppDownloadFile, or a plain browser anchor
(also the desktop VK iframe). Both artifacts ride the route: the .gcg
text (no more clipboard mode, except the legacy pre-8.0 Telegram
fallback) and the PNG of the final position.
The PNG is rasterized by a new internal 'renderer' sidecar (node:22-slim
+ skia-canvas + baked Liberation/Noto Color Emoji fonts) executing the
SAME ui/src/lib/gameimage.ts the web project unit-tests, bundled at
image build time — one renderer, no drift; the browser no longer draws
or delivers bytes itself. The backend rebuilds the render payload from
the journal + engine.AlphabetTable, verifies the HMAC (10-minute TTL,
BACKEND_EXPORT_SIGN_KEY, constant-time, uniform 404s) on its public
group, and streams the artifact as a named attachment; the gateway
forwards /dl/* behind the per-IP public rate limiter (caddy @gateway
matcher extended — the landing catch-all trap).
Deploy: renderer service (compose + prod overlay + roll before backend
+ prod push list), EXPORT_SIGN_KEY env (TEST_/PROD_ secrets), CI runs
the sidecar smoke in the ui job. Docs: ARCHITECTURE export-delivery
section, FUNCTIONAL(+_ru), UI_DESIGN, TESTING, deploy/README.