diff --git a/app/static/admin.html b/app/static/admin.html index e884098..7b9721a 100644 --- a/app/static/admin.html +++ b/app/static/admin.html @@ -241,14 +241,16 @@ function stopChainStatusPolling() { } const VIEWS = ['parametri', 'utenti', 'round', 'pending', 'audit']; +const VIEW_LOADERS = { utenti: loadUsers, round: loadRounds, pending: loadPending, audit: loadAuditLog }; +let currentAdminView = 'parametri'; function switchView(name) { + currentAdminView = name; for (const key of VIEWS) { document.getElementById('nav-' + key).classList.toggle('active', key === name); document.getElementById('view-' + key).classList.toggle('active', key === name); } - const loaders = { utenti: loadUsers, round: loadRounds, pending: loadPending, audit: loadAuditLog }; - if (loaders[name]) loaders[name](); + if (VIEW_LOADERS[name]) VIEW_LOADERS[name](); } function showDashboard() { @@ -521,6 +523,31 @@ window.addEventListener('pageshow', (event) => { if (event.persisted) initAuthState(); }); +// Same server-push channel as app/static/index.html (see app/rounds/events.py): +// a content-free "something changed" ping. Here it refreshes the chain-status +// bar immediately, and reloads whichever admin section is currently open +// (Utenti/Round/Transazioni pendenti/Audit log) so it doesn't need a manual +// switch-away-and-back to pick up a new row. Polling stays in place as a +// fallback if this connection is ever blocked or drops. +let adminEventSource = null; + +function onAdminServerEvent() { + if (!adminToken) return; + refreshChainStatus(); + if (VIEW_LOADERS[currentAdminView]) VIEW_LOADERS[currentAdminView](); +} + +function connectAdminEvents() { + if (adminEventSource) return; + adminEventSource = new EventSource('/rounds/stream'); + adminEventSource.addEventListener('update', onAdminServerEvent); + // Fires on the initial connection AND every successful auto-reconnect — + // re-syncs immediately instead of waiting for the next event or poll tick + // to notice whatever changed while this connection was down. + adminEventSource.addEventListener('open', onAdminServerEvent); +} + +connectAdminEvents(); initAuthState(); diff --git a/app/static/index.html b/app/static/index.html index 2d54dbf..c204974 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -682,8 +682,14 @@ async function showDashboard() { // data.winner_user_id === myUserId — otherwise that comparison could race // against an unset myUserId right after a reload. await refreshMe(); + // Awaited too, and before refreshRound(): on a brand-new browser/device that + // never saw this round live (nothing in localStorage), this is the only + // thing that knows the outcome once the round has fully closed. Resolving + // it first means refreshRound() finds the answer already in place instead + // of momentarily rendering "no result" and then flipping to the win/lose + // box a moment later once this backstop catches up. + await checkLastRoundResult(); refreshRound(); - checkLastRoundResult(); clearInterval(lastResultInterval); lastResultInterval = setInterval(checkLastRoundResult, 20000); clearInterval(roundTimerInterval); @@ -906,6 +912,39 @@ async function withdraw() { refreshMe(); } +// Server push: an SSE channel that notifies the instant round/bet/balance +// state changes anywhere (see app/rounds/events.py), instead of everyone +// waiting for their next poll tick. The message carries no payload — it just +// means "something changed", so we react by immediately re-running the same +// refreshes the polling loop would eventually do on its own. Polling is left +// completely in place as a fallback: if this connection is blocked/dropped +// (proxy, browser setting, flaky network), the page keeps working exactly as +// before, just without the instant nudge. +let roundEventSource = null; + +function onRoundServerEvent() { + if (token) { + refreshRound(); + refreshMe(); + checkLastRoundResult(); + } else { + refreshChainStatusOnly(); + } +} + +function connectRoundEvents() { + if (roundEventSource) return; + roundEventSource = new EventSource('/rounds/stream'); + roundEventSource.addEventListener('update', onRoundServerEvent); + // Fires on the initial connection AND every successful auto-reconnect (the + // browser retries this on its own after a drop) — re-syncs immediately + // instead of leaving the page on whatever it last knew until the next event + // or poll tick, which would otherwise widen the "missed while disconnected" + // window to the full reconnect gap. + roundEventSource.addEventListener('open', onRoundServerEvent); +} + +connectRoundEvents(); initAuthState();