d8d1b06eeefb66ce184730cb8ea1345b5090bb60
794 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1e087be90a |
fix(offline): hotseat finished-game medals by final score (fix all-last-place on a tie)
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
Reported: a hotseat game ended by 6 scoreless passes deducts each rack (correct rule, same as online: -8/-14/-8 here), which tied two seats for the lead. The engine's winner() returns -1 on a tie, so seatMedal read it as a full draw and gave EVERY seat the last-place medal. - result.ts: seatMedal now ranks by the FINAL score (competition ranking — a tie for the lead shares the trophy), not the single-winner flag. A resigned / host-excluded seat places last with no medal and does not push the others down. - model.ts: Seat.resigned (offline-only); engine.resignedOf getter; the local source surfaces it on the game view. - The scoreless rack deduction is unchanged (standard rules, owner-confirmed). |
||
|
|
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. |
||
|
|
c1ac77bc6a |
fix(offline): unify NewGame layout to natural-flow + PinPad verdict pause
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 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
- NewGame: all three forms (quick / friends / hotseat) now use the Profile sub-view's natural-flow .page (no forced height, no pinned-CTA .grow/.fg scroll), so the screen's single .content scroll + --vvh handle overflow and a focused name input scrolls into view cleanly — fixes the keyboard blank-space / deep-scroll regression. One layout rule, one place. - PinPad: pause 250ms after the 4th digit before the verdict, so the fill (and the shake on a wrong PIN) reads as a deliberate response. - e2e: typePin waits for empty dots before typing (robust to the pause). |
||
|
|
cafe67e9c8 |
fix(offline): hotseat form — natural scroll + focus-into-view (keyboard)
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 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m38s
The nested scroll region regressed the keyboard behaviour (a double scroll: the whole screen scrolled with a huge blank area under the Start button, and a focused bottom row hid behind the keyboard). Drop it: the hotseat form now flows naturally and scrolls with the screen's own scroll (.page.scrollform: no forced full height, no pinned button), and a name input scrolls itself into view (centred above the keyboard) on focus. |
||
|
|
3adc4e32c9 |
fix(offline): hotseat review fixes + PWA stale-shell (SW route order)
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 1m44s
Owner review of the pass-and-play PR: - Roster delete (bug): a correct host PIN now ARMS a delete cross next to the row (mirroring the lobby delete) instead of removing it silently; tapping the kebab again clears the host authorisation. - Variant picker (UX): replace the dropdown with the Quick-Match variant plaques + the multiple-words toggle, one-to-one. - Keyboard layout (UX): the roster sits in a scroll region with the Start button pinned (friends-form pattern), so a name input's soft keyboard shrinks the region instead of leaving a full-height blank spacer painted behind the keyboard. - e2e: variant via plaque + a row-delete (arm then cross) regression step. PWA stale-version (pre-existing, same PR): - sw.ts: register the network-first NavigationRoute BEFORE precacheAndRoute. Workbox matches in registration order and the precache route (directoryIndex index.html) shadowed the shell navigation, serving it cache-first — the old build's __APP_VERSION__ until a second load. Fixes both the maintenance self-reload and a cold open after a deploy. Doc updated (ARCHITECTURE). |
||
|
|
903de7d41d |
fix(offline): unlock finished hotseat games + raise entry budget to 120
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 1m5s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m47s
- source: a finished hotseat game is never locked (its final rack is shown for review, not an Unlock button); guard locked on !isOver. - bundle-size: raise the app-entry budget 115 -> 120 KB for the hotseat UI (PIN pad, roster, host menu), which lives in the always-loaded New Game / Game / Lobby screens; the offline engine + PIN hashing stay lazy. |
||
|
|
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. |
||
|
|
66278e34b7 |
test(offline): e2e hotseat flow + fix $state-proxy persist
Add the Playwright mock e2e for offline pass-and-play (Chromium+WebKit): create a 2-seat hotseat with a PIN-locked seat, verify the rack is withheld, unlock (wrong then right PIN), host-skip the current turn, and terminate from the lobby behind the master PIN. The e2e surfaced a persistence bug: the roster PINs are $state proxies, which fail structured-clone on the IndexedDB put (silently — best-effort store), so a created hotseat game vanished on reload. Snapshot the seats + host PIN to plain objects at the create() call site. |
||
|
|
452a686e07 |
feat(offline): hotseat lobby delete behind the master PIN
Lobby.svelte: hotseat games show the kebab / swipe delete in BOTH the active and finished groups (not finished-only); deleting one opens a master-PIN pad first — active = terminate (no result), finished = remove — so the last mover cannot instantly wipe a game. Local games stay deletable offline. Non-hotseat games are unchanged (finished-only, free). |
||
|
|
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). |
||
|
|
6216359c6f |
feat(offline): hotseat creation roster + host-participate flow
NewGame offline 'with friends' now builds a local pass-and-play game: - lib/roster.ts: keep-last-valid name + roster->seats (unit-tested). - NewGame.svelte: master host-PIN gate (rows disabled until set), 'are you playing too?' prompt seating the host at row 0, 2-4 player rows with inline name validation + optional per-seat PIN, add/remove (remove behind the master PIN), variant + multiple-words, Start. - PinPad: verification via a verify(pin) callback (UI-held lock at creation, source-held in-game) instead of a lock prop. - i18n: hotseat.* (en + ru); the offline mode selector is now shown offline. |
||
|
|
8c67d679d9 |
feat(offline): hotseat record schema + source (locks, host actions)
Extend the local game to offline pass-and-play (hotseat): - serialize.ts: Seat.pin, record hotseat + hostPin (all optional; old vs_ai records read hotseat as false). - source.ts: create() takes hotseat/hostPin + per-seat pins; stateView reveals the seat-to-move's rack and withholds it (locked) when that seat is PIN-locked, until unlockSeat; ephemeral per-turn unlock re-locks on advance; new unlockSeat / verifyHostPin / hostAction (skip/resign/terminate); gameView sets vsAi=!hotseat + the hotseat flag. - model.ts: GameView.hotseat, StateView.locked (offline-only, optional). Tests: source.hotseat (lock/unlock/re-lock, host skip/resign/terminate, master-PIN gate, natural-end persistence) + serialize hotseat shape. |
||
|
|
69673e7727 |
feat(offline): PIN lock primitives + Apple-style keypad
Groundwork for offline pass-and-play (hotseat) seat/host PIN locks: - lib/pin.ts: salted SHA-256 PinLock (newSalt/hashPin/newLock/verifyPin); a social lock for a shared device (documented, not cryptography). - components/PinPad.svelte: 4-digit keypad (set/verify/change), auto-submit on the 4th digit, shake on mismatch, hardware-keyboard support. - i18n: pin.* keys (en + ru). Engine/source/UI wiring follows. |
||
|
|
d7f3d93c6c |
Merge pull request 'fix(router): sync route in navigate() — kill boot lobby-flash + offline e2e flake' (#210) from fix/boot-route-lag-e2e-flake into development
CI / changes (push) Successful in 1s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m4s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m39s
|
||
|
|
16cd3d0411 |
fix(router): update route rune synchronously in navigate()
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 1m4s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
navigate() wrote location.hash and left router.route to the asynchronous
hashchange event. bootstrap flips app.ready in the same tick right after
navigate('/login') on an unauthenticated cold start, so for one frame the app
rendered with app.ready=true and the stale route ('lobby', from the empty hash)
— briefly mounting the lobby shell (tab bar + a doomed games.list) under the new
login screen before hashchange settled it.
Update router.route synchronously inside navigate(); the later hashchange
re-parses to the same value. This removes the boot lobby-flash (and its spurious
games.list 401) and, as the root cause, the offline e2e flake: enterLobby could
latch that transient lobby tab-bar instead of clicking through login, then the
Settings-tab click at line 44 fought the tab detaching into the login slide
(~7% of runs, both engines).
Also:
- offline.spec enterLobby: wait for the guest button and click it, instead of a
point-in-time count() that a pre-login splash frame sampled as 0 (skipping the
click and hanging / latching the transient).
- New e2e router.spec pins the synchronous-route property (RED on the old code,
which returned the previous route; GREEN now) via a mock-only __router seam.
Verify: check 0 / unit 490 / build / bundle 114.3/115 / full e2e 200 / the
offline+router stress at 40x on both engines 160/160 (was 6/80 failing).
|
||
|
|
ef8a32bb82 |
Merge pull request 'feat(hint): unify the vs_ai idle hint online + offline (server-enforced, monotonic)' (#209) from feature/online-vsai-hint into development
CI / changes (push) Successful in 1s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 16s
CI / ui (push) Successful in 1m12s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m40s
|
||
|
|
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. |
||
|
|
ee7ff06403 |
Merge pull request 'feat(hint): idle-gated wallet-free vs_ai hint on a monotonic clock (B4)' (#207) from feature/offline-hint-gate 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 1m11s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m39s
|
||
|
|
537a265409 |
Merge branch 'development' into feature/offline-hint-gate
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 1m28s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
# Conflicts: # ui/src/lib/localgame/source.ts |
||
|
|
8c991429c6 |
Merge pull request 'fix(offline): zero tile weights on cold boot + wrong-mode game in the lobby' (#208) from fix/offline-alphabet-and-lobby 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 1m12s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m48s
|
||
|
|
359af83c34 |
fix(offline): zero tile weights on cold boot + wrong-mode game in the lobby
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 1m29s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
Two pre-existing offline bugs, both exposed once offline mode became usable (cold boot + toggling), owner-reported on the contour. Bug 1 -- offline tiles render weight 0. The rack/board read tile values from the lib/alphabet cache, populated only by the online wire codec (server-sent alphabet) or the mock (so the e2e masked it). A local game never goes through the codec, so on a cold offline boot with no prior online session the cache was empty and every value read 0 (scores stayed correct -- they come from the offline ruleset). Fix: the local source seeds lib/alphabet from the static offline ruleset (letters + values) when it builds a game view. Bug 2 -- the lobby showed (and could open) the other mode's game after a toggle. Two causes: (a) the lobby cache was not mode-tagged, so getLobby() instant-rendered the last snapshot regardless of mode; (b) load() wrote the module list after an await with no generation guard, so a slow online gamesList() finishing after a fast offline list() (or vice versa) left the wrong mode's games on screen. Fix: tag the snapshot with offline and gate getLobby() on it; add a load-sequence guard so only the latest load() writes. - localgame/source.ts: ensureAlphabet from the ruleset in gameView (+ unit test). - lobbycache.ts: LobbySnapshot.offline + getLobby(offline) mode check (+ tests). - Lobby.svelte: setLobby tags the mode; load() carries a generation guard. check 0 / unit 485 / e2e 198 / app entry 113.8/114. |
||
|
|
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).
|
||
|
|
08792dad3d |
Merge pull request 'test(offline): mock e2e for a device-local vs_ai game (C8)' (#206) from feature/offline-mock-e2e into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 11s
CI / integration (push) Successful in 16s
CI / ui (push) Successful in 1m28s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m42s
|
||
|
|
e9f4cb0178 |
test(offline): mock e2e for a device-local vs_ai game (C8)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
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 1m43s
A Playwright spec drives the whole offline flow in the mock build: force the installed-PWA display mode, enter offline via the Settings toggle (its readiness check fetches the dawgs), assert the blue chrome + online-games-hidden + Stats-disabled, then create and play a device-local English vs_ai game with a pinned bag seed (deterministic rack NEWYMAO) -- play the opening WAY across the centre, watch the robot reply with a real move, and reload to confirm the IndexedDB replay. Enabling infra (all e2e-only; nothing enters the production build): - mock/client.ts fetchDict serves the per-variant dawgs from the preview build's /e2edict/ (was: threw 'unsupported in mock'). - scripts/e2e-dict.mjs copies the real dawgs into dist-e2e from E2E_DICT_DIR (the ui CI job fetches the scrabble-dictionary release like the Go jobs; local default: the sibling scrabble-solver/dawg); playwright.config runs it between build and preview. - localgame/id.ts setForcedSeed + gateway.ts window.__mock.setLocalSeed: a mock-only seam to pin a local game's bag seed (tree-shaken from prod). - ci.yaml ui job: fetch the dawgs + pass E2E_DICT_DIR to the e2e step. - docs/TESTING.md: the offline e2e + the mock-dawg wiring. Verified: check 0 / unit 482 / e2e 198 (both engines) / app entry 113.8/114. |
||
|
|
8fbbb3c5ef |
Merge pull request 'feat(offline): gate the offline toggle on dictionary readiness' (#205) from feature/offline-toggle-readiness 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 1m10s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m41s
|
||
|
|
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 |
||
|
|
05c445e4da |
Merge pull request 'docs(offline): connectivity auto-detect + kill switch user story' (#204) from feature/offline-autodetect-docs into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Has been skipped
CI / conformance (push) Has been skipped
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m44s
|
||
|
|
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. |
||
|
|
350013acd9 |
Merge pull request 'feat(offline): mid-session flight-mode reactivity [PR2]' (#203) from feature/offline-flightmode 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 1m10s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m52s
|
||
|
|
3ad66f49c7 |
fix(offline): set the auto flag in setOfflineMode; remove temp diagnostic
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 1m51s
The auto-offline -> online return was dead because setOfflineMode never set the 'auto' flag: I added the state + getter but forgot the assignment, so it stayed false. So scheduleRecovery bailed immediately (no poll ran) and the online event's 'if (active && auto)' was false. The contour diagnostic confirmed offline.auto stayed false after an auto-offline. Add 'auto = on && !persist'. Also remove the temporary network diagnostic panel (netdiag + the lobby strip) -- it did its job of pinpointing this. |
||
|
|
09c5a5e72e |
chore(offline): TEMPORARY network diagnostic panel (remove before release)
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 0s
CI / deploy (pull_request) Successful in 1m44s
A fixed strip in the lobby showing live navigator.onLine / offline.active / offline.auto / connection.online plus a timestamped event log (offline/online events, poll ticks, checkReachable results, mode changes) - to pinpoint where the auto-offline -> online transition breaks on the contour. A 1s heartbeat logs navigator.onLine flips even if the events never fire. Skipped in the mock; to be reverted with the fix. |
||
|
|
fffc6030ce |
fix(offline): poll navigator.onLine to return online (the online event is unreliable)
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 1m24s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
The auto-offline -> online return still did not fire: the retry hung on the 'online' event, which an installed PWA often does not deliver. Replace it with a lightweight poll while in auto-offline that reads navigator.onLine (a reliable live flag, unlike the event) and only hits the network for a reachability check when the interface is actually up - so flight mode ON costs no radio, and flight mode OFF is detected within ~4s and returns online. The 'online' event, when it does fire, just kicks an immediate check. Runs only in auto-offline (deliberate offline is the player's choice); wired for both the mid-session and cold-start auto-offline paths. |
||
|
|
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). |
||
|
|
e0d28733ff |
feat(offline): mid-session flight-mode reactivity (auto-offline self-heals)
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 1m41s
React to the network changing while the app is open (e.g. the player toggling flight mode), via passive online/offline events - no polling, no battery cost: - interface lost -> enter offline mode for the session (auto); - interface back -> if the offline was auto, verify the gateway is really reachable (an interface being up does not guarantee it) and return online; a deliberate offline (the toggle or the cold-start dialog) is left as-is. - offline.svelte: track `auto` (auto-detected vs the player's deliberate choice). - connection.svelte: checkReachable is now a pure one-shot (the caller decides); the reachability watcher never probes in offline mode (events drive recovery). - transport.ts: the reachability probe is exempt from the kill switch - it IS the mechanism that decides whether to return online, fired only deliberately. - app.svelte.ts: initNetworkReactivity wires the events (web-only, skipped in the mock); called from bootstrap. Online unaffected (skipped in the mock e2e): e2e 196. Mid-session reactivity is contour-verified. |
||
|
|
ccd65f61b8 |
Merge pull request 'feat(offline): auto-detect no network at cold start + go-offline dialog [PR1]' (#202) from feature/offline-autodetect into development
CI / changes (push) Successful in 1s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m13s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m52s
|
||
|
|
5bc2ad3b6d |
feat(offline): auto-detect no network at cold start + 'go offline?' dialog
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 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
A sticky-online cold start with no network hung the splash on adoptSession's retrying profile fetch. Now, for an offline-capable web install with a cached profile: - No network interface (navigator.onLine === false) -> enter offline mode for the session (no dialog; the next launch re-evaluates). - Interface up but the gateway is unreachable within 3s (a single-attempt reachability probe, not the 6-retry loop) -> a 'No connection. Enable offline mode?' dialog: Enable -> sticky offline; Keep trying -> the normal online adopt (retries, 'Connecting...'). - connection.svelte: checkReachable(timeout) - a bounded single probe. - offline.svelte: setOfflineMode(on, persist) - auto-offline is session-only, a deliberate choice (dialog/toggle) is sticky. - app.svelte.ts: the cold-start auto-detect in bootstrap + the dialog resolver; App.svelte renders the boot dialog. i18n en/ru. - App-entry bundle budget 113->114 (the boot path cannot be lazy-loaded). Online cold-start unaffected (auto-detect gated to isStandalone, off in the mock e2e): e2e 196. The offline paths are contour-verified. Next: PR2 - mid-session flight-mode reactivity (online/offline events). |
||
|
|
2a4b0fb25e |
Merge pull request 'fix(offline): delete a finished local game from the device, not the network' (#201) from feature/offline-delete-game into development
CI / changes (push) Successful in 1s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m10s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m38s
|
||
|
|
2a7c632840 |
Merge pull request 'fix(pwa): network-first navigation so a new deploy loads online immediately' (#200) from feature/sw-fresh-online into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 14s
CI / ui (push) Successful in 1m24s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m43s
|
||
|
|
3a72bc29ba |
fix(offline): delete a finished local game from the device, not the network
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 1m41s
The lobby's finished-game delete (hide) always called gateway.hideGame; for a local (offline) game the transport kill switch refused it, so it toasted and the game stayed. Route by id: a local game is removed from the device store + the source cache (LocalSource.delete), an online game still hides on the backend. |
||
|
|
020742fad3 |
fix(pwa): network-first navigation so a new deploy loads online immediately
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 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
The C1 service worker served the precached shell for every navigation, even online, so a new deploy only reached a client after the worker updated its precache in the background (a one-launch lag): a cold online start showed the old version until then, and only a reinstall forced it fresh. - sw.ts: navigations are now network-first — fetch the fresh (no-cache) shell from the server on each online launch (it references the new hashed assets, fetched fresh), with a 3s timeout falling back to the precached shell when the network is unreachable or too slow. Only the tiny HTML is re-fetched; the immutable hashed assets stay cache-first and re-download only when their hash (the version) changes. Offline cold-launch still works via the fallback. - webui.go: serve sw.js with Cache-Control: no-cache so the browser reliably detects a new deploy's worker (regression-tested). Online launch is contour-verified (the mock e2e disables the SW); e2e 196. No new deps — the network-first handler is hand-written (~12 lines). |
||
|
|
fd225564a3 |
Merge pull request 'fix(offline): cold-boot offline from the persisted session + profile' (#199) from feature/offline-boot 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 1m9s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m41s
|
||
|
|
fdf14d5897 |
fix(offline): skip the NewGame friend fetch 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 1m40s
NewGame's onMount fetched the friend list (gateway.friendsList) for a non-guest; offline the transport kill switch refuses it, so it toasted on entering the New Game screen (the local game still created fine). The friends section is hidden offline anyway — skip the fetch when offline. |
||
|
|
1a456c4847 |
feat(offline): transport kill switch + gate the network-requiring UI
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 1m43s
Offline mode was 'fiction': the UI only disabled the Stats tab, but Profile was reachable and a profile save actually hit the server and reported 'saved'. Two layers now: - Transport kill switch (transport.ts): in offline mode every network op — each unary RPC (exec), the live stream (subscribe), the dict/metrics fetches and the reachability probe — is refused before it leaves the device. Offline truly means offline regardless of any UI that slips through. The local (device-only) game path never uses this transport, so it is unaffected. - UI gating: the Profile and Friends tabs (SettingsHub) and the Feedback entry (About) are disabled offline, and the hub falls back to Settings (which holds the online/offline toggle); the lobby Stats tab was already disabled. In-game social/export are already hidden for a vs_ai game. Online unaffected (offlineMode is off): e2e 196. Offline gating is contour-verified (the mock e2e cannot enter offline). |
||
|
|
2a045a5b37 |
fix(offline): give the local human seat the real account id
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 1m41s
In a local game the human seat's account id was a synthetic 'local:human:0',
so seatName's `accountId === session.userId` check never matched: the game
header rendered BOTH seats as 🤖 (the vs_ai fallback), and the lobby's
groupGames could not find the viewer's seat, so a human's turn read as
'Their turn' with the hourglass. Carry the real account id on the human seat
(create -> record -> GameView); the robot keeps its synthetic id.
Local games created before this fix keep the old display (no migration).
|
||
|
|
19e7ea5da0 |
fix(offline): persist a plain profile snapshot; keep the sticky offline flag
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 1m40s
The offline cold-boot never engaged: it persisted app.profile — a Svelte $state proxy — which is not structured-cloneable, so the IndexedDB write threw and fell back to a localStorage entry that loadProfile (IDB-only on a successful-empty read) never reads. loadProfile returned null, the boot short-circuit missed, and it even cleared the sticky offline flag — so offline mode stopped persisting across relaunches (owner-observed on the contour). - Persist $state.snapshot(app.profile) (a plain object) at both persist sites, so the IndexedDB write succeeds and loadProfile round-trips. - Drop the setOfflineMode(false) fallback: keep the deliberate offline flag on a cache miss (the mode is the player's choice; an online boot re-persists the profile so the next launch goes offline). A truly offline launch with no cached profile is unreachable (enabling offline needs a prior online session). |