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.
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).
Connectivity failures become state, not a toast on every attempt. A global online
signal (lib/connection.svelte.ts) flips on a transport unavailable / rate_limited and
on the live stream's drop, driving a pure-CSS header spinner + 'Connecting…' in place
of the title and softly disabling the in-game server actions (commit / exchange / pass
/ hint; local board/rack/reset stay live).
- transport: exec auto-retries with capped exponential backoff — every op on a
rate-limit (rejected before processing, safe), reads only on unavailable (a mutation
is never blindly re-sent, to avoid double-applying one whose response was lost; its
button is disabled while offline so the player re-issues on reconnect). A reachability
watcher (profile.get probe) and any successful traffic clear the signal.
- the old red error.unavailable toast is gone (handleError suppresses connection codes;
the indicator replaces it). A server-data screen still opens with the spinner and
fills on reconnect (global indicator + read auto-retry), so navigation is never dead.
- pure retry policy unit-tested (retry.ts); a mock-only window.__conn hook drives a
Chromium+WebKit e2e (indicator shows offline, the action disables, both clear on
reconnect). Full suite + build green.
- docs: ARCHITECTURE transport note, FUNCTIONAL (+ _ru), PLAN tracker (incl. #1 — the
bot already drains all updates, no change).
Also records #1 as investigated/no-change in PLAN. Other server-action buttons (chat
send, profile save, …) still degrade to a safe no-op offline; visual disable is easy to
extend.