Wire up the SSE push channel in both frontend dashboards

app/static/index.html: opens an EventSource against /rounds/stream (no auth
needed, see the previous commit) alongside the existing polling loops. On an
"update" notification, immediately re-runs the same refreshes polling would
eventually do (refreshRound/refreshMe/checkLastRoundResult when logged in,
refreshChainStatusOnly when logged out). Also reacts to the browser's "open"
event, which fires on the initial connection and on every automatic
reconnect — this re-syncs right away instead of leaving the page on stale
state until the next event or poll tick, which matters most right after a
dropped connection comes back.

app/static/admin.html: same channel, refreshing the chain-status bar and
whichever admin section is currently open (Utenti/Round/Transazioni
pendenti/Audit log) instead of requiring a manual tab switch to see new data.

Polling intervals are untouched in both pages — this is purely additive, so
a blocked/dropped SSE connection just degrades to the pre-existing behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 10:52:50 +02:00
co-authored by Claude Sonnet 5
parent f229f91632
commit dda5bd14e1
2 changed files with 69 additions and 3 deletions
+29 -2
View File
@@ -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();
</script>
+40 -1
View File
@@ -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();
</script>