From 09c5a5e72e80d8ef0b8dbf9d55ea0dbc99ca83b0 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Mon, 6 Jul 2026 19:03:33 +0200 Subject: [PATCH] chore(offline): TEMPORARY network diagnostic panel (remove before release) A fixed strip in the lobby showing live navigator.onLine / offline.active / offline.auto / connection.online plus a timestamped event log (offline/online events, poll ticks, checkReachable results, mode changes) - to pinpoint where the auto-offline -> online transition breaks on the contour. A 1s heartbeat logs navigator.onLine flips even if the events never fire. Skipped in the mock; to be reverted with the fix. --- ui/src/lib/app.svelte.ts | 16 ++++++++++++--- ui/src/lib/netdiag.svelte.ts | 38 ++++++++++++++++++++++++++++++++++++ ui/src/screens/Lobby.svelte | 10 ++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 ui/src/lib/netdiag.svelte.ts diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 49ab6aa..6d751ea 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -57,6 +57,7 @@ import { import type { CoachSeries } from './coachmark'; import { connection, reportOffline, reportOnline, resetConnection, checkReachable } from './connection.svelte'; import { offlineMode, setOfflineMode } from './offline.svelte'; +import { netlog } from './netdiag.svelte'; // TEMP diagnostic import { shouldBootOffline } from './offline'; import { isConnectionCode } from './retry'; import { clearGameCache, getCachedGame, setCachedGame } from './gamecache'; @@ -603,9 +604,15 @@ export function scheduleRecovery(delayMs: number): void { recoveryTimer = null; if (!offlineMode.active || !offlineMode.auto) return; const interfaceUp = typeof navigator === 'undefined' || navigator.onLine; - if (interfaceUp && (await checkReachable(BOOT_REACHABILITY_TIMEOUT_MS))) { - if (offlineMode.active && offlineMode.auto) setOfflineMode(false); - return; + netlog(`poll tick: navOnline=${interfaceUp}`); // TEMP diagnostic + if (interfaceUp) { + const reachable = await checkReachable(BOOT_REACHABILITY_TIMEOUT_MS); + netlog(`checkReachable=${reachable}`); // TEMP diagnostic + if (reachable && offlineMode.active && offlineMode.auto) { + setOfflineMode(false); + netlog('-> RETURNED ONLINE'); // TEMP diagnostic + return; + } } scheduleRecovery(4000); }, delayMs); @@ -614,14 +621,17 @@ export function scheduleRecovery(delayMs: number): void { export function initNetworkReactivity(): void { if (typeof window === 'undefined' || import.meta.env.MODE === 'mock') return; window.addEventListener('offline', () => { + netlog(`offline EVENT (active=${offlineMode.active})`); // TEMP diagnostic if (!offlineMode.active) { setOfflineMode(true, false); // auto (session) + netlog('-> auto-offline; poll started'); // TEMP diagnostic scheduleRecovery(4000); // poll for the network to return } }); // The online event, when it fires, just kicks an immediate reachability check; the poll above is // the reliable fallback for platforms where it does not fire. window.addEventListener('online', () => { + netlog(`online EVENT (active=${offlineMode.active} auto=${offlineMode.auto})`); // TEMP diagnostic if (offlineMode.active && offlineMode.auto) scheduleRecovery(0); }); } diff --git a/ui/src/lib/netdiag.svelte.ts b/ui/src/lib/netdiag.svelte.ts new file mode 100644 index 0000000..d1a487c --- /dev/null +++ b/ui/src/lib/netdiag.svelte.ts @@ -0,0 +1,38 @@ +// TEMPORARY network diagnostic — REMOVE before release. A live view of the online/offline signals so +// we can pinpoint where the auto-offline -> online transition breaks. Rendered by a panel in +// Lobby.svelte. Skipped in the mock build. + +let entries = $state([]); +let navOnline = $state(typeof navigator !== 'undefined' ? navigator.onLine : true); + +export const netdiag = { + /** entries is the newest-first ring buffer of timestamped diagnostic lines. */ + get entries(): string[] { + return entries; + }, + /** navOnline is the live navigator.onLine value (the property itself is not reactive). */ + get navOnline(): boolean { + return navOnline; + }, +}; + +/** netlog appends a timestamped diagnostic line (kept to the last 24). */ +export function netlog(msg: string): void { + const t = new Date().toLocaleTimeString(); + entries = [`${t} ${msg}`, ...entries].slice(0, 24); + if (typeof navigator !== 'undefined') navOnline = navigator.onLine; +} + +// A 1 s heartbeat keeps navigator.onLine live in the panel and logs when it flips — so a flight-mode +// toggle is visible even if the online/offline events never fire (the exact thing we are chasing). +if (typeof window !== 'undefined' && import.meta.env.MODE !== 'mock') { + let last = typeof navigator !== 'undefined' ? navigator.onLine : true; + setInterval(() => { + const now = typeof navigator !== 'undefined' ? navigator.onLine : true; + navOnline = now; + if (now !== last) { + last = now; + netlog(`navigator.onLine -> ${now}`); + } + }, 1000); +} diff --git a/ui/src/screens/Lobby.svelte b/ui/src/screens/Lobby.svelte index 1cd9405..60ac74b 100644 --- a/ui/src/screens/Lobby.svelte +++ b/ui/src/screens/Lobby.svelte @@ -6,6 +6,7 @@ import Modal from '../components/Modal.svelte'; import { app, handleError, refreshFeedbackBadge, seedChatUnread } from '../lib/app.svelte'; import { connection } from '../lib/connection.svelte'; + import { netdiag } from '../lib/netdiag.svelte'; // TEMP diagnostic import { gateway } from '../lib/gateway'; import { navigate } from '../lib/router.svelte'; import { t } from '../lib/i18n/index.svelte'; @@ -258,6 +259,15 @@ } + +
+
+ nav.onLine={String(netdiag.navOnline)} · offline={String(offlineMode.active)} · auto={String(offlineMode.auto)} · conn.online={String(connection.online)} +
+ {#each netdiag.entries as e, i (i)}
{e}
{/each} +
+
{#if invitations.length}