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.
This commit is contained in:
Ilia Denisov
2026-07-06 18:15:12 +02:00
parent 5bc2ad3b6d
commit e0d28733ff
4 changed files with 42 additions and 6 deletions
+25
View File
@@ -577,6 +577,29 @@ function promptOfflineChoice(): Promise<boolean> {
});
}
/**
* initNetworkReactivity reacts to mid-session network changes (e.g. the player toggling flight mode):
* losing the network interface enters offline mode automatically (session-only); regaining it
* verifies the gateway is really reachable (an interface being up does not guarantee it) and returns
* to online — but only if the offline was auto. A deliberate offline (the Settings toggle or the
* cold-start dialog) is left as the player's choice. These are passive OS events — no polling, no
* battery cost. Web-only and skipped in the mock (the e2e drives connectivity directly).
*/
export function initNetworkReactivity(): void {
if (typeof window === 'undefined' || import.meta.env.MODE === 'mock') return;
window.addEventListener('offline', () => {
if (!offlineMode.active) setOfflineMode(true, false); // auto (session) — self-heals below
});
window.addEventListener('online', () => {
if (offlineMode.active && offlineMode.auto) {
void checkReachable(BOOT_REACHABILITY_TIMEOUT_MS).then((reachable) => {
// Re-read the flags — the player may have chosen offline meanwhile — before returning online.
if (reachable && offlineMode.active && offlineMode.auto) setOfflineMode(false);
});
}
});
}
/**
* applyLinkResult applies a completed account link or merge: it adopts a
* switched session (a guest initiator whose durable counterpart won, so the active
@@ -826,6 +849,8 @@ export async function bootstrap(): Promise<void> {
// worker so the app is installable — Chromium needs a registered SW, notably on Android. Web-only
// and skipped in the mock build; see lib/pwa.svelte.
registerServiceWorker();
// React to mid-session network changes (e.g. flight mode) for the rest of the session.
initNetworkReactivity();
// Deliberate offline mode carried over from a prior session: with no network the session adoption
// + profile fetch below would hang the splash, so skip them and launch straight from the cached