Files
scrabble-game/renderer
Ilia Denisov 3471d40576
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m27s
feat(export): server-rendered artifacts behind one signed download URL
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.
2026-07-02 18:44:07 +02:00
..

renderer — the finished-game image-render sidecar

An internal-only Node service that rasterizes the finished-game export PNG. It runs the same ui/src/lib/gameimage.ts the web project unit-tests — bundled verbatim at image build time (src/entry.ts → esbuild → dist/gameimage.mjs) — on skia-canvas, so the server render is pixel-identical to the design the owner signed off in the browser.

Interface

  • POST /render{game, moves, alphabet, labels, dateLocale, hostname, scale?}image/png. game/moves are the ui-model shapes (GameView / MoveRecord[]); alphabet is the per-variant (index, letter, value) table tile values are drawn from; labels localizes the non-play moves (pass/exchange/resign/timeout); hostname + dateLocale feed the footer.
  • GET /healthz — liveness (the compose healthcheck the backend's depends_on gates on).

The service draws and nothing else: authentication, the participant check and the signed public download URL all live in the backend (backend/internal/server/export.go); the network path is client → gateway /dl/* → backend → this sidecar.

Development

pnpm install          # skia-canvas + esbuild MUST stay approved in pnpm-workspace.yaml
                      # (allowBuilds) or the native binary is silently never fetched
pnpm test             # bundles, then node --test against testdata/request.json
node src/server.mjs   # local run on :8090 (RENDERER_PORT overrides)

The runtime image (renderer/Dockerfile, node:22-slim) bakes in Liberation Sans (the UI font stack's Linux face) and Noto Color Emoji (the scoresheet 🏆); skia-canvas picks both up through fontconfig. The fixture testdata/request.json is a real 35-move self-played Russian Scrabble game.