ca2c6487cf78625599afa74d8d886b42666cdf81
123 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
de003e862a |
feat(android): resolve gateway origin on native, skip SW, hide MVP purchases
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. |
||
|
|
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. |
||
|
|
82648a4398 |
fix(game): show granted/bought hints in-game — finish the D31 wire removal
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 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m58s
The in-game hint badge ignored the payments hint wallet: loading a game clobbered app.profile.hintBalance with the deprecated StateView.wallet_balance (zeroed in the D31 domain removal but left in the protocol as a dead 0, then synced over the real balance at game load). Finish the removal honestly. StateView carries the per-game allowance alone (hints_remaining); the purchasable wallet lives solely on the profile and the client adds it (lib/hints). Removed StateView.wallet_balance across every layer — backend StateView/DTO, notify.PlayerState (live events), gateway StateResp + wire.StateView, the client model/codec/mock/localgame — and dropped the now-unused wallet arg from hintsRemaining. The FBS field is tombstoned `(deprecated)` (not deleted) so the vtable slots after it stay stable across a rolling deploy; no accessor is generated. HintResult keeps wallet_balance (the real post-spend payments balance the client adopts into the profile). The StateView type no longer has walletBalance, so the clobber cannot return without a compile error. Tests: hintsRemaining (2-arg); hints.hintsLeft (allowance + live wallet, no strip); the game state/hint integration (allowance-only HintsRemaining); gateway transcode; FBS regen. |
||
|
|
2ce80c241d |
fix(ads): fire the hint interstitial on the confirmed move, not on the hint
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
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 1m48s
Taking a hint fired the post-move interstitial immediately, when the hint's preview tiles landed — interrupting the turn and reverting the board to the rack on the ad's close while the hint stayed spent. Move the trigger to the move confirmation: a hint applied this turn marks it (hintUsedThisTurn), and the confirmed play fires the hint-kind interstitial (its own cooldown) instead of the plain move one; the marker clears on any turn boundary (applyMoveResult) so a hint-then-pass does not leak into the next move. e2e: taking a hint fires no ad; confirming a move does. |
||
|
|
13be7c3d9a |
feat(ads): VK post-move interstitial + retire deprecated hint_balance/paid_account
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s
Interstitial video after a confirmed play or a hint, VK-only, offline banner-only. The gate is client-mirrored: the backend puts the config cooldowns (global 5m / vs_ai 30m / hint 1m) and a suppressed flag (the no-ads / no_banner gate, same as the banner) on Profile.ads via adsFor; the client self-gates on a per-kind last-shown time in localStorage, with the VITE_ADS_STUB contour "ad fired" toast. Never fires after a pass, exchange or resign. maybeShowInterstitial + vkShowInterstitial; the codec and gateway transcode carry the ads block. Also retires the deprecated accounts.hint_balance / paid_account domain usage (expand-contract, code only — the columns stay for a later DROP so image rollback stays DB-safe): drop the Account fields and their scan, the dead account.SpendHint, account.GrantHints and the admin grant-hints action; the in-game hint display now comes wholly from the payments hint benefit. Banner eligibility and account merge no longer read the legacy flags. Tests: ads.test.ts (the client-mirrored gate), the codec ads round-trip, the gateway ads transcode test, and a profile ads-config integration test (cooldowns + suppressed under no-ads / no_banner). Docs: PAYMENTS §10 (+ru) interstitial, the decision amends, backend README, the plan. |
||
|
|
80cc2a76e8 |
fix(ui): unify tie-for-lead result across lobby + game (shared victory, not a draw)
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 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
Bring the online lobby badge and the in-game 'you won/lost' text to the same competition ranking the hotseat seat medals use: in a 3-4 player game a TIE for the lead is a SHARED victory for the top scorers and a placed finish for those below — not a full draw for everyone (winner() is -1 on any tie, so isWinner alone conflated them). An aborted game and a genuine all-level finish stay a draw; a win by resignation (winner at a not-higher score) is unchanged. - result.ts: resultBadge no-winner branch ranks by final score; placeBadge helper; resigned seats excluded from the ranking. - Game.svelte: resultText aligned (shared lead = won, below = lost). |
||
|
|
84d0385c95 |
fix(offline): scope the medal treatment to hotseat only
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) Successful in 1m7s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Per owner: a vs_ai game (one human) keeps its 'you won/lost' status text AND its lobby medal — the hidden-status + per-seat-medal treatment applies only to hotseat, where 2-4 local players make a single 'you' meaningless. isLocalGameId -> game.hotseat at the three call sites (statusBlock, seat plaque, lobby card). |
||
|
|
0ed34c7720 |
feat(offline): per-seat medals in local games; drop the you-won text + lobby medal
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 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
For a local (offline) game the outcome is not always about a single 'you' (hotseat is 2-4 players), so: - Game.svelte: a finished local game no longer shows the viewer-centric 'you won/lost/draw' status; instead each seat plaque shows a per-seat place medal, left of the name (result.seatMedal — trophy for the winner, then places by score; a draw medals everyone). - Lobby.svelte: a local game shows no lobby medal (resultBadge is viewer-centric and no seat matches the account) — its result lives on the in-game plaques. - result.ts: seatMedal(game, seat), unit-tested. |
||
|
|
52d0c559f9 |
fix(ui): local-game history (hide social, keep dictionary); Enter dismisses keyboard; email-code autosubmit
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 1m7s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
- History drawer in a local (offline) game: the seat plaques no longer show the add-friend/block controls — canAddFriend/canBlock now exclude a local game (hotseat seats have synthetic account ids that slipped past the vs_ai-only guard) — and the Dictionary entry is restored: an active hotseat game keeps the comms button, and CommsHub is Dictionary-only for a chatless vs_ai OR hotseat game (ChatScreen never mounts offline). - Enter on any single-line <input> now dismisses the soft keyboard (blur); a <textarea> (feedback) keeps Enter for newlines. One global handler. - Login email code: the friend-code spread-digit style (.codein), a 6-char cap, auto-submit on the 6th digit, and Enter to submit. - e2e: the local-game history has no social controls and keeps the dictionary entry. |
||
|
|
beda6ccd3d |
fix(ui): iOS soft-keyboard shell alignment + hotseat relock on return
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) Successful in 1m6s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
iOS Safari/WKWebView does not shrink the layout viewport for the soft keyboard (Android Chrome does): the visual viewport shrinks AND offsets down toward the focused field, and the values do not cleanly revert. The pinned shell tracked only the height (--vvh), not the offset, so its top-anchored content misaligned on iOS — empty space below the form, worst on a repeat focus. Fix (one place; covers NewGame, Chat, Feedback): - app.svelte.ts syncViewport: also mirror visualViewport.offsetTop into --vv-top, and scroll the focused field into view on the keyboard-open transition (iOS does not reliably scroll a pinned document to it). - app.css: the pinned app-shell body follows top=--vv-top + height=--vvh (was inset:0), so it stays on the visible area. - e2e/viewport.spec.ts emulates the visual-viewport resize+offset (a fake window.visualViewport) to verify the shell follows, on Chromium+WebKit. Hotseat: returning to the lobby now re-locks the current seat (its PIN is re-prompted) — LocalSource.relock, cleared in Game.svelte onDestroy. Root-caused a crash it exposed: a $props() value (id) reads back undefined during Svelte teardown, so isLocalGameId(id) threw and aborted onDestroy (breaking navigation) — read the id from the loaded view instead. |
||
|
|
4ddc690c38 |
feat(offline): hotseat in-game seat lock + host menu
Game.svelte drives a hotseat game: - the seat-to-move's rack is replaced by an Unlock button when the seat is PIN-locked (board stays visible); canMove gates the move controls on the unlock; PinPad(verify) -> source.unlockSeat reveals it. - the hint slot becomes a host button (hints off in hotseat), always enabled while the game runs (acts on a locked seat too). Master-PIN -> action sheet: skip current / exclude a player / end the game, each with a confirm + a fading check; terminate deletes and returns to the lobby. - per-turn advance re-fetches the next seat's (locked) state; self-resign and chat are hidden (hotseat resign is a host action, no comms). - gamesource: expose unlockSeat / verifyHostPin / hostAction on the local proxy. i18n hotseat.* (en + ru). |
||
|
|
53311cbc95 |
fix(hint): arm the vs_ai idle-gate from the warm cache so the lock shows without a delay
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 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
Entering a vs_ai game from the lobby armed the idle-hint countdown only in load() (after the gameState round-trip), so the lock popped in after a visible delay. Arm it on the instant warm-cache render in onMount too — the preloadGames-warmed StateView carries hint_unlock_left_seconds; load() then refreshes the snapshot. A first move (0 seconds left) stays open. |
||
|
|
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).
|
||
|
|
fc8143758a |
fix(offline): return online after flight-mode off; gate online-game actions offline
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 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m38s
Two mid-session issues found on the contour: - Auto-offline did not return to online after the network came back: a single reachability check right after the 'online' event failed (the interface is up before the gateway is actually reachable again). Retry a few times with backoff (tryReturnOnline) — that is what actually gets the app back online. - An online game viewed while offline (flight mode on) still enabled its network actions, so they hit the kill switch and raised 'something went wrong' toasts. Gate them: netReady = isLocalGame || (connection.online && !offlineMode.active) — a local game stays fully usable; an online game's make/exchange/hint/resign disable while offline and re-enable when back. Also suppress the 'offline' code in handleError (a blocked call in offline mode is expected, not a toast). |
||
|
|
5643c8be10 |
fix(offline): AI comms hub opens straight to the Dictionary, not via Chat
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) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
For an honest-AI (vs_ai) game the comms hub relied on a post-mount $effect to switch from the Chat tab to the Dictionary, so ChatScreen mounted for a beat and fired its chat/state fetch over the network. Online that was a wasted call; offline it threw and raised a 'something went wrong' toast on entering the word-check form (the check itself already worked). Start the comms hub on the Dictionary tab immediately for a vs_ai game, so ChatScreen never mounts. |
||
|
|
2f70ef1b85 |
fix(offline): route word-check through the game source; reload lobby on offline flip
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 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Two offline bugs found on the contour:
- The word-check screen (CheckScreen) called gateway.gameState/checkWord
directly, so in an offline (local) game it hit the network and errored
("something went wrong"). Route both through gameSource(id) — the local
source answers from the device dawg. The complaint control (online-only,
no offline backend) is hidden for a local game.
- The lobby did not react to the offline-mode toggle (which lives in Settings,
so the lobby can stay mounted): an online game lingered until the next
reload. Reload on an offlineMode flip so entering offline immediately shows
only device-local games.
Cold offline launch hanging on the splash (boot still fetches the profile over
the network) is the separate C2 offline-boot follow-up (needs a persisted
profile + a boot short-circuit).
|
||
|
|
1654131904 |
feat(offline): wire the local game source into the game screen (Phase B3.2)
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 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
The game screen now drives a local vs_ai game through the offline engine, dispatched by game id — completing the playable local game (on top of the source, #193). Online play is unchanged. - gamesource.ts: gameSource(id) returns the local source for a `local:` id, else the gateway (the same game-loop interface). The offline engine stays OUT of the app entry bundle — it is dynamically imported on first use (a separate chunk), so online-only users never pay for it (the app entry stays within its size budget). - localgame/id.ts: the tiny id helper (no engine imports) the dispatcher branches on. - Game.svelte: the game-loop calls (state/history/submit/pass/exchange/resign/hint/ evaluate/draft) go through gameSource(id) instead of the gateway directly; a local game's robot-reply events route through the same app event hub the network stream feeds, so the screen reacts to opponent_moved / game_over identically. Behaviour-preserving for network games (gameSource returns the gateway for them). Local verify green: check + test:unit + build + bundle-size gate + e2e (196 passed). |
||
|
|
4dfedd02a3 |
feat(ui): support old Android in-app WebViews (es2019 + core-js + engine screen)
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m5s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m54s
Old Android System WebViews (the Telegram/VK in-app browser; a real device with Google Play auto-updates the WebView, but emulators / no-Play / restricted devices stay frozen) showed a white screen. Two causes, fixed in order: - build.target es2022 shipped ?./?? verbatim, which the old engine cannot parse. Lowered to es2019 so esbuild down-levels the syntax. - The es2020+ runtime globals the bundle and its deps call (globalThis, structuredClone, Array.at, ...) are missing on those engines. A conditional core-js loader (emitPolyfills writes dist/polyfills.js, document.write'd by an index.html gate only when needed) covers them; modern engines download nothing, so the bundle-size budget is untouched. BigInt (the 64-bit FlatBuffers timestamp decode) and Proxy (Svelte 5 runes) cannot be polyfilled, so the effective floor is Chrome 67. Below it, a permanent ES5 boot guard in index.html shows a friendly "this device's OS or browser can't run the app" screen (with the web-version link inside a Mini App) and a "Diagnostic information" view with a Copy button, instead of a white screen. A reactive net raises the same screen on an uncaught boot error that never signals window.__booted. Board tile glyphs used container-query units (cqw, Chrome 105+) under .cell font-size:0, so they collapsed to 0px on Chrome 74 (invisible letters); added vmin fallbacks. The unsupported-engine telemetry beacon is a separate follow-up PR. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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. |
||
|
|
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). |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
500a01cf97 |
fix(ui): landscape board zoom/pan + rack tile rendering, shorter mobile block label
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 1m28s
Bugs surfaced while testing in VK but present on every platform (verified across browsers, not VK-specific): - Rack tiles: the letter used a fixed rem size, so in landscape — where tiles shrink below 46px in the narrow left panel — it overflowed and dropped into the bottom-left corner. Size it relative to the rack (cqw, like the board's labels; the tile's own container-query size resolves unreliably under flex + aspect-ratio). - Landscape board zoom positioned "in two steps": the per-frame clamp-to-max reached the wide axis before the tall one. Interpolate both scroll axes together by time over the grow/shrink transition instead (settles on zoom-out too). - The zoomed board could not be panned with a mouse (touch scrolls the overflow:auto viewport natively; a mouse cannot drag-scroll a div). Add a drag-to-pan handler, active only while zoomed, off pending tiles, past a small movement threshold, swallowing the trailing click so it does not also act on a cell. - Shorten the in-game block-confirm label ru "Блокируем?" -> "В бан?" (the long form overflowed the seat score chip on mobile). |
||
|
|
2ba7cc3086 |
feat(telegram): native dialogs for confirms and deep-link notices
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 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Inside the Mini App, route the destructive confirmations (resign, block, unfriend) through Telegram's native showConfirm, and present the deep-link info modals (stale invite, welcome-on-redeem) as a native showPopup whose button opens the bot chat. Outside Telegram, or on a client predating the dialogs, the existing in-app Modal is used unchanged; offline also keeps the modal so the action retains its disabled state. Adds showConfirm/showPopup wrappers to telegram.ts and a pure popup-params builder (nativedialogs.ts) with unit tests; the deep-link modal components choose native vs in-app via an effect gated on insideTelegram + dialog availability. |
||
|
|
6aa5023b24 |
fix(ui): Android Telegram nav — windowed mode, no close guard
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 1m18s
Three Android Mini App issues, all in the Telegram chrome: - Entering a game showed no native back button, and the Android system swipe-back minimised the app instead of navigating. Root cause: immersive fullscreen (requestFullscreen). On Android, fullscreen replaces the native header — and its BackButton, which also captures the system back — with a bare close/menu pill, so back navigation has no control to land on. Request fullscreen on iOS only; Android stays windowed, keeping the native header + BackButton (and the system swipe-back that routes to it). iOS is unchanged. - Closing the game board always prompted "changes that you made may not be saved", even on a board just opened and untouched. The close-confirmation was armed unconditionally on game mount. Remove it entirely: move drafts auto-save (debounced during play + flushed on destroy), so nothing is lost on close. Drops the now-unused telegramClosingConfirmation + isMobilePlatform helpers and their SDK interface fields. Docs: UI_DESIGN.md. |
||
|
|
62f66735a3 |
fix(ui): nudge the rack blank star up a pixel
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 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
By eye the centred star still read a hair low; subtract 1px from its top offset (top: calc(0.5% - 1px)). |
||
|
|
81680a1d5e |
fix(ui): raise the rack blank star to centre on the letters
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 1m18s
Top-aligning it still read a touch low; move the empty-blank star up (top 8% -> 0.5%) so its ink centres against the rack letters' block, matching the board tile's centred mark. Size unchanged. |
||
|
|
a393561d79 |
fix(ui): align the Erudit blank star with neighbouring tiles
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 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
Rack: the empty-blank star is now top-anchored level with the letters and a touch larger (was centred, sitting low). Board and Stats best-move tiles: the placed-blank star's ink is centred on the value digits' line (was slightly high). CSS-only nudges; pixel offsets measured against the rendered glyphs. |
||
|
|
e3e4cedc77 |
feat(ui): Erudit blank tiles carry the star (✻) mark
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 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
The Erudit variant's blank is the "звёздочка", so render it with a star. An empty rack blank (and its drag ghost) shows ✻ centred; a placed blank keeps its designated letter and carries ✻ where the (absent) point value sits — on the board and in the Stats best-move tiles. The Scrabble variants are unchanged. Gated by usesStarBlank() in lib/variants.ts. |
||
|
|
c127bc9f0e |
feat(social): per-game friend request to disguised robots + lobby/stats/tile cosmetics
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m25s
Functional: the in-game add-friend handshake aimed at an auto-match opponent who is secretly a pooled robot now records the request per (game, seat) in a new robot_friend_requests table -- never against the shared robot account -- mirroring the robot_blocks pattern. The shared account stays out of friendships, the "requested" state is pinned to the seat (not leaked across the player's other games), the robot ignores it, and a background reaper drops the row 7 days after its game finishes. The outgoing-requests list carries these per-game rows so the seat control stays disabled across reloads. No withdraw UI, per owner decision. Cosmetics: - Lobby: an in-progress game tints the viewer's own score number green when leading or tied, red when trailing (reusing --ok/--danger); scores now render in seat-number order, matching the over-the-board scoreboard. - Stats: the per-variant best move is laid out on two lines -- the variant label, then the score (right-aligned in its column) and the word tiles (left-aligned) below it. - Dark theme: the played-tile background is a touch darker so a player-placed tile reads with more contrast (light theme unchanged). Docs (ARCHITECTURE, FUNCTIONAL + _ru mirror, backend README, UI_DESIGN) updated in the same change. |
||
|
|
6e77de4c1e |
feat: sparser robot nudges, typed unread badge, lobby unread bump
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s
Three owner-requested polish changes: - robot: replace the lengthening 60-90 min -> 6 h proactive-nudge ramp with a flat uniform 9-12 h wait before every nudge; the existing sleep-window gate still skips and defers a nudge that would land in the robot's night. - ui: colour the lobby/in-game unread dot by type -- the regular danger colour when a chat message is unread, a softer amber (--warn) when only nudges are. Adds a per-viewer unread_messages flag (chat_messages.kind='message') across the backend DTO, FlatBuffers wire, gateway transcode and the UI store. - ui: float games with any unread notification to the top of the lobby's your-turn and opponent-turn sections (finished keeps its order), reusing the existing unread_chat flag. Docs (ARCHITECTURE 7, FUNCTIONAL + _ru) updated. No DB migration; the new wire field is backward-compatible. |
||
|
|
0dd4099d68 |
perf(ui): reuse hint result as move preview, skip redundant evaluate
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 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
Taking a hint auto-placed the suggested tiles and then called recompute(), which round-trips a debounced evaluate for that exact placement. The hint is the engine's own top-ranked, fully scored legal move, so its move already carries the score, words and direction an evaluate would return — the second call was pure duplicate work and added a visible disabled->enabled flicker on the submit button over slow links. Seed the move preview directly from the hint move via previewFromHint and cancel any pending evaluate timer so a stale one cannot clobber it. A later manual edit re-arms recompute() as before, so rearranged tiles are re-evaluated normally. Client-only; no backend, wire or schema change. |
||
|
|
b156d3403c |
fix(ui): polish board→rack recall drag and its gesture interplay
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
- The rack now animates the opening drop gap for a board-tile recall drag too
(the reorder transition was gated on a rack-source drag only).
- While a board tile is dragged over the rack (a recall, gap shown), the ✅
confirm and its preview caption are hidden so they don't sit over the gap;
they return if the tile is dragged back out over the board.
- A recall drop lands off the boardwrap, so its pointerup never reached the
boardwrap handler and left a stale id in the active-pointer set — the next
board swipe then read as multi-touch and the shuffle / history pull went dead.
Reconcile the pointer when a drag ends or cancels. Regression e2e added.
|
||
|
|
4adb608ad1 |
feat(ui): vs-AI comms trim, overlay confirm button, recall-to-slot drag
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 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
- vs-AI: the comms hub drops the Chat tab (Dictionary only); a finished AI game has no comms at all, so its 💬 history-header entry is hidden too. - Confirm-move ✅: redone as an absolute overlay pinned to the right edge (its right edge sits under the preview caption), so the rack keeps a fixed tile size — this reverts the tile-shrink that resized the letters at 6 tiles. - Recall: dragging a placed tile back to the rack now drops it at the slot the pointer is over (drag-to-position, a gap opens), instead of snapping to its original slot; double-tap still recalls to the origin. New pure recallToSlot. Tests: placement recallToSlot units; e2e recall-to-position; vs-AI comms e2e updated. Docs: UI_DESIGN + FUNCTIONAL (+ru). |
||
|
|
b5688d4848 |
fix(ui): right-align the confirm-move glyph under the preview caption
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 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
The ✅ box already ended at var(--pad) from the screen edge, but place-items:
center centred the glyph inside the 56px box, so it sat ~14px left of the green
preview caption (.scores, text-align:right at the same var(--pad)). Right-align
the glyph (place-items: center end) so its right edge lands under the caption's.
|