The no-repeat-words rule never fired in an online game. The move preview
builds its set of already-used words from the history the client holds,
and the codec upper-cases those words for display, while the evaluator
decodes the candidate word to lower — so "БРА" was never found to equal
"бра" and every repeat scored as an ordinary play, right up to the server
refusing it.
Offline was unaffected: that engine compares alphabet-index keys taken
from its own journal, which carry no case at all.
The tests missed it because they hand-built the played set in the form
the server sends rather than deriving it the way the game screen does.
They now go through playedWordsFromHistory with history records shaped as
the codec produces them — upper-cased words and all — which is the path
that was broken; three of the five fail without this fix.
A product title containing & < > " ' silently breaks VK purchases: VK
HTML-escapes the character in its order_status_change payment-callback echo
(e.g. " -> ") but signs the notification over a different form, so the
gateway's signature check no longer matches ("vk callback: bad signature")
and the purchase never confirms — while get_item (whose request carries no
title) still passes.
validateProduct now rejects those characters at the admin catalog editor —
the single write path for CreateProduct/UpdateProduct/SetProductActive — so a
title can't reintroduce the break. Reject, not transform: the operator retypes
a clean title (« » guillemets, not straight quotes), so the stored value stays
identical everywhere (storefront, wallet, VK box).
Docs: docs/PAYMENTS.md §7 + agent field notes; Go Doc on validateProduct.
Test: validateProduct HTML-special-char cases. No wire/schema change.
Reword every code comment and doc that referenced ANDROID_PLAN.md or its stage anchors (§E, G-step-0, O1) so the living docs no longer depend on the plan file: .claude/CLAUDE.md, deploy/README.md, docs/ARCHITECTURE.md, docs/TESTING.md, ui/README.md, ui/android/.gitignore, ui/e2e/native.spec.ts, ui/src/lib/netstate.ts.
Re-enable the manual signed-APK workflow (android-build.yaml.disabled -> android-build.yaml; the CI host already has the Android SDK; workflow_dispatch-only, so it does not auto-run on this PR). Add docs/ICONS.md — the single-master icon/logo format reference (web, PWA, Android, future iOS, store listings).
The earlier safe-area fix reached the bottom bars (they apply
--tg-safe-bottom) but not the header: its top inset lived only in a
Telegram-fullscreen-scoped rule, so on the native build the header .bar had
just 5px top padding and its content sat under the status bar (measured: the
title at y=11px behind the 54px bar; the back button untappable). Give .bar a
top inset from the SystemBars plugin's --safe-area-inset-top (native-only via
the plugin var; unset -> 0 on web/PWA/Telegram/VK, so no regression;
tg-fullscreen still overrides via specificity). Verified on-device (Pixel_10 /
API 37): the title moved from y=11 to y=65, clear of the status bar.
targetSdk 36 forces edge-to-edge, so the WebView draws behind the system
bars. On Android WebView < 140, env(safe-area-inset-*) wrongly reports 0, so
the app chrome (which only read env()) drew under the status bar (top nav
untappable) and the gesture-nav home indicator (the game's centre Hint button
intercepted; side buttons fine). Capacitor 8's SystemBars core plugin (built
into @capacitor/core, insetsHandling:'css' by default) injects the correct
--safe-area-inset-* values on every WebView; consume them ahead of env():
--tg-safe-*: var(--safe-area-inset-*, env(safe-area-inset-*, 0px))
Web / PWA / Telegram / VK are unchanged (--safe-area-inset-* is unset there,
so it falls back to env()). Verified on-device (Pixel_10 / API 37) via the
injected var — the emulator's auto-updated WebView 149 hides the env() bug,
so the visual alone won't reproduce it.
Native (Capacitor) cold boot with no cached session now enters as a
device-local guest in auto-offline mode and lands straight in the lobby
(never /login), so the app opens and plays local vs_ai / hotseat with the
APK's bundled dictionaries and zero network. When the gateway becomes
reachable, reconcileServerGuest silently mints + adopts a server guest and
clears the auto-offline, lighting up online features. Web / PWA / Telegram /
VK are byte-for-byte unchanged.
- transport: exec gains { silent, allowOffline } so background reconciliation
bypasses the offline kill switch (like the reachability probe) and never
raises the terminal update overlay (a too-old client stays a local guest);
new authGuestSilent on the client interface, the real transport and the mock.
- app: native no-session boot branch; reconcileServerGuest fired at boot, by
the recovery poll and the online event; the poll routes the session-less
guest through reconciliation, since checkReachable needs a token it lacks.
- native: initNativeShell tolerates a missing Capacitor bridge.
- e2e: new native.spec.ts (inject window.androidBridge; boot -> offline lobby,
local vs_ai move, reconcile -> online, hotseat start) + playwright.config
bundles the dawgs into dist-e2e/dict for the loader's bundled tier.
Make the web SPA behave correctly inside the Capacitor native shell, where the
bundle loads from a local origin:
- New lib/origin.ts gatewayOrigin(): absolute URLs resolve to VITE_GATEWAY_URL on
native (the finished-game export/share URL in Game.svelte and the Wallet
site-root link), falling back to the page origin on web. transport.ts already
resolved via VITE_GATEWAY_URL and is left as-is.
- Skip the PWA service worker on the native channel (the assets are already local;
a worker would risk serving stale content across store updates).
- Hide the money-purchase UI in the MVP: new distribution.purchasesHidden() folds
VITE_PAYMENTS_DISABLED and the Google Play flag. The Wallet "buy" tab shows a
neutral, pointer-free note (wallet.purchasesSoon) for the RuStore MVP and keeps
the RuStore stub Google-Play-only. A ?nopay mock force mirrors ?gp for the e2e.
- Native env types in vite-env.d.ts.
Client-only, contour-safe: no wire/proto/schema change; web/VK/Telegram unchanged.
Tests: origin + purchasesHidden unit tests, a ?nopay wallet e2e. svelte-check
clean, vitest green, web + native vite build clean.
Correct ANDROID_PLAN.md: bundled offline dictionaries come from the scrabble-dictionary release (DICT_VERSION), not scrabble-solver; pin the toolchain to Capacitor 8 (compileSdk/targetSdk 36, minSdk 24, JDK 21 — @capacitor/android compiles at Java 21). Add a Progress section marking the scaffolding milestone done, and capture the native-Android build recipe + toolchain gotchas in .claude/CLAUDE.md so a fresh session resumes without re-deriving them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
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.
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).
Mirror the Telegram Mini App wrapper for VK: the SPA loads at a new /vk/
entry, authenticates from VK's signed launch parameters, and provisions a
'vk' platform identity — the minimum to run the game in VK test mode.
- Gateway verifies the launch signature in-process (internal/vkauth:
HMAC-SHA256 over the sorted vk_* params under GATEWAY_VK_APP_SECRET,
base64url) — a pure offline check, no side-service. New auth.vk op
(gated on the secret), backendclient.VKAuth, /vk/ SPA mount.
- Backend: KindVK + ProvisionVK/vkSeed, /sessions/vk handler, identity
kind widened to include 'vk' (migration 00005, expand-contract).
- UI: src/lib/vk.ts (VK Bridge, lazy-imported), bootVK + the /vk/ boot
dispatch, encodeVKLogin + authVK across transport/client/mock. VK omits
the name from the signed params, so the client reads it via
VKWebAppGetUserInfo as an unsigned display seed.
- Deploy: /vk in the edge Caddyfile, GATEWAY_VK_APP_SECRET wired through
compose + .env.example + CI (TEST_) + prod-deploy (PROD_).
- Admin console: surface the VK user id (link to the VK profile) next to
the Telegram id on the user card.
- Docs: ARCHITECTURE §12/§13, FUNCTIONAL (+ _ru), gateway README; VK
integration reference under .claude/.
Signature algorithm verified against dev.vk.com plus independent Node/Python
references and a %2C edge-case vector.
The deploy-check skill is repo-specific: it encodes this project's hard-won
pre-deploy runtime constraints (distroless nonroot, edge Alt-Svc/HTTP3, caddy
recreate, DICT_VERSION boot, expand-contract migrations, the Telegram permission
model) and points at deploy/README.md, docs/ARCHITECTURE.md, docs/EDGE_HTTP3.md and
the agent memory files. It belongs with the deploy logic it guards, not in the global
config, so it travels with the repo and stays versioned alongside it.