v1.22.0
124 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1d306ec0fc |
chore(ui): change hints symbol for backward unicode compatibility
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m15s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
|
||
|
|
da25eac070 |
feat(link): auto-merge a guest initiator into its durable account (seamless sign-in)
When a guest links an identity (email/TG/VK) that already belongs to a durable account, the guest-primary rule already made the durable account the survivor and switched the session — but the client still showed an irreversible 'merge two accounts?' confirmation, which is nonsense from the user's side (they are simply signing into their account). The confirm step now merges inline for a GUEST initiator and returns the completed merge (the switched token); a durable initiator still gets the explicit confirmation (consolidating two real accounts is consequential). The active-game guard still refuses, surfaced as the clear error.merge_active_game_conflict message rather than swallowed. Also fixes the merged-away device-local games: their human/host seat was recorded under the retired account id, so after the switch the lobby and game header could not identify 'me' and showed every seat as an opponent (the game still played — turn logic is seat-index based). applyLinkResult now re-points local game seats from the retired id to the survivor (repointLocalGameSeats). Docs: FUNCTIONAL.md (+_ru). |
||
|
|
471fadc6a4 |
fix(ui): offline in-game — usable dictionary check + hide (not disable) resign/chat
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m15s
CI / conformance (pull_request) Successful in 12s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m5s
Two fixes from contour testing of the offline-in-game UX: - The dictionary word check was unusable offline (Check button disabled) when the panel was reached while already offline: CheckScreen fetched the variant + pinned version over the network in onMount, which fails offline, leaving the default variant so input sanitising stripped every letter. Seed both from the cached game (present once the board has opened) so the input and the on-device dawg fallback work without a round-trip; the network refresh still runs when online and on a cold deep-link. - The resign and chat controls were only functionally disabled (chat) or not gated at all (resign) offline, so they still looked active. Hide both while offline, matching the frozen social controls. Tests: the in-game offline e2e now asserts the drawer action icons are hidden (not disabled); a new e2e guards the offline dictionary flow. Docs updated. |
||
|
|
28afcff551 |
feat(ui): explicit offline state inside an online game (+ offline word check)
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) Failing after 12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 1s
CI / deploy (pull_request) Has been skipped
When an online game loses the connection the game screen now says so and freezes instead of silently greying out: a "connection lost" banner appears and the rack, the move controls, the add-friend/block controls and the chat/dictionary entry all disable (a started move stays a draft, committed by the player on reconnect). It is driven off the net-state machine (netState.offline), so it also covers the Telegram/VK mini-apps, where a lost connection was previously mute in-game. If the player is already in the dictionary when the drop happens, the word check falls back to the game's pinned on-device dictionary (exact when that dawg is cached; "unavailable offline" otherwise) and the network-only complaint + external look-up hide. Chat, when already open, keeps its existing read-only degrade (send/nudge disable). New pure helper localWordCheck is unit-tested; the in-game gating gets an e2e. |
||
|
|
3b485883ee |
feat(tg-bot): unpin auto-forwarded channel posts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m56s
Telegram non-disableably auto-pins each channel post it auto-forwards into the linked discussion group. The bot now detects that message by Message.is_automatic_forward in the moderated chat (TELEGRAM_CHAT_ID) and unpins it by id, so a pin set by a human admin — or by the bot for another message — is never touched (no unpinAllChatMessages). Needs the can_pin_messages right in the chat; the startup self-check now also warns when it is missing. Bot-only; no wire/schema/DB change. |
||
|
|
cc34622630 |
feat(vk): show a launch diagnostic on a direct /vk/ open instead of a guest
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 1m13s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m7s
Opening the dedicated /vk/ entry directly in an ordinary browser (no signed VK
launch) fell through to the web flow and silently started a throwaway guest,
which is wrong for a VK-only entry. Mirror the existing /telegram/ launch-error
behaviour: render a compact, shareable, privacy-safe diagnostic screen instead.
- Boot: a new branch `onVKPath() && !insideVK()` sets app.launchError and stops
the fall-through, the VK counterpart of the /telegram/ diagnostic guard.
- Diagnostic (passive, no VK Bridge round-trip): reads the URL launch-parameter
NAMES (never the `sign` value — auth material), whether the URL was signed,
`vk_platform`, the iframe/referrer context, plus the shared client-environment
lines. VK signs the launch URL at load, so — unlike Telegram's initData — the
parameters never arrive late; hence the VK screen offers Share only, no Retry.
- Generalise the screen: TelegramLaunchError.svelte -> LaunchError.svelte, which
renders a neutral pre-formatted report and a per-platform title, with Retry
gated on a `retry` flag (Telegram true, VK false). app.launchError is now a
neutral LaunchDiag { platform, report, retry }.
- New lib/launchdiag.ts holds the shared pieces both platforms reuse: the
LaunchDiag shape, the client-environment lines, and the query field-NAME
reader (moved out of telegram.ts so VK does not duplicate them).
Tests: pure vkDiagLines units, incl. a guard that the `sign` value never leaks
into the report. i18n: launch.errorTitleVk (en/ru). Docs: FUNCTIONAL (+ru) user
story and ARCHITECTURE entry-path note.
|
||
|
|
de5ab9186c |
feat(legal): bilingual (RU + EN) legal pages with a language + theme switcher
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s
Add English versions of the privacy policy, EULA and offer (ui/legal/*_en.md) and render each legal page as one self-contained bilingual document: both language bodies plus a header language toggle and theme toggle (no back), a small inline ES5 script that switches language client-side (no reload, default Russian + persisted) and applies the theme (system default + persisted override) — mirroring the landing. The offer's English view transliterates the Russian product names spliced from the live catalog. renderLegalHtml now takes both language sources; renderOffer splices the price list into both; the renderer bakes and reads the _en sources and pre-renders the static pages at boot. Also wrap the /offer/ contour probe in the same retry+timeout as the legal probe (the offer page is now bilingual and larger). Docs + renderer/ui tests updated. |
||
|
|
0f3b4dbcff |
feat(legal): host the privacy policy and EULA at /privacy/ and /eula/
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 40s
Author ui/legal/{privacy,eula}_ru.md, reworked from the source documents: the seller INN is unified (290210610742), contacts unified to the Telegram bot + email + postal address, the EULA governing-law clause leads with Russian law + jurisdiction as residence tiers, the single-binding-language clause is dropped, data collection is scoped to voluntary/support provision, and "Компания" is no longer shout-cased.
Serve both as static pages through the render sidecar, reusing a shared renderLegalHtml generalised from renderOfferHtml: new GET /privacy/ and GET /eula/ (301 from the slashless form), the markdown baked into the image, no backend fetch. Route them at the edge via the @legal caddy matcher with a CI probe asserting each page's canonical URL + the seller INN, so a missing route cannot silently fall through to the landing. Add the two links to the landing footer.
Docs baked in: ARCHITECTURE (renderer legal pages + edge routing), FUNCTIONAL (+_ru) footer, renderer/README routes. Tests: renderer legal.test.mjs, ui renderLegalHtml unit, landing footer e2e.
|
||
|
|
93ccc8c449 |
Merge pull request 'feat(telegram): add /support command with support-desk info reply' (#251) from feature/telegram-support-command into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 11s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / changes (pull_request) Successful in 2s
CI / integration (push) Successful in 20s
CI / ui (push) Has been skipped
CI / deploy (push) Successful in 1m44s
CI / integration (pull_request) Successful in 27s
CI / ui (pull_request) Has been skipped
CI / unit (pull_request) Successful in 10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
|
||
|
|
e0360c98e2 |
feat(telegram): add /support command with support-desk info reply
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m55s
Intercept /support in the main bot with a dedicated handler (registered like /start), replying with a fixed support-desk info message: the operators' working hours and what to include (a description and, when possible, screenshots). The reply is Russian or English by the sender's reported Telegram language, and the command is listed in the bot's command menu (localized). Being a dedicated handler, it intercepts /support before the support relay, so the command line itself is not forwarded into an operator topic while the user's following description still is. Update the telegram README and FUNCTIONAL.md (+ _ru mirror). |
||
|
|
2d1fadb50c |
feat(offline): implicit net-state model, two-tier version gate, unified lobby
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
Land the offline-model redesign (ANDROID_PLAN.md O1-O7): replace the explicit offline toggle with a single detected net-state machine, unify the lobby, and add a two-tier client-version gate. Contour-safe: both version vars empty => dormant, the wire change is additive, so web/pwa/vk/tg behaviour is unchanged unless the gate is deliberately configured. O1 net-state reducer (test-first). O2 store + wiring (connection/offline shims; +@capacitor/network). O3 remove the offline toggle + migrate the pref. O4 two-tier gate: hard update_required degrades to an offline Update/Play-offline notice (not terminal); soft GATEWAY_RECOMMENDED_CLIENT_VERSION -> X-Update-Recommended nudge. O5 unified lobby (device-local + greyed-from-cache server games; self-set identity; closes G-step-0). O6 create flows (with-friends online/offline segment + offline dict guard). O7 docs. Telegram/VK stay online-only (contour review): the offline model is channel-gated via offlineCapable() (native + plain web; false in the mini-apps). offlineMode.active is hard-gated on it, so the whole model (blue chrome, unified/greyed lobby, transport kill switch, device-local vs_ai/hotseat create) stays inert in Telegram/VK regardless of the detected net state (closing a version-lock path that could have flipped them offline); and New Game's with-friends hides the online/offline segment there, leaving the remote invite alone. ARCHITECTURE already declared "Telegram/VK are exempt" - this makes the code match. Deploy/CI: wire the version gate through the deploy (GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION as plain unprefixed vars via compose + ci.yaml + prod-deploy.yaml + write-prod-env.sh + .env.example) so the gate is live when set (test-contour commit-hash version fails open; empty => dormant). Disable the manual android-build CI workflow for now (rename .disabled). Bump the app bundle budget 127->130 KB for the added always-loaded wiring. Fix the UI Docker stage (gateway/Dockerfile): install --ignore-scripts so the Alpine/musl SPA build no longer tries to native-build sharp (a local android:assets tool, unused in the image); esbuild's binary is an optional dep so Vite still builds. Tests: gateway go green (two-tier gate over a real HTTP handler); docker build of the landing + gateway targets green locally; compose --no-interpolate confirms the gateway env; two new telegram.spec.ts e2e (offline signal keeps the chrome online + quick-match opponent choice visible => server enqueue; with-friends shows no pass-and-play), RED-verified; svelte-check 0/0, vitest 617, e2e 248 (chromium + webkit), build + bundle-size 127.8/130. |
||
|
|
eec225c4ee |
docs(android): bake the version gate, identity model + native build into the docs
ARCHITECTURE §2 (client-version gate + frozen wire contract + gate x offline), §3 (local-guest / server-guest / reconciliation identity), §13 (native Capacitor build). FUNCTIONAL (+_ru) a new "Native app (Android)" domain. TESTING (the clientver/gate Go tests, the native/update e2e + retry mapping, and the manual on-device Android smoke checklist). deploy/README (GATEWAY_MIN_CLIENT_VERSION + the wire-break bump discipline + the Android build/release runbook). ui/README native VITE_* vars. Mark F done in ANDROID_PLAN.md. |
||
|
|
b6c2598710 |
feat(offer): live catalog price list in the public offer, served by the render sidecar
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 26s
CI / ui (pull_request) Successful in 1m13s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 16m19s
Robokassa moderation requires the public offer to list every digital good with its price. Move /offer/ off the static landing container to the render sidecar: it splices the live catalog price list (§4.4) into the owner-edited ui/legal/offer_ru.md and renders it with the shared ui/src/lib/offer.ts — one renderer, no drift, always matching the current catalog with no redeploy. - backend: /api/v1/internal/offer/pricing (internal, off the edge allow-list) projects the active catalog into two markdown tables — chip packs priced per rail (roubles / VK votes / Telegram Stars) and chip-priced values — through payments.Money so no float reaches the page. Cached in memory: warmed at boot, marked stale on every catalog mutation, so a served render issues no query. - renderer: GET /offer/ fetches the tables and substitutes them at the <#pricing_template#> marker, then renders; offer_ru.md is baked into the image and marked is bundled from ui. GET /offer -> 301. Only /offer/ is edge-exposed. - caddy: route /offer/ to the sidecar; drop the now-dead landing /offer/ handlers and the vite emit-offer plugin. - offer: fill §4.3 (the chip-payment wording) and drop the in-page back link. - landing footer: a feedback link (the offer's Telegram contact) beside the offer link. - docs (ARCHITECTURE, FUNCTIONAL +_ru, renderer README), CI /offer/ probe, unit + integration + node tests. |
||
|
|
8ce986922a |
feat(ui): wallet storefront redesign — split buy/spend, compact balance, exchange confirm
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 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m55s
Reworks the Wallet screen to be more compact and to separate the two flows, and fixes a
reported bug.
Bug: buying a value with too few chips showed the generic "Something went wrong" toast — the
backend's `insufficient_chips` (409) code was propagated correctly but had no i18n mapping, so
`errorKey` fell back to `error.generic`. Add `error.insufficient_chips` / `error.product_not_found`.
Also disable a value's Exchange button up front when the spendable balance cannot cover it (the
server stays authoritative).
Redesign:
- a compact **balance** row leads the screen — the context's own chips (🪙 N) then any linked
other-platform chips behind that platform's logo (monospaced digits); inside a VK/TG store only
that store's own segment shows;
- the benefits section is renamed **Active**, moved above the store, shows one inline line
(hints + ad-free) and hides when nothing is active;
- the **store** splits by a two-way toggle into **Buy chips** (money packs + the watch-an-ad row at
the top) and **Spend chips** (values, exchanged for chips);
- a value's action reads **Exchange** and opens a single confirmation dialog (chip spends are
instant, with no provider window to confirm them); the existing cross-platform store-compliance
warning folds into that same dialog when the spend would draw VK/TG chips.
No payments-model or wire change; the money→chips→values model is unchanged (verified: money buys
only chip packs, values are chips-only — "no-ads for money" is structurally impossible).
Tests: wallet e2e updated for the new layout (chromium + webkit green). Docs: FUNCTIONAL (+_ru)
wallet story rewritten. App bundle budget 126 → 127 KB (always-loaded settings hub).
|
||
|
|
bf46b9492d |
fix(ui): one-word games must not highlight phantom cross words; review polish
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m25s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m2s
Board-highlight bug (reported on the contour): formedGeometry walked cross words
unconditionally, so in a single-word (one-word-per-turn) game a staged tile sitting
next to a committed tile lit up a green "cross word" the engine ignores — and which
need not even be a real word (the reported "БО lights up green in a one-word ПОПА
play"). Gate the cross-word walk on the game's multipleWordsPerTurn flag. The score
(8) was already correct — a premium square under the main word.
Also, from review:
- the turn strip reads the staged play's "WORD+WORD = N" while composing a legal move,
reverting to the turn / result text otherwise;
- the Exchange/Pass dialog shows the bag count ("In the bag: N" / "Bag is empty")
right-aligned in the title row, via a new optional Modal `titleAside`;
- cosmetics: half the turn strip's bottom padding (the plaques below carry their own
top pad); a top gap above the landscape rack (it sat flush under the docked history);
more horizontal padding on tab count badges so a 2-3 digit bag count clears the pill
ends;
- admin console: the game Summary now shows the single-word / multiple-words rule.
Tests: formed single-word case added; full unit (584) + e2e (chromium + webkit, 113
each) green; backend build + adminconsole templates parse. Docs (FUNCTIONAL +_ru,
UI_DESIGN) updated.
|
||
|
|
77a690fcf6 |
feat(ui): move in-game status to the board — highlight, score badge, full-width rack
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) Failing after 13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
Remove the under-board status strip and relocate its signals: - bag count -> a badge on the exchange/pass control + the foot of the move table - whose-turn / win-lose -> a thin strip above the score plaques - the tentative-move caption -> the board itself: staged tiles tint green (legal) or pink (illegal), the board tiles a formed word runs through go a shade darker, and an orange score badge sits on the main word (digit sized like a tile value, clamped on-board) The word geometry (covered cells + badge anchor) is a new pure client-side helper (ui/src/lib/formed.ts), independent of the move evaluator, so it works on the local or network preview path alike; the badge's number still comes from the preview score. Rack: a seven-column grid filling the tray width in both layouts — square, full-width tiles — with the confirm control in the fixed 7th slot. Settings: a touch-only "Zoom the board" toggle (default on, device-local) gates the tile-placement auto-zoom; taking a hint while zoomed in now zooms out so the highlighted hint word is never left off-screen. Docs (FUNCTIONAL +_ru, UI_DESIGN, ARCHITECTURE) and e2e/unit tests updated. |
||
|
|
3306a016a0 |
feat(ui): per-kind active-game limit lock on the New Game screen
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 28s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m46s
Carry the caller's per-tier active-game caps and each game's kind on the wire (Profile.game_limits + GameView.kind, additive FBS + gateway transcode + client codec, committed regen). The New Game screen counts the player's active games per kind from the lobby and locks a capped start: an outline button with a lock that opens a funnel modal instead of a game -- a sign-in prompt for a guest, a "finish a current game first" notice for a signed-in account (native Telegram popup, in-app modal elsewhere). The lock lifts via the existing profile refetch after a guest->durable upgrade. Remove the lobby's old at_game_limit New-Game tab disable + notice: the flag (now the random-kind cap) conflicted with the per-kind lock -- it hid the screen where the lock lives and wrongly blocked an unfulfilled kind. The New Game tab is always enabled; the per-kind start lock is the only gate. The at_game_limit wire field stays (unused by the client) for a later cleanup. Tests: client lock logic + codec kind/game_limits roundtrip + gateway transcode encode + native popup builders (unit); a mock e2e for the lock badge and the modal. |
||
|
|
4aa6174968 |
feat(payments): wallet screen with balances, benefits and storefront
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
Add the "Кошелёк" section to the settings hub: context-visible chip balances, active benefits (no-ads term/forever, hints) and a storefront of chip-priced values and money-priced chip packs. Guests have no wallet; the Google Play build hides the money purchases behind a RuStore stub; a web purchase that would draw VK/Telegram chips warns first. Add the catalog read path the storefront needs — a context-projected GET /api/v1/user/wallet/catalog (payments service + store, gateway op wallet.catalog, FBS Catalog/CatalogProduct/CatalogAtom, client decode) — plus the client leg for the existing wallet.get/buy ops. Value spends reuse the existing spend path; the chip-pack purchase (money order flow) arrives with payment intake, so its action is a disabled placeholder for now. Covered by Go unit (catalog projection) + integration (/wallet/catalog over Postgres), vitest (formatting, spendable selection, web-spend warning, GP flag, codec + gateway encode round-trips) and Playwright mock e2e (render, guest-hidden, GP stub, warning) on Chromium + WebKit. |
||
|
|
baa9961c3e |
docs(offline): document hotseat pass-and-play
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) Failing after 12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
ARCHITECTURE (offline section) + FUNCTIONAL (+_ru mirror): the local pass-and-play mode — host/referee master PIN, per-seat optional PIN locks (social lock, salted SHA-256, client-only), the board-visible seat unlock, host overrides (skip/exclude/terminate), no hints/chat, and the master-PIN-gated lobby deletion. |
||
|
|
7fc1301b31 |
feat(hint): unify the vs_ai idle hint online + offline (server-enforced, monotonic)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m28s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
Online vs_ai hints were broken: #207 made the vs_ai hint button always-enabled and wallet-free (assuming all vs_ai = the offline idle-gate), but the backend still served online vs_ai from the allowance/wallet, so over-clicking hit ErrNoHintsLeft -> a generic error toast. Now online vs_ai uses the SAME idle-gate model as offline. Backend: Hint() for a vs_ai game skips the allowance/wallet and increments no hints_used (owner: vs_ai counts toward no hint statistic), and is idle-gated from the SERVER clock -- it returns ErrHintLocked (code hint_locked) until the robot's last move + 30 min, else serves the top move. GameState/StateView expose hint_unlock_left_seconds (server-computed seconds remaining; 0 for a human game / first move / not-your-turn). Pure helper hintUnlockLeftSeconds unit-tested. Wire: StateView gains hint_unlock_left_seconds (FlatBuffers, additive); pkg/wire + gateway transcode carry it (round-trip test). Client: the gate counts down from a MONOTONIC clock (performance.now()) anchored to the source's seconds-left when it lands (on load from the view; to the full window when the robot moves), so a client clock change cannot skew it and a relaunch re-reads a fresh value. The vs_ai hint button (online + offline) shows the lock + toast; doHint handles the hint_locked backstop by re-syncing. Replaces #207's absolute hintUnlockAtMs on the wire/model/delta (the offline record keeps the absolute for persistence; the view exposes seconds-left). Docs FUNCTIONAL(+_ru)/ARCHITECTURE updated. Verified: go build/vet + game/server/transcode tests (+ new hintUnlockLeftSeconds); ui check 0 / unit 490 / e2e 198 (one pre-existing webkit offline flake) / app entry 114.2/115. |
||
|
|
ff486c80f8 |
feat(hint): persist the vs_ai idle-hint wait (wall-clock + read sanitiser)
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 1m28s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
Per the owner's call, the idle-hint gate now PERSISTS across leaving and reopening the app, instead of the session-scoped monotonic clock: the unlock is a wall-clock instant (hintUnlockAtMs) stamped from the robot's reply, stored on the local record, carried on the game view + the opponent_moved move delta, and read through a sanitiser that caps it at now + the window. So: - the wait survives a relaunch (a stuck turn is not forgotten); - a device clock set BACK cannot freeze the gate (the cap bounds the remaining to the window and self-heals on the next read); - a clock set FORWARD just opens the hint early -- accepted as harmless for a solo game. - lib/hints.ts hintGateRemainingMs now takes the unlock instant + clamps to the window. - localgame: re-add hintUnlockAtMs to the record/meta; stamp it off the robot's reply; sanitise on read (a shared hintUnlock helper feeds stateView + the opponent_moved event). - gamedelta + PushEvent: the move delta carries hintUnlockAtMs so the view stays fresh. - Game.svelte: hintUnlockAt derives from the view; the tick + lock + toast unchanged. - offline.spec.ts: also assert the lock survives a reload (the wait persisted). - Docs FUNCTIONAL(+_ru)/ARCHITECTURE updated (persist + sanitiser, not monotonic-resets). |
||
|
|
42a6308261 |
feat(hint): idle-gated wallet-free vs_ai hint on a monotonic clock (B4)
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 1m11s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
A vs_ai hint is now unlimited and wallet-free, but idle-gated as an anti-frustration
aid: it unlocks only after the player has been stuck ~30 min on a turn (timed from the
robot's last move; the human's first move, before the robot has played, is exempt).
While gated the hint button carries a small lock badge and a tap shows the remaining
minutes ('Available in N min.'); the lock lifts live at the mark.
The gate is enforced CLIENT-SIDE against a MONOTONIC clock (performance.now()), never a
wall-clock timestamp: a device clock the player controls (or an auto-sync) must not be
able to open or freeze it. The wait is therefore session-scoped -- a reload restarts it
(there is no tamper-proof way to carry idle time across a relaunch without a wall clock).
An online vs_ai game will gate the same way but from the server's clock (a follow-up).
- lib/hints.ts: HINT_GATE_MS + pure hintGateRemainingMs (monotonic) + hintLockMinutes;
removed the wall-clock hintRemainingMs. Unit-tested red->green.
- Game.svelte: the monotonic gate (hintGateStart/monoNow, armed on the turn change), a
vs_ai hint button (plain: no confirm, no count; lock badge + gated tap -> toast), a
live 10s tick.
- localgame: removed the wall-clock hint gate and the now-dead robotLastMoveAtUnix field
from source.ts/serialize.ts (the client is authoritative); hint() just serves the top
move.
- i18n game.hintLockedIn.
- offline.spec.ts: assert the first move is un-gated and the lock arms after the robot moves.
- Docs FUNCTIONAL(+_ru) + ARCHITECTURE; bundle budget 114->115 (game-screen feature).
|
||
|
|
30770a759b |
feat(offline): gate the offline toggle on dictionary readiness
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 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m44s
Flipping the Settings toggle to offline now checks that every enabled variant's dictionary is on the device before entering offline mode: it fetches missing ones cache-first and waits up to ~5 s (raceOfflineReady + the lazy dict/offlineready), greying the toggle meanwhile. If they cannot be readied in time it stays online and shows a 'needs internet' note, while the fetch keeps warming the cache in the background so a later flip is instant. Leaving offline is never gated. Prevents entering a half-baked offline mode (no dawg -> cannot create/play a local game) when the background preload has not finished (poor connection, or an immediate flip right after install). - offline.ts: raceOfflineReady (pure, injected sleep; unit-tested red->green) - dict/offlineready.ts: ensureOfflineDicts (cache-first preloadDicts, lazy chunk) - offline.svelte.ts: requestOffline + TOGGLE_READY_BUDGET_MS - Settings.svelte: checking/needsData state, disabled toggle, inline note - i18n: settings.offlineChecking / settings.offlineNeedsData (en+ru) - docs: FUNCTIONAL(+_ru) offline story + ARCHITECTURE offline paragraph |
||
|
|
d43a740eb6 |
docs(offline): document connectivity auto-detect + transport kill switch
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
The offline auto-detect shipped in #202 (cold-start no-connection dialog / auto offline) and #203 (mid-session flight-mode reactivity) but neither PR baked the docs. Add the user story to FUNCTIONAL.md (+ _ru mirror) and the mechanism to the ARCHITECTURE offline section: the auto vs deliberate distinction, the cold-start navigator.onLine / bounded reachability-probe decision, the mid-session window online/offline events backed by a navigator.onLine poll (the events are unreliable on some platforms, notably iOS PWAs), and the reachability probe being the one call exempt from the offline kill switch. |
||
|
|
ef832b823d |
feat(offline): offline lobby lists + creates local vs_ai games
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 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
In offline mode the lobby now shows only the device-local games and its New-vs-AI entry creates one through the in-browser engine — the visible payoff of the offline mode. - LocalSource.list() reconstructs a lobby GameView per stored local game by replay, exposed through the lazy gamesource proxy; unit-tested via an in-memory store. - Lobby.load() branches on offlineMode: lists local games and skips every gateway call (no online games/invitations/incoming); the Stats tab is disabled offline. - NewGame offline: find() creates a device-local vs_ai game via LocalSource.create using the profile's advertised dict version + a local seed; the friends flow and the random-opponent option are hidden, and the variant picker / Start are enabled offline (were gated on connection). - id.ts: newLocalGameId + randomSeed (tested). Docs: ARCHITECTURE + FUNCTIONAL(+ru) offline-mode section. Deferred to fast-follow: the Settings Friends/Profile/Feedback affordance gating, the flip-to-offline readiness wait, the offline mock e2e (needs mock-dawg support), and the local-hint UI. The offline flow is verified on the test contour — the mock e2e cannot enter offline mode (the toggle is gated to an installed PWA). |
||
|
|
061366da5a |
feat(email): PWA login sends code only; drop admin link from alert emails
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Two email changes, per the owner: 1. Code-only login from an installed PWA. A login requested while running as a standalone PWA now omits the one-tap confirm link from the email — the link would open in a separate browser whose minted session cannot reach the PWA, stranding the login. The code is typed in the same window instead. The client sends a `pwa` flag (isStandalone) on the email-code request (a new FBS field, threaded through the gateway); the backend omits the deeplink when it is set, reusing the existing deletion-code link-omission path. Non-PWA browser logins keep the one-tap link. No polling, no migration. 2. Security: the operator alert digest no longer embeds the admin-console (/_gm) URL. An admin link must never travel in an email, where a mail provider could cache or index it; the operator opens the console directly. Tests: an inttest asserting the PWA login email omits the link (and a browser one keeps it); a codec round-trip of the new pwa field; the alert-digest test flipped to guard the admin link is absent. Docs: ARCHITECTURE + FUNCTIONAL (+ru). |
||
|
|
51147a1429 |
feat(pwa): installable web app + landing web entry
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
Make the web SPA an installable PWA and surface it to users: - manifest.webmanifest + an install-only service worker + 192/512/maskable icons (ui/public); PWA head tags in index.html; the .webmanifest MIME type registered in the gateway (the distroless image has no /etc/mime.types). - Platform-adaptive install CTA (components/InstallApp.svelte + lib/pwa): one-tap on Chromium, manual Add-to-Home-Screen instructions on iOS Safari, hidden elsewhere / once installed / inside a Mini App. Shown under the logged-out login card and at the bottom of Settings. - The landing gains a third entry linking /app/ (the brand tile), with a caption under all three (Telegram / VK / Веб-версия). The service worker is navigation-only (network-first, cached-shell fallback); hashed assets and the Connect stream are untouched. It exists to satisfy Chromium's installability requirement and is the single growth point for a future opt-in offline mode. Tests: pwa.ts unit tests; a webui probe (manifest/sw.js content-type + the SPA-fallback-to-HTML trap); e2e for the one-tap CTA, the iOS instructions modal and the landing web entry. Docs: ARCHITECTURE §13, FUNCTIONAL (+ru), gateway README. |
||
|
|
6db9178449 |
feat(banner): per-campaign colour overrides and urgent alerts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
Non-default campaigns gain an optional colour override (background / text / link) in two sets — one for every theme, one for the dark theme only — and an "urgent" flag. - Colours ride profile.get as six trailing FlatBuffers strings on BannerCampaign (backward-compatible). The client resolves the cascade (dark <- dark ?? all, light <- all) per rendered theme and derives the strip border from the background in JS (no CSS color-mix, for the old Android WebView floor); AdBanner applies them as inline vars scoped to the strip. - Urgent is resolved entirely server-side: while any enabled, in-window urgent campaign exists, computeActiveSet returns only the urgent campaigns and bannerFor skips the eligibility gate — so a system notice reaches every viewer (paid / hint-holding / no_banner included) and preempts the ordinary feed. No wire field; it appears on each viewer's next profile.get. - Admin console (/_gm/banners): native colour pickers + a live light/dark preview of the strip, and an urgent toggle. The default campaign stays plain, enforced by the service and a DB CHECK. Migration 00009 is additive (nullable colour columns + a bool default + all-or-nothing / hex / default-plain CHECKs) — expand-contract, rollback-safe. Docs: ARCHITECTURE §10, UI_DESIGN, FUNCTIONAL (+ru). Tests: ads unit (urgent preempt + colour validation), codec + resolver unit, gateway transcode, and integration (colour round-trip + urgent bypass against real Postgres). |
||
|
|
c1805e5b7c |
feat(ui): hide current-host sign-in row in profile
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 1m6s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Inside a Telegram/VK Mini App the host provider is auto-linked. Once the player also linked an email, `canUnlink` turned true and the "Unlink" control appeared on the host platform's own row — letting them unlink the very platform they are signed in through, which is meaningless. Gate the Telegram row on `!insideTelegram()` and the VK row on `!insideVK()`, reusing the runtime host detectors that already gate the "link" buttons. Symmetric: inside TG only the TG row is hidden (the VK row still shows, since VK is not the current host), and vice versa. The web and native builds are unchanged (both detectors are false there); the backend is untouched — this is a UI display gate, and `linkUnlink` still refuses to remove the last identity. Docs: FUNCTIONAL.md (+_ru mirror). |
||
|
|
2c465c01d2 |
feat(account): VK ID web login to link a VK identity from a browser
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
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 1m39s
A browser has no signed VK Mini App launch params, so linking VK on the web uses
VK ID's raw OAuth 2.1 flow (PKCE, no @vkid/sdk): the SPA redirects to VK's hosted
login and returns with an authorization code, which the gateway exchanges
server-side (confidential, under the VK "Web" app's protected key) for the trusted
vk user id — then the existing link/merge machinery attaches or merges it.
- fbs LinkVKRequest{code, device_id, code_verifier}; codec + TS bindings.
- backend link.Service ConfirmVK/MergeVK/attachVK (KindVK, mirror Telegram),
handleLinkVK[Merge], routes /user/link/vk[/merge], backendclient LinkVK[Merge].
- gateway internal/vkid confidential code exchange (id.vk.com/oauth2/auth);
transcode link.vk.confirm/merge (registered only when configured) + config
GATEWAY_VK_ID_{APP_ID,CLIENT_SECRET,REDIRECT_URL} + main wiring.
- UI lib/vkid (PKCE authorize redirect + callback), Profile "Link VK" control,
boot callback handling; a merge re-authorizes for a fresh code (VK codes are
single-use). Web-only (a redirect strands a Mini App webview).
- Deploy: VITE_VK_APP_ID + VITE_VK_ID_REDIRECT_URL build args + gateway env,
ci.yaml/prod-deploy TEST_/PROD_ vars, compose/Dockerfile/.env.example/README.
- Tests: vkid exchange unit (string/number user_id, id_token fallback, errors),
transcode link.vk, backend ConfirmVK/MergeVK inttest, codec encodeLinkVK.
- Docs: ARCHITECTURE §4, FUNCTIONAL(+ru), gateway README.
|
||
|
|
c2a8426b74 |
docs: account deletion = legal retention, not erasure
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
FUNCTIONAL (+ru): the deletion user story — anonymised live surfaces ([Deleted]), freed sign-in methods, a two-year-retained dossier, the code/phrase step-up, game forfeit + all-robot drop, fresh account on reopen. ARCHITECTURE: the retention model (retained_identities journal on every detach, tombstone + [Deleted] sentinel, drop all-robot games, purpose=delete step-up, cold-load last-login, two-year TTL reaper). |
||
|
|
150660819a |
docs: sign-in methods matrix — unlink + change-email
FUNCTIONAL (+ru): drop the stale 'linking UI hidden' note; document the profile sign-in-methods matrix, the unlink last-identity guard, and the non-disclosing atomic email change. ARCHITECTURE: unlink + change-email behaviour and the purpose=change deeplink branch. |
||
|
|
1dca6741f1 |
feat(ui): auto-confirm the email deeplink on load
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) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
Drop the confirm button: the /confirm screen confirms the token as soon as it loads. The token rides the URL fragment (never sent to the server), so a plain link prefetch cannot reach it, and the manual six-digit code is the fallback if an aggressive scanner runs the page. Update the ARCHITECTURE/FUNCTIONAL wording accordingly and swap the confirm.prompt/action strings for confirm.busy (en+ru). |
||
|
|
65f2c87a74 |
docs+test(email): document the confirm deeplink + wire-contract tests
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
Document the one-tap confirm deeplink in ARCHITECTURE (§4 the login magic-link / link confirm+profile-refresh, prefetch-safe token) and the new notify 'profile' sub-kind (§10), and add the one-tap link to the FUNCTIONAL email story (+ru). Add codec round-trip assertions for the EmailRequest language field and encodeEmailConfirmLink (the mock e2e bypasses the codec). |
||
|
|
9e0f929e40 |
test+docs(email): squat-fix coverage and the email-pipeline docs
Add an integration test asserting the guest-until-confirmed squat fix (an email-login account is a reapable guest until the code is confirmed, then durable). Document the branded relay pipeline in ARCHITECTURE (go-mail, TLS by port, no client cert, PUBLIC_BASE_URL anti-injection, per-recipient send throttle, guest-until-confirmed), FUNCTIONAL (+ru), TESTING and the backend README config table (BACKEND_SMTP_* + BACKEND_PUBLIC_BASE_URL). |
||
|
|
d5fbaa3034 |
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
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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).
|
||
|
|
4458f0e545 | Merge pull request 'fix(ui): copy GCG to clipboard on Android in-app WebViews' (#151) from feature/gcg-export-android-fix into development | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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). |
||
|
|
65c194264c |
feat(vk): embed the game as a VK Mini App
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 1m0s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
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. |
||
|
|
29b6c7e4d8 |
docs(telegram): document the Mini App embedding enhancements
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
Record the current state in ARCHITECTURE and FUNCTIONAL (+ _ru mirror): inside the Mini App the client tracks Telegram's live theme switch, fits the full device safe-area, exposes a native Settings button into the in-app settings, and syncs the device-independent display prefs (theme, reduce-motion, board labels — not the interface language) across the user's Telegram devices via CloudStorage; the validator denies a bot user (is_bot) before provisioning an account. |