feat(export): server-rendered artifacts behind one signed download URL
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
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
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.
This commit is contained in:
+22
-22
@@ -326,29 +326,29 @@ journal; an AI game is throwaway). The format chooser is always the app's **own
|
||||
modal** — deliberately not Telegram's native popup, whose callback runs without
|
||||
user activation and silently breaks the share/clipboard delivery it leads to.
|
||||
|
||||
The **image** is rendered on the client (Canvas 2D, always the light theme): the
|
||||
final board with classic coordinate axes A..O / 1..15 on the left — premium squares
|
||||
as plain colour fills, no text labels — and a compact per-seat scoresheet on the
|
||||
right: each seat's name and final score in the header (🏆 by the winner), then one
|
||||
row per move carrying the main word's classic coordinate (an across play is
|
||||
row-first, `8G`; a down play column-first, `H8` — the GCG convention), the word and
|
||||
its points; extra words of a multi-word play ride a smaller second line, non-play
|
||||
moves show as localized notes (pass/exchange/resign/timeout), and a closing ± row
|
||||
shows the endgame rack settlement when there was one (the final scores are
|
||||
authoritative — running totals alone do not include it). The footer stamps the site
|
||||
host and the finish date in the device locale. The scoresheet typography is fixed;
|
||||
a long game stretches the board (never below its minimum) so the image carries no
|
||||
dead space.
|
||||
The **image** is rendered on the server (the internal render sidecar runs the same
|
||||
drawing module the web client tests; always the light theme): the final board with
|
||||
classic coordinate axes A..O / 1..15 on the left — premium squares as plain colour
|
||||
fills, no text labels — and a compact per-seat scoresheet on the right: each seat's
|
||||
name and final score in the header (🏆 by the winner), then one row per move
|
||||
carrying the main word's classic coordinate (an across play is row-first, `8G`; a
|
||||
down play column-first, `H8` — the GCG convention), the word and its points; extra
|
||||
words of a multi-word play ride a smaller second line, non-play moves show as
|
||||
localized notes (pass/exchange/resign/timeout), and a closing ± row shows the
|
||||
endgame rack settlement when there was one (the final scores are authoritative —
|
||||
running totals alone do not include it). The footer stamps the site host and the
|
||||
finish date in the device locale. The scoresheet typography is fixed; a long game
|
||||
stretches the board (never below its minimum) so the image carries no dead space.
|
||||
|
||||
Delivery per format: the **GCG** file is Web-Shared where the platform supports it;
|
||||
on an Android in-app client (Telegram / VK), which has neither Web Share nor a
|
||||
working file download, it copies the GCG text to the clipboard (with a confirming
|
||||
toast); otherwise it downloads the file. The **PNG** is Web-Shared or downloaded the
|
||||
same way, but a binary image has no clipboard-text fallback at all — so the image
|
||||
option is currently **withheld inside the in-app webviews** (Telegram / VK) and
|
||||
offered on the plain web and mobile browsers only; it returns there with the
|
||||
server-rendered signed-URL delivery (Telegram `downloadFile` /
|
||||
`VKWebAppDownloadFile`). Statistics (durable accounts only):
|
||||
Delivery is **one route for both formats on every platform**: the app requests a
|
||||
signed, short-lived download link and hands it to the platform's native download —
|
||||
Telegram's download dialog, VK's `VKWebAppDownloadFile`, or an ordinary browser
|
||||
file download elsewhere (including the desktop VK iframe). The link needs no login
|
||||
to fetch (the platforms' download calls carry none), is valid for minutes, and
|
||||
serves the artifact as a named file attachment. The single exception is a legacy
|
||||
Telegram client without the download dialog (pre-Bot API 8.0): there the GCG falls
|
||||
back to the old clipboard copy (with the confirming toast) and the image option is
|
||||
not offered. Statistics (durable accounts only):
|
||||
wins, losses, draws, max points in a game, and max points for a single move (the
|
||||
best play, which already includes every word it formed plus the all-tiles bonus). It
|
||||
also shows the player's **move count** (their plays — passes and exchanges do not
|
||||
|
||||
Reference in New Issue
Block a user