feat(export): server-rendered artifacts behind one signed download URL (#160)
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m2s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m42s
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 15s
CI / ui (push) Successful in 1m2s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m42s
The finished-game export (GCG + a new PNG of the final position) is one signed, short-lived relative URL (game.export_url; HMAC-SHA256, 10-min TTL, BACKEND_EXPORT_SIGN_KEY) resolved against the client's own origin and delivered by the best affordance each platform has (five on-device review rounds): - TG Android/desktop: native showPopup chooser -> native downloadFile dialog (bridge-only chain, activation-safe). - TG iOS: app-modal chooser -> OS share sheet with the fetched file (a popup callback cannot supply the activation the sheet needs). - VK iOS: VKWebAppDownloadFile for both formats. - VK Android: the PNG opens in VK's native image viewer, the GCG copies to the clipboard (the VK Android downloader hangs on any download, Content-Length/Range notwithstanding). - VK desktop iframe / desktop browsers: plain anchor downloads. - Mobile browsers: the OS share sheet (fetch-then-share). - Legacy TG (< Bot API 8.0): app modal + GCG clipboard, no image option. The PNG is rasterized on demand by the new internal `renderer` sidecar (node:22-slim + skia-canvas + baked Liberation/Noto Color Emoji fonts) executing the SAME ui/src/lib/gameimage.ts the ui project unit-tests; the backend rebuilds the render payload from the journal + engine.AlphabetTable, and the device date locale, IANA time zone and localized non-play labels ride the signed URL. Nothing is stored — the artifact re-derives from the immutable journal on each GET. The gateway forwards /dl/* (caddy @gateway matcher extended) behind the per-IP public rate limiter and serves bytes via http.ServeContent. Deploy: renderer service in compose + prod overlay + rolling order + prod push list; TEST_/PROD_EXPORT_SIGN_KEY secrets; the sidecar smoke runs in the ui CI job. Docs: ARCHITECTURE, FUNCTIONAL(+_ru), UI_DESIGN, TESTING, deploy/README, renderer/README.
This commit was merged in pull request #160.
This commit is contained in:
@@ -61,6 +61,36 @@ services:
|
||||
memory: 512M
|
||||
networks: [internal]
|
||||
|
||||
# The finished-game image-render sidecar: internal-only, called by the backend for the
|
||||
# PNG export artifact. It runs the same ui/src/lib/gameimage.ts the web client
|
||||
# unit-tests, on skia-canvas (renderer/README.md). node:22-slim carries a shell, so —
|
||||
# uniquely among the built services — it has a real container healthcheck the backend's
|
||||
# depends_on gates on.
|
||||
renderer:
|
||||
container_name: scrabble-renderer
|
||||
image: scrabble-renderer:latest
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: renderer/Dockerfile
|
||||
args:
|
||||
VERSION: ${APP_VERSION:-dev}
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
environment:
|
||||
RENDERER_PORT: "8090"
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8090/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
# A render peaks well under 100 MiB (a 2-MP canvas + skia); idle sits near 60 MiB.
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 192M
|
||||
networks: [internal]
|
||||
|
||||
backend:
|
||||
container_name: scrabble-backend
|
||||
image: scrabble-backend:latest
|
||||
@@ -80,9 +110,15 @@ services:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
renderer:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# search_path=backend matches the migrations (00001 creates the schema).
|
||||
BACKEND_POSTGRES_DSN: postgres://${POSTGRES_USER:-scrabble}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-scrabble}?sslmode=disable&search_path=backend
|
||||
# The finished-game export: the render sidecar address + the HMAC key signing the
|
||||
# public download URLs (deploy env: TEST_/PROD_EXPORT_SIGN_KEY).
|
||||
BACKEND_RENDERER_URL: http://renderer:8090
|
||||
BACKEND_EXPORT_SIGN_KEY: ${EXPORT_SIGN_KEY:?set EXPORT_SIGN_KEY — the export download URL signing secret}
|
||||
# The pool caps at 25 conns (~28 backends) around 500 players; 40 gives headroom
|
||||
# for bursts. Postgres (2 cores / 512 MiB) handles it.
|
||||
BACKEND_POSTGRES_MAX_OPEN_CONNS: "40"
|
||||
|
||||
Reference in New Issue
Block a user