feat(offline): implicit net-state model, two-tier version gate, unified lobby
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s

Land the offline-model redesign (ANDROID_PLAN.md O1-O7): replace the explicit offline toggle with a single detected net-state machine, unify the lobby, and add a two-tier client-version gate. Contour-safe: both version vars empty => dormant, the wire change is additive, so web/pwa/vk/tg behaviour is unchanged unless the gate is deliberately configured.

O1 net-state reducer (test-first). O2 store + wiring (connection/offline shims; +@capacitor/network). O3 remove the offline toggle + migrate the pref. O4 two-tier gate: hard update_required degrades to an offline Update/Play-offline notice (not terminal); soft GATEWAY_RECOMMENDED_CLIENT_VERSION -> X-Update-Recommended nudge. O5 unified lobby (device-local + greyed-from-cache server games; self-set identity; closes G-step-0). O6 create flows (with-friends online/offline segment + offline dict guard). O7 docs.

Telegram/VK stay online-only (contour review): the offline model is channel-gated via offlineCapable() (native + plain web; false in the mini-apps). offlineMode.active is hard-gated on it, so the whole model (blue chrome, unified/greyed lobby, transport kill switch, device-local vs_ai/hotseat create) stays inert in Telegram/VK regardless of the detected net state (closing a version-lock path that could have flipped them offline); and New Game's with-friends hides the online/offline segment there, leaving the remote invite alone. ARCHITECTURE already declared "Telegram/VK are exempt" - this makes the code match.

Deploy/CI: wire the version gate through the deploy (GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION as plain unprefixed vars via compose + ci.yaml + prod-deploy.yaml + write-prod-env.sh + .env.example) so the gate is live when set (test-contour commit-hash version fails open; empty => dormant). Disable the manual android-build CI workflow for now (rename .disabled). Bump the app bundle budget 127->130 KB for the added always-loaded wiring. Fix the UI Docker stage (gateway/Dockerfile): install --ignore-scripts so the Alpine/musl SPA build no longer tries to native-build sharp (a local android:assets tool, unused in the image); esbuild's binary is an optional dep so Vite still builds.

Tests: gateway go green (two-tier gate over a real HTTP handler); docker build of the landing + gateway targets green locally; compose --no-interpolate confirms the gateway env; two new telegram.spec.ts e2e (offline signal keeps the chrome online + quick-match opponent choice visible => server enqueue; with-friends shows no pass-and-play), RED-verified; svelte-check 0/0, vitest 617, e2e 248 (chromium + webkit), build + bundle-size 127.8/130.
This commit is contained in:
Ilia Denisov
2026-07-13 01:44:28 +02:00
parent 09e05eef18
commit 2d1fadb50c
55 changed files with 2065 additions and 976 deletions
+33 -23
View File
@@ -277,22 +277,28 @@ add-friend are off. AI games are **practice** — they never count toward a play
statistics.
### Offline mode
An installed web PWA signed in with a confirmed email can switch to a deliberate **offline mode**
(Settings → Play mode). Offline, the app never touches the network: the header turns blue with an
*Offline* chip, the lobby lists only the games stored on the device, and online-only surfaces are
disabled or hidden (the Stats tab; the *random opponent* option in New Game).
A New Game against the robot creates a device-local **vs_ai** game — it
plays entirely in the browser with no backend. Local games are kept on the device, visible only in
offline mode, and never sync to the account. So a game can be created and played with no connection,
the app quietly preloads the dictionaries for the player's enabled variants while still online, and
(once installed) launches from a precached shell even with no network. Switching **to** offline first
checks that the enabled variants' dictionaries are on the device — if any are missing it fetches them
and waits briefly, and if they cannot be readied it stays online with a short *needs internet* note
(the download keeps running in the background, so a later switch is instant). The mode is
device-scoped and sticky across launches.
The **web and native apps** play **offline automatically** — there is **no on/off switch**; the
online-only **Telegram and VK mini-apps** never go offline (everything below applies to the web and
native apps only). When it cannot reach the
server the header turns blue with an *Offline* chip and play is confined to games on the device; a
momentary blip is ridden out as a quiet *"Connecting…"* (it never drops you offline), and when the
connection returns the app goes back online on its own with a brief *back online* toast. Offline, the
app never touches the network.
Offline, New Game's **with friends** starts a **local pass-and-play** game for 2-4 people sharing one
device. You first set a **host (leader) password** — a 4-digit PIN entered on a lock-screen-style
The **lobby stays unified**: your device-local games are active, while your server games are still
listed but **greyed out** — visible but un-openable until you are back online (a tap explains why).
Online-only surfaces (the Stats tab) are disabled and invitations are hidden while offline. A New Game
against the robot creates a device-local **vs_ai** game that plays entirely on the device with no
backend; local games are kept on the device and never sync to the account, so a game can be created and
played with no connection. The app quietly preloads the dictionaries for the player's enabled variants
while still online, and (once installed, or on the native app) launches from bundled/precached data even
with no network; if a variant's dictionary is not available offline, creating that game is disabled with
a short note.
New Game's **with friends** offers a choice — **invite a friend** to play online, or a **local
pass-and-play** game for 2-4 people sharing one device; with no network only pass-and-play is available.
(In the online-only Telegram/VK mini-apps *with friends* is the friend invite alone — no pass-and-play.)
In pass-and-play you first set a **host (leader) password** — a 4-digit PIN entered on a lock-screen-style
keypad; until it is set the player rows stay locked. The app then asks whether you are playing too —
if so you take the first seat with your profile name. You name each player (2 to 4; add or remove
rows, where a removal asks for the leader password) and may give any seat its **own optional PIN**.
@@ -305,14 +311,18 @@ pass-and-play game. A game that finishes normally is saved to the offline lobby
deleting any pass-and-play game — running or finished — from the lobby asks for the leader password,
so no one can wipe a game the moment they have moved.
The app also **enters offline mode on its own** when it cannot reach the network. On a cold launch
with no connection it switches to offline for that session; when the device is online but the gateway
stays silent for a few seconds it asks first — *No connection. Switch to offline mode?*, with
**Switch** (go offline) or **Wait for network** (keep retrying online). While the app is open, losing
the network — airplane mode — switches to offline automatically, and restoring it returns online by
itself. A self-entered offline is temporary: it reverts to online the moment the network is back, and
the next launch re-checks. A **deliberate** offline — the Settings toggle, or **Switch** in that
dialog — is the player's choice: it is kept, and never undone automatically.
Going offline and coming back are both **automatic**. On a cold launch with no connection the app
starts straight in the offline lobby; while it is open, losing the network — airplane mode — turns it
offline on its own, and restoring the network returns it online by itself. There is no dialog and no
switch to remember: a brief drop is ridden out quietly and only a sustained loss turns the chrome
offline, so you are never interrupted for a hiccup.
### Staying up to date
When a new client version is required, the app does not lock you out. On the **native** app a
too-old build shows a notice with **Update** (opens the store) and **Play offline** (dismiss and keep
playing your on-device games); on the web it offers **Update** (reload) instead. A softer,
non-blocking **"update available"** banner appears in the lobby when a newer — but not yet
required — version exists; you can update or dismiss it, and play continues either way.
### Native app (Android)
The game also ships as a standalone **Android app** (first on **RuStore**), a packaged build of the same