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
+3 -1
View File
@@ -40,7 +40,9 @@ export function createTransport(baseUrl: string): GatewayClient {
// (it must reject when there is no session, so the watcher keeps waiting rather than reporting up).
const reachabilityProbe = async (): Promise<void> => {
if (!token) throw new Error('no session');
assertOnline();
// Exempt from the offline kill switch: the probe IS the reachability check that decides whether to
// return online, and it fires only deliberately — the connection watcher is guarded off in offline
// mode, and checkReachable runs it on an online event — so it must reach the gateway while offline.
await client.execute({ messageType: 'profile.get', payload: codec.empty(), requestId: '' }, { headers: headers() });
};
registerProbe(reachabilityProbe);