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 21s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 12s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 22s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 12s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 22s
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, and the wire change is additive, so web/pwa/vk/tg behaviour is unchanged unless the gate is deliberately configured. O1 pure net-state reducer (test-first). O2 reactive store + event wiring (connection/offline become thin 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 (ARCHITECTURE, FUNCTIONAL +_ru, TESTING, deploy/README). Also disable the manual android-build CI workflow for now (rename to .disabled); the Android APK release comes later. Tests: gateway go green, svelte-check 0/0, vitest 617, e2e 122/122 (chromium + webkit).
This commit is contained in:
+157
-63
@@ -695,12 +695,12 @@ Wire in `ui/android/app/build.gradle` `defaultConfig`:
|
||||
|
||||
### G. Release + owner handoff
|
||||
|
||||
0. **Land the offline-model redesign (design below — "Offline-model redesign").** This resolves the deferred
|
||||
native local-game-visibility decision (a native guest must see / resume their device-local games once online
|
||||
— today the online lobby hides them, `Lobby.svelte:42/54`) as part of a broader, owner-approved change:
|
||||
remove the explicit offline toggle, make offline an implicit **net-state machine**, unify the lobby, and add
|
||||
the **two-tier** version gate. Cross-cutting (web + native + a small additive backend/wire bit), its own
|
||||
staged work; the Android release waits on it.
|
||||
0. **Land the offline-model redesign — ✅ DONE (O1–O7, 2026-07-13; staged detail below).** Resolved the deferred
|
||||
native local-game-visibility decision — a native guest now sees / resumes their device-local games once online
|
||||
(the **unified lobby**, O5) — as part of the owner-approved change: the explicit offline toggle is gone, offline
|
||||
is an implicit **net-state machine** (O1/O2), the lobby is unified (O5), and the **two-tier** version gate is in
|
||||
(O4). Cross-cutting (web + native + a small additive backend/wire bit); **contour-safe** (both version vars empty
|
||||
⇒ dormant; the wire add is additive). The remaining G steps below are the release chain.
|
||||
1. Merge `feature/android-native` → `development`; verify on the contour (contour-safe; gate dormant).
|
||||
2. Promote `development` → `master`; tag `vX.Y.0`.
|
||||
3. Dispatch `android-build`; watch green; retrieve the signed APK.
|
||||
@@ -863,73 +863,167 @@ untouched**; wire changes **additive-only**; **TG/VK inert** (always online); **
|
||||
vars empty ⇒ dormant). Minimal-diff: keep `connection`/`offline` module paths as thin **derived shims** over
|
||||
`netState` so the ~14 `offlineMode.active` consumers are not mass-renamed (single source of truth underneath).
|
||||
|
||||
- **O1 — Pure net-state reducer + exhaustive unit tests (test-first).**
|
||||
- **O1 — Pure net-state reducer + exhaustive unit tests (test-first). — ✅ DONE (2026-07-12).**
|
||||
- Files — Create: `ui/src/lib/netstate.ts`, `ui/src/lib/netstate.test.ts`.
|
||||
- Interfaces — Produces: `type NetState = 'online'|'connecting'|'offlineNoNetwork'|'offlineVersionLocked'`;
|
||||
`type NetEvent` (`callFailed`/`callOk`/`probeOk`/`probeFailed`/`osOffline`/`osOnline`/`versionRejected`/
|
||||
`versionRecommended`/`boot`); `reduce(prev, event, cfg) => { state: NetState; effects: Effect[] }`
|
||||
(effects: `toast`, `startProbe`, `showVersionNotice`, `setNudge`); `cfg` carries `K` + `OFFLINE_DEBOUNCE_MS`.
|
||||
Consumes: nothing (pure).
|
||||
- Interfaces (as built) — Produces: `type NetState = 'online'|'connecting'|'offlineNoNetwork'|'offlineVersionLocked'`;
|
||||
`type NetEvent` (`boot`/`callOk`/`callFailed`/`probeOk`/`probeFailed`/`osOnline`/`osOffline`/`versionRejected`/
|
||||
`versionRecommended`); `reduce(prev: NetSnapshot, ev: NetInput, cfg: NetConfig) => NetResult` where
|
||||
`NetInput = { type: NetEvent; at: number }` (**time-on-the-event** — owner-chosen option A: the reducer stays
|
||||
a pure function of `(state, event)` and decides *both* hysteresis branches; O2 stamps `Date.now()`),
|
||||
`NetSnapshot = { state; fails; connectingSince }` (the hysteresis bookkeeping is carried in the reduced value
|
||||
so `reduce` is pure yet the whole anti-flap is unit-tested), `NetResult = { next: NetSnapshot; effects: Effect[] }`,
|
||||
`Effect = {kind:'toast',toast:'offline'|'online'} | {kind:'startProbe'} | {kind:'showVersionNotice'} | {kind:'setNudge'}`,
|
||||
`NetConfig = { k; debounceMs }` (K + `OFFLINE_DEBOUNCE_MS`), plus `INITIAL` (optimistic pre-boot `online`).
|
||||
Consumes: nothing (pure). **As-built semantics:** entry via `callFailed`/`probeFailed` counts as `fails 1`;
|
||||
`osOffline` is a hint (enters `connecting` with `fails 0`, only `startProbe`) — so a single blip stays `connecting`
|
||||
for K≥2; `boot` resets from any state (the sole exit from the sticky lock) → `connecting` + `startProbe`;
|
||||
`offlineVersionLocked` is sticky (no connectivity event exits it, notice shown once on entry); the soft nudge is
|
||||
**effect-only** (`setNudge` from `online`), latched later in O4, not stored in the snapshot.
|
||||
- Acceptance: every transition-table row + all 12 edge cases pass as pure `reduce` assertions; the blip vs
|
||||
sustained hysteresis (a single fail→ok stays `connecting`; K/debounce → `offlineNoNetwork`); `versionRejected`
|
||||
from **any** state → `offlineVersionLocked`; `versionRecommended` sets the nudge only from `online`.
|
||||
- Targeted tests: `netstate.test.ts` (vitest, node — pure, no jsdom).
|
||||
- Targeted tests: `netstate.test.ts` (vitest, node — pure, no jsdom). **31 assertions, green** (transition table,
|
||||
hysteresis blip/K/debounce, two-tier version gate, edge cases #1–#12, purity).
|
||||
|
||||
- **O2 — Runtime store + wire the events; migrate the two modules.**
|
||||
- Files — Create: `ui/src/lib/netstate.svelte.ts` (the `$state` store running `reduce`, the derived
|
||||
`offline`/`connecting` getters, the probe timer). Modify: `ui/src/lib/connection.svelte.ts` +
|
||||
`ui/src/lib/offline.svelte.ts` (become thin shims re-exporting the derived getters), `ui/src/lib/transport.ts`
|
||||
(emit `callFailed`/`callOk`/`versionRejected`/`versionRecommended`), `ui/src/lib/native.ts` (wire
|
||||
`@capacitor/network`), `ui/src/lib/app.svelte.ts` (boot event), `ui/package.json` (+`@capacitor/network`).
|
||||
- Interfaces — Produces: `netState` store `.state`/`.offline`/`.connecting`, `emit(event)`. Consumes: `reduce`
|
||||
(O1), the existing `registerProbe`.
|
||||
- Acceptance: airplane-mode → `offlineNoNetwork` → back → `online` via the machine; native uses
|
||||
`@capacitor/network`, web `navigator`; the derived `offline` matches the old `offlineMode.active` for every
|
||||
consumer (no visual/behaviour regression).
|
||||
- Targeted tests: `e2e/offline.spec.ts` still green; `e2e/native.spec.ts` extended (a `$state` store ⇒ e2e, like `update.svelte.ts`).
|
||||
- **O2 — Runtime store + wire the events; migrate the two modules. — ✅ DONE (2026-07-12).**
|
||||
- Files (as built) — Create: `ui/src/lib/netstate.svelte.ts` (the `$state` store running `reduce`; the derived
|
||||
`state`/`online`/`connecting`/`offline`/`versionLocked` getters; one self-rescheduling probe/recovery watcher —
|
||||
backoff while `connecting`, a 4 s poll while `offlineNoNetwork`, idle otherwise; the OS-signal wiring). Modify:
|
||||
`ui/src/lib/connection.svelte.ts` + `ui/src/lib/offline.svelte.ts` (thin shims over `netState` — `connection.online`
|
||||
= the online state, `offlineMode.active` = the machine `offline`; `reportOffline`→`callFailed` **once** from online,
|
||||
`reportOnline`→`callOk`), `ui/src/lib/update.svelte.ts` (`reportUpdateRequired` also emits `versionRejected`),
|
||||
`ui/src/lib/native.ts` (+`initNativeNetwork` — `@capacitor/network` → `osOnline`/`osOffline`),
|
||||
`ui/src/lib/app.svelte.ts` (the boot/recovery-seam rewrite), `ui/src/lib/gateway.ts` (the mock `__net` hook),
|
||||
`ui/src/App.svelte` (dialog removal), `ui/src/lib/i18n/{en,ru}.ts` (`net.offline`/`net.online` toast), `ui/package.json`
|
||||
(+`@capacitor/network@8.0.1`). **Owner-confirmed scope A (full migration):** the store subsumes the old
|
||||
`scheduleRecovery` poll + `initNetworkReactivity` listeners + the cold-boot `promptOfflineChoice` **dialog** (removed —
|
||||
"auto + toast, no dialog"); the native offline-first boot + the web unreachable-cached boot now enter via `bootOffline()`.
|
||||
- Interfaces (as built) — Produces: `netState` (`.state`/`.online`/`.connecting`/`.offline`/`.versionLocked`),
|
||||
`emit(type)` (stamps `Date.now()`), `bootOffline(kickNow?)`, `checkReachable`, `resetNetState`, `initNetSignals`, and a
|
||||
**register-hook inversion** so the store stays a leaf module (no cycle with `app.svelte.ts`): `registerProbe`
|
||||
(transport's reachability read), `registerRecovery` (the app's smart reconcile-or-reachability), `registerNetToast`
|
||||
(i18n toast). `NetConfig = { k: 3, debounceMs: 15000 }` (owner-confirmed). **Wiring:** `reportOffline` enters
|
||||
`connecting` once (only from online) and the **probe decides** offline via K/debounce; the session-less native
|
||||
reconcile IS the probe (`recover()` → `reconcileServerGuest` → `emit('callOk')` before adopt). **`boot` is not
|
||||
emitted** — a real relaunch reloads the bundle to `INITIAL` online, which also clears the sticky version lock; O1's
|
||||
`boot` handling stays valid but unused here. **TG/VK inertness falls out for free:** the registrations run only past
|
||||
the Telegram/VK early-returns in `bootstrap`, so those channels are never fed an offline signal (they can show
|
||||
`connecting` but never reach `offline`).
|
||||
- Acceptance: airplane-mode → `offlineNoNetwork` → back → `online` via the machine; native uses `@capacitor/network`,
|
||||
web `navigator`; the derived `offline` matches the old `offlineMode.active` for every consumer (no regression). **Verified.**
|
||||
- Targeted tests: `e2e/offline.spec.ts` **rewritten** (implicit offline via the mock `__net` hook — no toggle — + the
|
||||
toast + local play + persist + self-heal), `e2e/hotseat.spec.ts` migrated off the toggle, `e2e/native.spec.ts` green
|
||||
(the native boot/offline/reconcile IS the store's e2e). **Full suite 119/119 on chromium + webkit; svelte-check 0/0;
|
||||
vitest 622; `cap sync` registers `@capacitor/network`.**
|
||||
|
||||
- **O3 — Remove the explicit offline toggle + migrate the pref.**
|
||||
- Files — Modify: `ui/src/screens/Settings.svelte` (delete the `offlineEligible` toggle + `goOffline`),
|
||||
`ui/src/lib/offline.ts`/`offline.svelte.ts` (drop `requestOffline`/`loadOfflinePref`/`saveOfflinePref`; clear
|
||||
the stale key on boot), `ui/src/screens/SettingsHub.svelte` (tab-disable from `netState`).
|
||||
- Acceptance: no toggle in Settings; a seeded stale `offlinePref` boots **online** (nobody stuck); offline
|
||||
chrome/tab-gating still driven from `netState`.
|
||||
- Targeted tests: `e2e` — Settings has no toggle; a stale pref does not force offline.
|
||||
- **O3 — Remove the explicit offline toggle + migrate the pref. — ✅ DONE (2026-07-12).**
|
||||
- Files (as built) — Modify: `ui/src/screens/Settings.svelte` (deleted the whole "Play mode" Online/Offline segment +
|
||||
`offlineEligible` + `goOffline` + the `checking`/`needsData` state + the now-unused `isStandalone`/`insideVK`/offline
|
||||
imports + the `.onote` CSS), `ui/src/lib/offline.svelte.ts` (dropped the inert `setOfflineMode`/`requestOffline`/
|
||||
`TOGGLE_READY_BUDGET_MS` stubs + the dead `offlineMode.auto` getter — `offlineMode.active` = `netState.offline` stays),
|
||||
`ui/src/lib/offline.ts` (dropped `loadOfflinePref`/`saveOfflinePref`/`shouldBootOffline`/`offlineReady`/`missingDicts`/
|
||||
`raceOfflineReady`; **new** `clearOfflinePref`; only `offlinePreloadEligible` + the cleanup remain), `ui/src/lib/app.svelte.ts`
|
||||
(calls `clearOfflinePref()` once at boot, before the Mini-App branches), `ui/src/lib/i18n/{en,ru}.ts` (pruned 7 orphaned
|
||||
keys each: `settings.{offlineMode,online,offlineChecking,offlineNeedsData}` + `offline.prompt{Title,Yes,No}`). **Deleted**
|
||||
`ui/src/lib/dict/offlineready.ts` (orphaned once `requestOffline` went). Tests: `ui/src/lib/offline.test.ts` slimmed to
|
||||
`offlinePreloadEligible` + `clearOfflinePref`; `ui/e2e/offline.spec.ts` +1 case.
|
||||
- **Kept, not changed:** `settings.offline` (the Header offline chip still uses it) + `offline.preloadWarning` (the lobby
|
||||
dict-preload notice). **`SettingsHub.svelte` needed no change** — its tab-disable already reads `offlineMode.active`,
|
||||
which is `netState.offline` (the O2 shim); the plan's "tab-disable from `netState`" is satisfied without a mass-rename.
|
||||
- Acceptance: no toggle in Settings; a seeded stale `offlinePref` boots **online** (nobody stuck) and is cleared; offline
|
||||
chrome/tab-gating still driven from `netState`. **Verified.**
|
||||
- Targeted tests: `e2e/offline.spec.ts` new case (a stale `scrabble.offlineMode` → boots online, key cleared, Settings has
|
||||
no `Offline` toggle even in the eligible context); `offline.test.ts` (`clearOfflinePref`). **Full suite 120/120 on
|
||||
chromium + webkit; svelte-check 0/0; vitest 616.**
|
||||
|
||||
- **O4 — Two-tier version gate.**
|
||||
- Files — Modify: `gateway/internal/config/config.go` (+`RecommendedClientVersion`, validate ≥ `MinClientVersion`),
|
||||
`gateway/internal/connectsrv/server.go` (soft band ⇒ `X-Update-Recommended: 1` response header; hard band
|
||||
unchanged), `gateway/cmd/gateway/main.go` (dep). Client: `ui/src/lib/transport.ts` (read the header ⇒
|
||||
`versionRecommended`), `ui/src/lib/update.svelte.ts` (nudge state), rework `ui/src/components/UpdateOverlay.svelte`
|
||||
from terminal → the `offlineVersionLocked` notice ("Update"/"Play offline"), new `ui/src/components/UpdateNudge.svelte`.
|
||||
- Interfaces — Produces: `GATEWAY_RECOMMENDED_CLIENT_VERSION`, the `X-Update-Recommended` header, `updateRecommended` store.
|
||||
- Acceptance: `v<min` → notice → "Play offline" plays local; `min≤v<recommended` → dismissible banner, online
|
||||
continues; `v≥recommended`/both empty → nothing; fail-open on a garbled header.
|
||||
- Targeted tests: Go `config_test`/`server_test` (three bands + ordering + fail-open); `e2e/update.spec.ts`
|
||||
(notice → play offline; nudge banner).
|
||||
- **O4 — Two-tier version gate. — ✅ DONE (2026-07-13).**
|
||||
- Files (as built) — Backend: `gateway/internal/config/config.go` (+`RecommendedClientVersion` from
|
||||
`GATEWAY_RECOMMENDED_CLIENT_VERSION`; validate parseable + `≥ MinClientVersion`, an empty min imposing no lower bound),
|
||||
`gateway/internal/connectsrv/server.go` (soft-tier fields `recClient`/`recOn`; `clientUpdateRecommended` = `min ≤ v <
|
||||
recommended`, fail-open; Execute sets `X-Update-Recommended: 1` on any served response in the band via a named-return
|
||||
`defer`; hard band + Subscribe unchanged), `gateway/cmd/gateway/main.go` (dep). Client: `ui/src/lib/transport.ts` (a
|
||||
Connect-ES **interceptor** reads `x-update-recommended` on unary responses ⇒ `reportUpdateRecommended`),
|
||||
`ui/src/lib/update.svelte.ts` (dropped the terminal `updateRequired` latch — `reportUpdateRequired` now only emits
|
||||
`versionRejected`; new `updateRecommended` store + `latchUpdateNudge`/`dismissUpdateNudge`/`reportUpdateRecommended` +
|
||||
shared `openUpdate`), `ui/src/lib/netstate.svelte.ts` (`registerNetNudge`; the `setNudge` effect fires it),
|
||||
`ui/src/lib/app.svelte.ts` (`registerNetNudge(latchUpdateNudge)`), `ui/src/lib/gateway.ts` (mock `__update.recommend`),
|
||||
`ui/src/lib/i18n/{en,ru}.ts` (`update.playOffline` + `update.available`). Rework: `ui/src/components/UpdateOverlay.svelte`
|
||||
(terminal → dismissable notice reading `netState.versionLocked`, "Update" + "Play offline"). New:
|
||||
`ui/src/components/UpdateNudge.svelte` (the soft banner, rendered **bottom of the lobby, above the tab bar** — shown only
|
||||
while `online && updateRecommended.active && !dismissed`).
|
||||
- Owner-chosen (interview): the hard notice keeps the **full-screen** form (now dismissable); the soft nudge is a
|
||||
**bottom-of-lobby** banner (not in-game); the header rides **Execute only** (Subscribe untouched); read via a transport
|
||||
interceptor; nudge dismiss is **per-session**; the nudge latches via `setNudge → registerNetNudge` (machine is the source,
|
||||
"only from online"). `update.body` copy kept as-is (owner).
|
||||
- Acceptance: `v<min` → notice → "Play offline" → offline lobby; `min≤v<recommended` → dismissible banner, online continues;
|
||||
`v≥recommended`/both empty → nothing; fail-open on an absent/garbled header. **Verified.**
|
||||
- Targeted tests: Go `config_test` (`TestLoadRecommendedClientVersion`) + `server_test` (`TestExecuteUpdateRecommended` — 3
|
||||
bands + at-min/at-recommended boundaries + fail-open + recommended-alone + dormant); `e2e/update.spec.ts` rewritten (notice
|
||||
+ both actions; Update reloads; Play offline → offline lobby; the soft nudge shows in the lobby + dismisses). **Full gateway
|
||||
go-test/build/vet/gofmt clean; e2e 122/122 chromium + webkit; svelte-check 0/0; vitest 616.**
|
||||
|
||||
- **O5 — Unified lobby.**
|
||||
- Files — Modify: `ui/src/screens/Lobby.svelte` (merge `localSource.list()` + `gateway.gamesList()`; offline ⇒
|
||||
local active + cached server greyed; drop the mode-exclusive `loadSeq` branch), `ui/src/lib/lobbysort.ts`
|
||||
(sort the merged set), a self-identity helper (recognise `session.userId` **and** `localGuestId()`).
|
||||
- Acceptance: online ⇒ local + server both listed; offline ⇒ local active + server greyed-from-cache
|
||||
(un-openable); a reconciled guest's local games stay visible online (**closes G-step-0**); TG/VK server-only.
|
||||
- Targeted tests: `e2e` unified lobby (greyed offline; both online); `lobbysort.test.ts` extended.
|
||||
- **O5 — Unified lobby. — ✅ DONE (2026-07-13).**
|
||||
- Files (as built) — `ui/src/screens/Lobby.svelte` (`load()` merges `localSource.list()` + `gateway.gamesList()`;
|
||||
online ⇒ `[...local, ...server]`; offline ⇒ `[...localFresh, ...cachedServer]` greyed; the mode-exclusive branch is
|
||||
gone; `grey(g) = offline && !isLocalGameId` drives a `.rowwrap.greyed` dim + an un-openable `openGame` that toasts
|
||||
`net.offline`; **invitations hidden offline** — owner addition — via `{#if invitations.length && !offline}`), `ui/src/lib/lobbysort.ts`
|
||||
+ `ui/src/lib/result.ts` (`myId: string` → `myIds: readonly string[]` self-set across `isMyTurn`/`scoreStanding`/
|
||||
`gamePhase`/`groupGames`/`resultBadge`; game seat-matching uses `myIds.includes(...)`), `ui/src/lib/lobbycache.ts`
|
||||
(dropped the `offline` snapshot key — **one unified snapshot**; `getLobby()` no arg; offline reuses its server games),
|
||||
`ui/src/screens/NewGame.svelte` (`getLobby()` + filter to server games — device-local games do not count toward the
|
||||
server per-kind limit), `ui/src/lib/localguest`/`telegram`/`vk` imports in the lobby.
|
||||
- **Identity split:** game functions take the self-set `myIds = [session.userId, localGuestId()]` (a reconciled
|
||||
guest's local games seat you under `localGuestId`, server games under `userId`, ids never collide); **invitations
|
||||
keep `session.userId`** (a server concept). **TG/VK server-only:** the local merge is gated on
|
||||
`insideTelegram()`/`insideVK()` (the shared-origin local store must not leak into a mini-app).
|
||||
- Acceptance: online ⇒ local + server both listed; offline ⇒ local active + server greyed-from-cache (un-openable);
|
||||
a reconciled guest's local games stay visible online (**closes G-step-0**); invitations hidden offline; TG/VK
|
||||
server-only. **Verified.**
|
||||
- Targeted tests: `lobbysort.test.ts` + `result.test.ts` extended (two-id "self" recognition), `lobbycache.test.ts`
|
||||
reworked (unified snapshot, mode-aware tests removed); `e2e/native.spec.ts` proves the local vs_ai game stays listed
|
||||
online (G-step-0); `e2e/offline.spec.ts` + `hotseat.spec.ts` + `update.spec.ts` updated for greyed-from-cache server
|
||||
games offline. **Full suite 122/122 on chromium + webkit; svelte-check 0/0; vitest 617.**
|
||||
|
||||
- **O6 — Create flows.**
|
||||
- Files — Modify: `ui/src/screens/NewGame.svelte` (with-friends online/offline segmented control, online
|
||||
disabled when `netState.offline`; vs_ai already branches on the flag — verify; the dict-availability guard).
|
||||
- Acceptance: with-friends online = invite / offline = hotseat; no network ⇒ online disabled, offline
|
||||
preselected, create works; vs_ai offline→local / online→server unchanged; create disabled with a reason when
|
||||
the variant's dawg is unavailable offline.
|
||||
- Targeted tests: `e2e` with-friends toggle (online disabled offline); vs_ai local-vs-server by state.
|
||||
- **O6 — Create flows. — ✅ DONE (2026-07-13).**
|
||||
- Files (as built) — `ui/src/screens/NewGame.svelte`: **with-friends** now carries a `friendsMode` segmented control
|
||||
(`new.playRemote` = online invite / `new.playLocal` = offline hotseat); the online segment is `disabled` when
|
||||
`offlineMode.active` and an `$effect` forces `friendsMode='offline'` there (a `new.needsNetwork` note explains it);
|
||||
the friends markup is restructured from the mode-exclusive `{:else if offlineMode.active}` chain into
|
||||
`{:else} <segment> {#if friendsMode==='offline'} hotseat {:else if !friends} noFriends {:else} invite`. **vs_ai
|
||||
unchanged** (verified: `find()` still offline→`localSource.create`, online→`lobbyEnqueue`). **Dict guard** (owner-chosen
|
||||
**async getDawg**): a `dawgReady`/`dictBlocked` pair — an `$effect` awaits `getDawg(guardVariant, version)` while
|
||||
offline (`guardVariant` = the quick pick or the hotseat pick) and disables create + shows a `new.dictUnavailable` note
|
||||
only on a **confirmed** miss (`dawgReady === false`, never while `null`/checking, so a still-warming dawg does not
|
||||
false-block). `ui/src/lib/i18n/{en,ru}.ts` (`new.{playRemote,playLocal,needsNetwork,dictUnavailable}`).
|
||||
- Owner-chosen (interview): the dict guard uses the **async getDawg** precheck (no reliable sync signal; native bundles
|
||||
every variant so it mostly guards web offline); **guest gating unchanged** — an online guest still sees vs_ai only (no
|
||||
new online-guest hotseat); a guest offline keeps its hotseat.
|
||||
- Acceptance: with-friends online = invite / offline = hotseat; no network ⇒ online disabled, offline preselected,
|
||||
create works; vs_ai offline→local / online→server unchanged; create disabled with a reason on an unavailable offline
|
||||
dawg. **Verified.**
|
||||
- Targeted tests: `e2e/hotseat.spec.ts` asserts the offline segment (online disabled, hotseat forced); `offline.spec.ts`
|
||||
+ `native.spec.ts` exercise vs_ai-local + hotseat create through the new segment. **Full suite 122/122 on chromium +
|
||||
webkit; svelte-check 0/0; vitest 617.**
|
||||
|
||||
- **O7 — Docs + native e2e.**
|
||||
- Files — Modify: `docs/ARCHITECTURE.md` (§2 gate → two tiers + graceful degrade; §3 the net-state model),
|
||||
`docs/FUNCTIONAL.md` (+`_ru`), `docs/TESTING.md`, `deploy/README.md` (the two version vars), `ui/e2e/native.spec.ts`.
|
||||
- Acceptance: the F-baked docs describe both tiers + the implicit model + the unified lobby; `deploy/README`
|
||||
lists `GATEWAY_RECOMMENDED_CLIENT_VERSION`.
|
||||
- Targeted tests: n/a (docs); the native e2e reflects the new boot/offline.
|
||||
- **O7 — Docs + native e2e. — ✅ DONE (2026-07-13).**
|
||||
- Files (as built) — `docs/ARCHITECTURE.md`: **§2** rewrote the connectivity signal into the **net-state machine**
|
||||
(four states, implicit offline, hysteresis, self-heal, TG/VK-exempt), the version gate into the **two tiers** (hard →
|
||||
`offlineVersionLocked` + dismissable notice, *not* terminal; soft → `X-Update-Recommended` nudge), and the gate×offline
|
||||
rule; **§13** (PWA/offline) rewrote the offline model from the deliberate toggle/dialog to implicit detection + the
|
||||
**unified lobby** (greyed cached server games) + the with-friends **segment** + the dict guard. `docs/FUNCTIONAL.md`
|
||||
(+`_ru` mirror): rewrote *Offline mode* (implicit, unified lobby, segment) and added a *Staying up to date* /
|
||||
*«Актуальная версия»* story (Update/Play-offline notice + the soft nudge). `docs/TESTING.md`: the `netstate.test.ts`
|
||||
reducer layer, the `__net`-driven offline spec, the two-tier `update.spec`, the soft-tier Go server/config tests, and
|
||||
the native G-step-0 assertion. `deploy/README.md`: a **`GATEWAY_RECOMMENDED_CLIENT_VERSION`** row (soft tier;
|
||||
validated ≥ `MIN`). `ui/e2e/native.spec.ts` already reflects the new boot/offline (done in O5).
|
||||
- Acceptance: the docs describe both tiers + the implicit model + the unified lobby; `deploy/README` lists
|
||||
`GATEWAY_RECOMMENDED_CLIENT_VERSION`. **Verified** (a full-doc sweep leaves no stale deliberate-offline / terminal /
|
||||
toggle / dialog reference). Code unchanged from O6 (docs only): gateway Go green, svelte-check 0/0, vitest 617,
|
||||
e2e 122/122 chromium + webkit.
|
||||
|
||||
**Offline-model redesign COMPLETE (O1–O7).** G-step-0 is resolved — the redesign is contour-safe (both version vars empty
|
||||
⇒ dormant; the wire add is additive). Next is the normal G chain: PR → `development` → contour verify → `master` → tag →
|
||||
dispatch `android-build`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user