- 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.
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.
Telegram's native showPopup delivers its callback with no user
activation, so navigator.share (TG iOS) and clipboard writes (TG
Android GCG copy) silently fail from it — the chooser is now always the
app's own modal, keeping the button click's gesture alive for the
delivery APIs.
The data:URL preview modal is dropped: the Android TG/VK long-press
menu mangles data: URLs (dead download, black-screen open, base64
clipboard garbage), so a binary PNG has no working client-side route in
those webviews at all. The image option is withheld there until the
server-rendered signed-URL delivery (Telegram downloadFile /
VKWebAppDownloadFile) lands; the plain web and mobile browsers keep it.
On-device findings by the owner on the test contour (TG iOS, TG
Android, VK Android).
The history header's export button now opens a chooser — Telegram's
native popup inside Telegram, the app's own modal elsewhere — offering
the GCG file and a new client-rendered PNG of the final position
(lib/gameimage, Canvas 2D, lazy dynamic import, zero dependencies):
light theme, classic A..O/1..15 axes, label-free premium fills, and a
fixed-typography per-seat scoresheet with GCG-style move coordinates,
multi-word sub-lines, endgame rack-settlement row, winner trophy and a
hostname + device-locale finish date footer; a long game stretches the
board, never the typography.
Delivery mirrors the GCG rules (Web Share with no blob fallback, else
download) except on Android Telegram/VK WebViews and the desktop VK
iframe, where a binary PNG has no clipboard-text fallback: those get a
preview modal with a long-press/right-click save hint and a copy-image
button where ClipboardItem exists.