Commit Graph

491 Commits

Author SHA1 Message Date
Ilia Denisov 8db18f2a2a fix(export): per-platform polish from the on-device review
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
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 1m41s
- 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).
2026-07-02 23:05:35 +02:00
Ilia Denisov a9376c20a2 fix(e2e): Buffer body for the /dl route fulfill
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 1m2s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
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.
2026-07-02 22:30:54 +02:00
Ilia Denisov c3c26bbbc0 feat(export): platform-native delivery — TG popup+downloadFile, VK viewer, mobile share sheet
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Failing after 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
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.
2026-07-02 22:19:46 +02:00
Ilia Denisov c3c27bba5e fix(export): explicit Content-Length on the download responses
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m2s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
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.
2026-07-02 19:02:04 +02:00
Ilia Denisov 3471d40576 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
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
developer 16a4431158 Merge pull request 'feat(game): finished-game export as a PNG image behind a format chooser' (#159) from feature/game-export-image into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m0s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m21s
2026-07-02 15:29:40 +00:00
developer 422e3ccc2a Merge pull request 'fix(game): no placement auto-zoom in landscape' (#158) from feature/landscape-no-autozoom into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 59s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m20s
2026-07-02 15:19:24 +00:00
Ilia Denisov 946420db93 fix(game): own export chooser modal; PNG option outside in-app webviews only
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s
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).
2026-07-02 17:16:05 +02:00
Ilia Denisov a0eacf3011 feat(game): finished-game export as a PNG image behind a format chooser
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
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.
2026-07-02 16:20:35 +02:00
Ilia Denisov 622d3965a7 fix(game): no placement auto-zoom in landscape
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m12s
Landscape fits the whole board, so the coarse-pointer auto-zoom on tile
placement, drag hover-hold and hint only hid the rest of the position.
Gate all three on portrait; manual double-tap/pinch zoom is unchanged.
New e2e lock both sides: landscape placement stays unzoomed, portrait
placement still auto-zooms (touch-emulated, both engines).
2026-07-02 15:08:30 +02:00
developer 2ab01ed8f7 Merge pull request 'feat(landing): Russian default + SEO head, icons and OG card' (#157) from feature/landing-seo into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 57s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m15s
2026-07-01 23:29:28 +00:00
Ilia Denisov 1ed624eaf1 feat(landing): Russian default + SEO head, icons and OG card
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
The landing now always opens in Russian (saved 🌐 choice still wins) —
browser-language detection made the indexed content nondeterministic
(Googlebot renders with en-US). landing.html gains the static Russian
SEO head: title/description, canonical pinned to the production origin,
Open Graph card (Telegram/VK link previews), twitter:card, JSON-LD,
theme-color and the favicon set; the SPA shell turns noindex and its
tab title becomes «Эрудит (Скрэббл)». New assets/icons generator
(same tile design as the VK loader) produces favicon.svg/ico,
apple-touch-icon.png and og-image.png into ui/public/, plus robots.txt.
2026-07-02 01:25:19 +02:00
developer f3e2a6822b Merge pull request 'fix(ui): VK Android external links + landing VK entry + drop 1️⃣ badge' (#156) from feature/vk-links-landing into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 14s
CI / ui (push) Successful in 57s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m18s
2026-07-01 22:39:26 +00:00
Ilia Denisov 65063621a9 fix: landing smoke test
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
2026-07-02 00:29:38 +02:00
Ilia Denisov 4e169c368d chore: i18n game descriptions
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) Failing after 1m0s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
2026-07-02 00:24:16 +02:00
Ilia Denisov aec915d5c1 feat(ui): drop the one-word status-bar badge
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
The small 1️⃣ in the status bar's score-preview slot read as noise; the
single-word rule keeps its spelled-out label in the history header and
the lobby invitation card.
2026-07-02 00:10:08 +02:00
Ilia Denisov 5f574a765d feat(landing): VK entry logo + restore the Telegram channel build-arg
Add the VK Mini App logo next to the Telegram one on the landing hero,
linked via the new VITE_VK_APP_LINK build-arg (full URL, wired through
compose, CI and prod-deploy from TEST_/PROD_VITE_VK_APP_LINK).

Also restore the landing's Telegram link itself: commit 57c778f
collapsed the per-language vars to VITE_TELEGRAM_GAME_CHANNEL_NAME in
compose/CI but left gateway/Dockerfile with the stale _EN/_RU ARGs and
without the plain one, so the built bundle saw the var as undefined and
dead-code-eliminated the whole channel-link branch — deployed landings
(prod included) have shown no Telegram logo since.
2026-07-02 00:10:02 +02:00
Ilia Denisov db17287113 fix(ui): route external links out of the Android VK WebView
The Android VK client's WebView ignores target=_blank and navigates the
Mini App's own window to the target, stranding the player outside the
game with no way back (the dictionary lookup, About/Feedback links, the
ad banner and the bot-link modal fallbacks). vk-bridge 3.x has no method
to open an external URL, so external links are routed through VK's own
leave-VK redirect (vk.com/away.php), which the client intercepts
natively and hands to the system browser.

onExternalLinkClick moves from lib/telegram to a new lib/links that
composes the Telegram and VK routers; iOS and desktop VK open _blank
correctly and are left alone.
2026-07-02 00:09:52 +02:00
developer c864147982 Merge pull request 'feat(telemetry): local move-preview adoption metrics (Phase 4)' (#155) from feature/local-eval-telemetry into development
CI / changes (push) Successful in 3s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 17s
CI / ui (push) Successful in 58s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m18s
2026-07-01 21:39:59 +00:00
Ilia Denisov 2e8fa83814 feat(telemetry): local move-preview adoption metrics (Phase 4)
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Measure uptake of the client-side local move-preview accelerator (§5) so
adoption can be watched before defaulting it on: app cold starts, dictionary
loads by result (fetched / cache_hit / miss) and move previews by path
(local / network — the backend load shed).

A small best-effort client beacon (POST /metrics/local-eval, session-gated)
batches counter deltas and posts them on a 60s timer and when the app is
backgrounded — never on the gameplay path: the in-app counters are plain
in-memory increments, only the periodic flush touches the network and it is
fire-and-forget. The gateway folds each batch into three OTel counters
(local_eval_cold_start_total, local_eval_dict_load_total,
local_eval_preview_total), clamped against a spoofed inflation.

- gateway: counters + recordLocalEval + session-gated /metrics/local-eval handler
- ui: localeval-metrics accumulator/beacon; hooks in the dict loader, in
  Game.recompute and in bootstrap (skipped under the mock harness)
- caddy: route /metrics/* to the gateway
- docs: ARCHITECTURE §11; Grafana "Scrabble — Users" dashboard panels
2026-07-01 23:31:49 +02:00
developer 5f87b3fa43 Merge pull request 'feat: on-device move preview (local eval) with network fallback' (#154) from feature/local-eval into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 17s
CI / ui (push) Successful in 58s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m18s
2026-07-01 21:02:09 +00:00
Ilia Denisov 5689f7f6a3 feat: on-device move preview (local eval) with network fallback
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Score and validate a tentative move on-device instead of a per-arrangement network
round trip. The dawg reader and the validate/score/direction slice of the
scrabble-solver engine are ported to TypeScript (ui/src/lib/dict), pinned
byte-for-byte to the Go engine by a `conformance` CI job (full-dictionary reader
parity plus a battery of plays across every variant and both cross-word rules,
including the inferred orientation). The server stays authoritative — submit_play
re-validates — so the local result is an advisory accelerator only.

- backend: Registry.DictBytes + an authed GET /api/v1/user/dict/{variant}/{version}
  (immutable) streaming the pinned per-game dawg; caddy routes /dict to the gateway.
- gateway: a session-gated /dict edge route proxying it; fetchDict on the transport.
- client: the dictionary loads on game open (low priority so it never starves the
  game on a slow link; aborted at a 5s cap or when leaving the game), is cached in
  IndexedDB (best-effort, self-healing on a rejected blob) and reused across
  sessions; a warm-up overlay covers a cold load, then the network preview is the
  fallback; a bad-connection breaker stops warming after repeated misses; the move
  preview cancels its in-flight request when the tiles change.
- parity generators backend/cmd/{dictgen,validategen} + gated Vitest suites, run in
  CI against the release dictionaries. A hidden debug readout lists the cached
  dictionaries + breaker state, and its reset clears the cache.
- docs: ARCHITECTURE §5, TESTING, UI_DESIGN, FUNCTIONAL (+ru).
2026-07-01 22:58:40 +02:00
developer f0399e1bbc Merge pull request 'build(ui): drop sourcemaps from prod bundle' (#153) from feature/ui-drop-prod-sourcemaps into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 57s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m16s
2026-07-01 15:00:44 +00:00
Ilia Denisov e6c5198caa build(ui): drop sourcemaps from prod bundle
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
The production UI build shipped `.map` files with full `sourcesContent`, and
the gateway/landing images serve `dist/` verbatim, so anyone could fetch
`/app/assets/main-*.js.map` (same assets under `/vk/`, `/telegram/`) and
reconstruct the entire TypeScript/Svelte source at the edge.

Gate `build.sourcemap` off for `mode === 'production'` (the Docker image build).
Dev and the `mock` e2e build (`vite build --mode mock`) keep maps for debugging.
Document the posture in ARCHITECTURE.md §12.
2026-07-01 16:57:05 +02:00
developer 84cc56198e Merge pull request 'feat(ui): paint VK status bar to the app theme (VKWebAppSetViewSettings)' (#152) from feature/vk-status-bar-chrome into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 57s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m17s
2026-07-01 12:39:11 +00:00
Ilia Denisov 5f9b4a7a38 feat(ui): paint VK status bar to the app theme (VKWebAppSetViewSettings)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 58s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m13s
Parity with the Telegram chrome painting: on a VK Mini App launch and on
every theme change, set VK's status-bar appearance (and, on Android, the
action/navigation bar colours) from the app's live theme tokens, so the VK
chrome matches the UI instead of clashing.

syncVKChrome mirrors syncTelegramChrome; the status-bar appearance is
derived from the --bg token's luminance (appearanceForBg, unit-tested).
Wired into the VK onScheme handler (fires on launch + on theme change) and
setTheme. Docs: UI_DESIGN VK integration.
2026-07-01 13:54:14 +02:00
developer 4458f0e545 Merge pull request 'fix(ui): copy GCG to clipboard on Android in-app WebViews' (#151) from feature/gcg-export-android-fix into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 57s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m13s
2026-07-01 11:23:59 +00:00
developer 41f26da9a7 Merge pull request 'fix(game): clear nudges on game completion' (#148) from feature/nudge-clear-on-completion into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 17s
CI / ui (push) Has been skipped
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m15s
2026-07-01 11:23:39 +00:00
developer cf1d773c18 Merge pull request 'fix(ui): mobile WebView polish — tap-flash, VK haptics, VK swipe-back' (#149) from feature/mobile-webview-polish into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 57s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m14s
2026-07-01 11:23:29 +00:00
Ilia Denisov 88b6761e28 fix(ui): copy GCG to clipboard on Android in-app WebViews
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
On-device diagnostics from Android Telegram and VK confirmed both expose
no navigator.share AND no navigator.canShare, so the export fell to a Blob
<a download> that those WebViews silently ignore — nothing happened.

pickGcgDelivery is now a 3-way decision: Web Share where available (iOS),
a clipboard copy in an Android in-app WebView (Telegram/VK: no share, dead
download), else a desktop Blob download. shareOrDownloadGcg reports the
outcome so the game shows a "GCG copied" toast; the copy is VKWebAppCopyText
inside VK (which also covers the desktop VK iframe, where navigator.clipboard
is blocked) and navigator.clipboard otherwise.

Unit tests cover the 3-way choice and the copy/failed outcomes; new i18n key
game.gcgCopied (en+ru); docs ARCHITECTURE/FUNCTIONAL(+ru)/UI_DESIGN/TESTING.
2026-07-01 13:17:00 +02:00
Ilia Denisov f4dbb545e0 fix(ui): suppress tap-flash directly on the header title and back
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m30s
The global -webkit-tap-highlight-color on #app was not enough on Android
in-app WebViews (Telegram, VK): they only suppress the selection-like tap
flash when -webkit-user-select / -webkit-tap-highlight-color /
-webkit-touch-callout sit DIRECTLY on the tapped element, not inherited.
Set them on the two tappable header controls — the title (a 10-tap debug
target) and the back chevron.
2026-07-01 13:06:11 +02:00
Ilia Denisov 71c3411276 fix(ui): mobile in-app WebView polish (tap-flash, VK haptics, swipe-back)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
- Tap-highlight: add -webkit-tap-highlight-color: transparent on #app.
  Android in-app WebViews (Telegram, VK) flashed a momentary selection-like
  box on tappable nodes on tap — seen on the header title and the back
  chevron; user-select (already none) does not govern it. Inherited, so
  this clears it app-wide.

- VK haptics: mirror the Telegram haptic set on VK via VK Bridge taptic
  (impact / notification / selection), routed through a new shared
  lib/haptics.ts dispatcher. VK users previously got no haptics; the game
  and error call sites now fire haptic() instead of telegramHaptic().

- VK swipe-back: disable VK's horizontal swipe-back at launch
  (VKWebAppSetSwipeSettings history:false) so it does not fight the app's
  own edge-swipe-back and on-board tile drag — parity with Telegram's
  disabled vertical swipes; the app owns navigation via its back chevron.

Docs: UI_DESIGN (no-select / tap-highlight, VK integration).
2026-06-30 23:00:19 +02:00
Ilia Denisov f9acea1d9a fix(game): clear nudges on game completion
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Nudge badges lingered in the lobby on games that ended by turn-timeout,
resignation or forfeit: those paths commit the finish directly, bypassing
the move path's per-mover NudgeClearer, so the awaited seat's nudge was
never marked read. A finished game's nudges are stale, so clear them all.

Add a wired NudgeExpirer (social.ExpireNudges) called from the shared
commit finish block — covering every completion path — and from the
voidGame recovery path. It clears every seat's nudge bits for the game
and leaves chat messages unread; unlike ClearNudges it records no
publish-to-read latency, since a completion is an expiry, not a read.

An integration test reproduces the timeout case (nudge cleared, chat
kept). Docs: ARCHITECTURE §9.1, FUNCTIONAL (+_ru), backend/README.
2026-06-30 22:51:54 +02:00
developer adf7c55695 Merge pull request 'feat(ui): first-run onboarding coachmarks' (#147) from feature/onboarding-coachmarks into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 57s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m21s
2026-06-30 20:09:33 +00:00
Ilia Denisov 6636d7c309 feat(ui): first-run onboarding coachmarks
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
A one-time coachmark overlay walks a new player through the lobby and their
first game board: a light dimmed layer draws one tail-pointed hint bubble at a
time, advancing on a tap anywhere and removing itself for good after the last
hint. Two independent series (lobby: settings/stats/new game; game:
header/pass-exchange/hints/shuffle/rack), gated by a per-device persisted flag
and marked done only after the last hint, so an interrupted run replays from
the start. A deep-link into Settings -> Friends still triggers the lobby series
on the first trip back to the lobby.

Targets carry a data-coach attribute, so one positioning engine anchors the
bubble in both portrait and landscape, re-measuring each frame until the
geometry settles (route slide, hidden-banner reflow, fonts). The promo banner
hides while the overlay is up (app.coachActive); a hidden DebugPanel "Reset
visited" control replays the walk-through. Off by default in the mock build so
the Playwright smoke is unaffected; ?coach forces it on for the dedicated e2e.

Pure geometry (step lists, nextVisibleStep, placeBubble) in lib/coachmark.ts
(unit-tested); Coachmark.svelte renders. Docs: FUNCTIONAL(+ru) onboarding
story, UI_DESIGN coachmark section.
2026-06-30 21:48:56 +02:00
developer 90f2427fa7 Merge pull request 'feat(assets): VK loading-screen Lottie — bouncing Erudit tile' (#146) from feature/vk-loader-lottie into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Has been skipped
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m28s
2026-06-30 16:37:26 +00:00
Ilia Denisov d0f860a33e feat(assets): VK loading-screen Lottie — bouncing Erudit tile
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m30s
A 96x96 transparent Lottie for the VK app loading screen: the «Э» tile
(score 8) drops in under gravity, lands with a soft cushion squash
(left/right edges bulge convex, corners follow via a chordal spline),
springs back and loops (~1.1s). A warm glint is caught on the bounce.

Pure 2D shapes (ddd:0) for player compatibility; ~11KB (<24KB limit).
Glyphs are real LiberationSans outlines (Arial-metric, matching the
game's font stack). Includes a reproducible, dependency-light build
pipeline (build/generate.js + extract.js + glyphs.json) and a README.
The preview GIF's green-baize background is preview-only; the asset
itself is transparent.
2026-06-30 18:29:51 +02:00
developer d1ceccf033 Merge pull request 'docs: sync FUNCTIONAL/UI_DESIGN/.claude with the shipped VK + landscape behaviour (#140-#142 follow-up)' (#145) from fix/vk-docs-deeplink-and-pan into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Has been skipped
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m19s
2026-06-30 06:07:37 +00:00
Ilia Denisov 373aa2aa6d docs: sync VK theme-follow + home-bar safe-area into FUNCTIONAL/UI_DESIGN/.claude
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
The #142 polish shipped three VK behaviours the docs never picked up (they were
written for the #140 auth MVP):

- the "auto" theme follows the VK client light/dark (VKWebAppUpdateConfig), not
  the VK webview's prefers-color-scheme;
- the layout clears the VK mobile home bar via CSS env() max'd with the VK bridge
  insets (VKWebAppUpdateInsets) — the bridge value is needed on Android, where the
  VK webview exposes no env() inset (.claude said env() handled it alone);
- share/copy route through the bridge (VKWebAppShare/VKWebAppCopyText).

FUNCTIONAL (+_ru) VK paragraph gains theme+home-bar parity with the Telegram one;
UI_DESIGN gets a VK-integration note beside the Telegram one.
2026-06-30 08:05:38 +02:00
Ilia Denisov 4a6fe318be docs: correct VK invite-link reality + note desktop board drag-to-pan
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Two doc edits that did not land alongside their code:

- FUNCTIONAL (+_ru) claimed a friend invitation is shareable as a Telegram
  *or VK* deep link "that opens it directly". On VK that is false: vkShareLink
  emits a plain vk.com/app<id> link (VK forwards no payload to the Mini App),
  so the recipient enters the copied code by hand (#142, commit d76f1f4 pivoted
  off the earlier #f<code> plan; the doc kept the old wording).
- UI_DESIGN omitted the desktop / landscape-iframe mouse drag-to-pan of the
  zoomed board added in #141 (touch scrolls the viewport natively; a mouse
  cannot, so an explicit drag-to-pan handler moves it).
2026-06-30 07:51:11 +02:00
developer 00441e3657 Merge pull request 'chore(ci): bump dictionary test-suite seed to v1.3.1' (#143) from chore/dict-seed-v1.3.1 into development
CI / changes (push) Successful in 2s
CI / integration (push) Successful in 14s
CI / deploy (push) Successful in 1m16s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / unit (push) Successful in 9s
CI / ui (push) Successful in 55s
CI / gate (push) Successful in 0s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
2026-06-30 05:29:44 +00:00
developer 3b7c7c077e chore(ci): bump dictionary test-suite seed to v1.3.1
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
The scrabble-dictionary release v1.3.1 drops the obscene lexicon (mat).
Bump the centralised CI seed so unit/integration jobs validate against the
same dictionary the prod deploy now pins (vars.PROD_DICT_VERSION=v1.3.1).
2026-06-30 05:27:42 +00:00
developer e900d592f8 Merge pull request 'feat(vk): native share/copy, auto theme, friend-code deep link, home-bar safe area' (#142) from feature/vk-bridge-integration into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 56s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m25s
2026-06-29 21:04:53 +00:00
Ilia Denisov d76f1f4026 fix(vk): friend-code link is the plain vk.com/app link (VK strips iframe query payload)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m33s
The contour diagnostic confirmed VK forwards only the signed vk_* params to the iframe — a
custom payload on the app link is dropped (the '#' eaten by the vk.com SPA, a '?' query
stripped), so the friend-code cannot ride the link. The VK share link is now just
vk.com/app<id>; the recipient enters the copied code by hand (VKWebAppCopyText works). The
vkStartParam reader + bootVK routing stay as a no-op, ready for a post-moderation channel.
Removes the temporary deep-link diagnostic.
2026-06-29 22:58:43 +02:00
Ilia Denisov 0ea9764a0d chore(vk): temporary deep-link diagnostic (remove after contour catch)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s
2026-06-29 22:41:08 +02:00
Ilia Denisov 6a5ce12fab fix(vk): friend-code link as ?hash, Android safe-area via bridge insets, landscape home-bar colour
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
Contour review of the VK Bridge group:

- #6 invite link: VK's documented '#' direct-link payload is eaten by the vk.com SPA before it
  reaches the app, so the friend-code link now carries the payload as a query param
  (vk.com/app<id>?hash=f<code>); the recipient already reads the `hash` query param (vkStartParam).
  (Whether VK forwards the '?' through to the iframe is being confirmed on the contour.)
- #8 Android: the VK mobile webview does not surface the home-bar inset via CSS env() (config
  insets are iOS-only), so subscribe to the bridge insets (VKWebAppUpdateConfig + VKWebAppUpdateInsets)
  and set --tg-safe-* to max(env(), the VK value).
- #8 landscape colour: the home-indicator strip was the (grey) page background because the two-pane
  landscape game has no bottom bar. The left-panel controls bar now paints its own chrome into the
  inset (Screen gains a selfInset flag that drops the shell's detached padding strip), and the
  game-land runs flush to the edge.

Verified: svelte-check, 347 unit, build, bundle-gate; the landscape safe-area painting reproduced in
the mock (controls bar + board reach the edge, strip takes the bar colour). The VK-Bridge / VK launch
behaviours (Android insets, the ?hash forward) need the live contour.
2026-06-29 22:15:43 +02:00
Ilia Denisov da17c18895 feat(vk): native share/copy, auto theme, friend-code deep link, home-bar safe area
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m25s
Group B of the VK integration — the contour-verified follow-up to the launch+auth MVP:

- Share/copy inside the VK iframe go through VK Bridge: the friend-code invite shares via
  VKWebAppShare and copies via VKWebAppCopyText, since navigator.share is absent in the desktop
  iframe and navigator.clipboard is blocked there.
- The invite link is a VK Mini App direct link (vk.com/app<id>#f<code>) on VK instead of the
  Telegram link; the app id comes from vk_app_id in the launch params (no build arg needed). The
  recipient's launch routes the deep link from VK's `hash` launch query parameter.
- The app's "auto" theme follows the VK client's light/dark appearance (VKWebAppUpdateConfig),
  which the VK mobile webview's prefers-color-scheme does not track.
- The safe-area CSS vars default to env(safe-area-inset-*), so the VK mobile layout clears the
  home bar (and Capacitor/PWA too); Telegram still overrides them from its SDK.

vk.ts adds vkAppId/vkStartParam/vkShare/vkCopyText/vkOnScheme. Verified: svelte-check, 347 unit
(+ vkAppId/vkStartParam/vkShareLink), build, bundle-gate. The VK-Bridge behaviours need the live
contour (not reproducible headless).
2026-06-29 21:27:36 +02:00
developer 303348ed39 Merge pull request 'fix(ui): landscape board zoom/pan + rack tile rendering + mobile block label' (#141) from fix/landscape-board-ui into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 56s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m31s
2026-06-29 19:11:05 +00:00
Ilia Denisov a06dfe00fc fix(ui): pin the landscape confirm button into the freed tile slot
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m24s
Follow-up to the contour review: with the rack now exactly seven fixed-size slots in the
narrow landscape panel, the 56px confirm button (sized for the roomy portrait rack) was
wider than the single slot a staged tile frees, so it overlapped the now-rightmost tile.
Match the button width to one landscape tile slot, so it sits inside the freed slot with
its right edge level with the rack's right edge — the mirror of the first tile's left edge.
2026-06-29 20:54:44 +02:00
Ilia Denisov a88678c5c6 fix(ui): landscape zoom two-step (sync both axes) + fixed rack tile size
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 2m35s
Follow-up to the contour review of the landscape fixes:

- Zoom still positioned "in two steps": a wide viewport overflows vertically as soon as
  the square board grows past its height, but horizontally only once it grows past the
  (much wider) width — so the browser pins scrollLeft to 0 while the vertical axis
  already pans (no time/ease tween fixes it; the horizontal scroll range physically is
  not there yet). Found by instrumenting the scroll trajectory. Now drive both axes by
  one progress = how far the board has grown past the viewport width (when a horizontal
  pan first becomes possible): until then the board just zooms centred, past it both
  axes pan together in one diagonal motion. Also disable scroll-anchoring so the browser
  stops fighting the programmatic scroll mid-transition. Re-verified: both axes now
  start and move together.
- Rack tiles resized when a tile was placed: landscape used flex-grow, so removing a
  tile regrew the rest. Give them a fixed size (1/7 of the fixed-width rack), like the
  portrait rack, so placing a tile leaves the rest put.
2026-06-29 20:26:01 +02:00