mirror of
https://github.com/LucasKalil-Programador/world-2026-hub.git
synced 2026-07-04 17:41:28 -03:00
feat(app): poll results.json for live updates without reload
This commit is contained in:
parent
ddc82bba49
commit
5b1707a764
5 changed files with 75 additions and 1 deletions
|
|
@ -8,7 +8,7 @@ import { initSchedule } from './schedule.js';
|
||||||
import { initGroups } from './groups.js';
|
import { initGroups } from './groups.js';
|
||||||
import { initStadiums } from './stadiums.js';
|
import { initStadiums } from './stadiums.js';
|
||||||
import { initModal } from './modal.js';
|
import { initModal } from './modal.js';
|
||||||
import { initBracket } from './bracket.js';
|
import { initBracket, invalidateBracket } from './bracket.js';
|
||||||
import { initStats } from './stats.js';
|
import { initStats } from './stats.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------- data
|
// ---------------------------------------------------------------- data
|
||||||
|
|
@ -40,6 +40,73 @@ export function getData() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------ live data refresh
|
||||||
|
// results.json is the only file that changes during the tournament, and it is
|
||||||
|
// updated by a MANUAL daily push (scores land post-match, on deploy) — not a
|
||||||
|
// live feed. An open tab fetches it once at load; this poll surfaces a newly
|
||||||
|
// published result/stats without an F5. Static host → polling is the only
|
||||||
|
// option; because the data isn't live, a plain fixed interval is right (a
|
||||||
|
// per-match "live" tier would have nothing new to fetch). Paused while the tab
|
||||||
|
// is hidden and stopped once the final result is in — see .agents/issues.md.
|
||||||
|
const POLL_INTERVAL_MS = 90 * 1000;
|
||||||
|
let pollTimer = null;
|
||||||
|
let resultsSig = null;
|
||||||
|
|
||||||
|
// Nothing left to fetch once the final's REAL result is in the data. Guard on
|
||||||
|
// the JSON status, not the clock-driven 'over' — clock-over fires 3h after
|
||||||
|
// kickoff and could stop the poll before the actual score is published.
|
||||||
|
function tournamentOver() {
|
||||||
|
const final = data.matches.find((m) => m.bracketRef === 'FINAL');
|
||||||
|
return final ? data.resultByMatchId.get(final.id)?.status === 'finished' : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollResults() {
|
||||||
|
if (tournamentOver()) { stopResultsPolling(); return; }
|
||||||
|
let results;
|
||||||
|
try {
|
||||||
|
// ?t bypasses the frozen DATA_VERSION + Hostinger's missing cache headers
|
||||||
|
const res = await fetch(`data/results.json?t=${Date.now()}`, { cache: 'no-store' });
|
||||||
|
if (!res.ok) return;
|
||||||
|
results = await res.json();
|
||||||
|
} catch {
|
||||||
|
return; // network blip or mid-deploy partial — just retry next tick
|
||||||
|
}
|
||||||
|
// Content signature: catches scores, stats backfill and penalties alike —
|
||||||
|
// a finished-count signature would miss corrections and stats-only edits.
|
||||||
|
const sig = JSON.stringify(results);
|
||||||
|
if (sig === resultsSig) return; // unchanged → zero re-render
|
||||||
|
resultsSig = sig;
|
||||||
|
data.results = results;
|
||||||
|
data.resultByMatchId = new Map(results.map((r) => [r.matchId, r])); // derived map must be rebuilt too
|
||||||
|
// bracket-config.json (thirdPlaceAssignment) only ever changes alongside a
|
||||||
|
// results change — the one-time 3rd-place fill ships in the same daily push.
|
||||||
|
// So piggyback a refetch on the rare results-changed event (not every tick):
|
||||||
|
// closes the gap where the 8 third-place slots would otherwise need an F5.
|
||||||
|
try {
|
||||||
|
const cfg = await fetch(`data/bracket-config.json?t=${Date.now()}`, { cache: 'no-store' });
|
||||||
|
if (cfg.ok) data.bracketConfig = await cfg.json();
|
||||||
|
} catch { /* keep the in-memory config */ }
|
||||||
|
invalidateBracket(); // cached tree depends on results + bracketConfig
|
||||||
|
document.dispatchEvent(new CustomEvent('datachange')); // each view re-renders itself
|
||||||
|
if (tournamentOver()) stopResultsPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVisibility() {
|
||||||
|
if (!document.hidden) pollResults(); // catch up the instant the user returns
|
||||||
|
}
|
||||||
|
|
||||||
|
function startResultsPolling() {
|
||||||
|
if (pollTimer || tournamentOver()) return;
|
||||||
|
resultsSig = JSON.stringify(data.results); // seed from what loadData() already fetched
|
||||||
|
pollTimer = setInterval(() => { if (!document.hidden) pollResults(); }, POLL_INTERVAL_MS);
|
||||||
|
document.addEventListener('visibilitychange', onVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResultsPolling() {
|
||||||
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||||
|
document.removeEventListener('visibilitychange', onVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- time
|
// ---------------------------------------------------------------- time
|
||||||
|
|
||||||
export function matchDateUTC(match) {
|
export function matchDateUTC(match) {
|
||||||
|
|
@ -461,6 +528,7 @@ async function init() {
|
||||||
initTooltips();
|
initTooltips();
|
||||||
document.addEventListener('langchange', renderHome);
|
document.addEventListener('langchange', renderHome);
|
||||||
document.addEventListener('timemodechange', renderHero);
|
document.addEventListener('timemodechange', renderHero);
|
||||||
|
document.addEventListener('datachange', renderHome); // poll picked up new results → refresh hero + dashboard counts
|
||||||
try {
|
try {
|
||||||
await loadData();
|
await loadData();
|
||||||
renderHome();
|
renderHome();
|
||||||
|
|
@ -470,6 +538,7 @@ async function init() {
|
||||||
initBracket();
|
initBracket();
|
||||||
initStadiums();
|
initStadiums();
|
||||||
initStats();
|
initStats();
|
||||||
|
startResultsPolling(); // after the views register their datachange listeners
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error);
|
showError(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,7 @@ export function initBracket() {
|
||||||
render();
|
render();
|
||||||
document.addEventListener('langchange', render);
|
document.addEventListener('langchange', render);
|
||||||
document.addEventListener('favchange', render);
|
document.addEventListener('favchange', render);
|
||||||
|
document.addEventListener('datachange', render); // tree already invalidated by the poll → rebuilds
|
||||||
loadPredictionFromURL();
|
loadPredictionFromURL();
|
||||||
|
|
||||||
const root = document.getElementById('bracket-root');
|
const root = document.getElementById('bracket-root');
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export function initGroups() {
|
||||||
render();
|
render();
|
||||||
document.addEventListener('langchange', render);
|
document.addEventListener('langchange', render);
|
||||||
document.addEventListener('favchange', render);
|
document.addEventListener('favchange', render);
|
||||||
|
document.addEventListener('datachange', render); // new results → recompute standings
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export function initSchedule() {
|
||||||
document.addEventListener('simchange', renderList);
|
document.addEventListener('simchange', renderList);
|
||||||
document.addEventListener('favchange', renderList);
|
document.addEventListener('favchange', renderList);
|
||||||
document.addEventListener('timemodechange', renderList);
|
document.addEventListener('timemodechange', renderList);
|
||||||
|
document.addEventListener('datachange', renderList); // new published results/status
|
||||||
|
|
||||||
// delegation on the panel root — survives every list re-render
|
// delegation on the panel root — survives every list re-render
|
||||||
const root = document.getElementById('schedule-root');
|
const root = document.getElementById('schedule-root');
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,8 @@ export function initStats() {
|
||||||
// labels re-render on language change; the derived model never changes at
|
// labels re-render on language change; the derived model never changes at
|
||||||
// runtime (data is static per page load) so it is reused.
|
// runtime (data is static per page load) so it is reused.
|
||||||
document.addEventListener('langchange', render);
|
document.addEventListener('langchange', render);
|
||||||
|
// new published results change the aggregates → rebuild the memoized model
|
||||||
|
document.addEventListener('datachange', () => { model = null; render(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue