From 1d28ec2a3ffb8979c85ec760d49b3ace9dfbf918 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Sat, 29 Aug 2026 08:30:39 +0200 Subject: [PATCH] Refresh the page when it comes back from the background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Durations of running builds are ticked client-side, so a page left in the background kept counting up a build that had long finished on the server — on a phone the tab is frozen for an hour and comes back showing a build "running" for an hour. Resuming now re-arms the poll unconditionally instead of only when the page was stopped: a frozen page may never deliver the hidden event, and its throttled interval then fires whenever the browser feels like it. `pageshow` and `focus` are listened to as well, because not every browser reports a returning page as a visibility change; resume events arriving together are collapsed into one fetch. Verified in a browser: hidden for 11s over a 10s interval makes no request, a lone focus event makes exactly one, and a second resume event right after makes none. Co-Authored-By: Claude Opus 5 --- src/main/resources/static/gittally.js | 30 ++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/resources/static/gittally.js b/src/main/resources/static/gittally.js index f8ba3b6..58aa411 100644 --- a/src/main/resources/static/gittally.js +++ b/src/main/resources/static/gittally.js @@ -89,6 +89,8 @@ const FETCH_TIMEOUT_MS = 8000; const TABLE_POLL_MS = 10000; const CURRENT_POLL_MS = 3000; const SYSTEM_POLL_MS = 60000; +// resume events (visibilitychange, pageshow, focus) often arrive in pairs +const RESUME_MIN_GAP_MS = 1000; function metaContent(name) { const element = document.querySelector(`meta[name="${name}"]`); @@ -186,19 +188,34 @@ function setLiveIndicator(ok, detail) { // The page's refresh function; actions trigger it for an immediate update. let refreshNow = null; -/** Polls `refresh`; paused while the tab is hidden, refreshed immediately when visible again. */ +/** + * Polls `refresh`; paused while the tab is hidden, refreshed immediately when the page + * becomes visible again — the durations of running builds are ticked client-side, so a + * page returning from the background would otherwise keep counting up a build that has + * long finished on the server. + * + * Resuming re-arms the interval unconditionally instead of only when the page was + * stopped: a backgrounded page (phone, app switch) may be frozen without ever + * delivering the hidden event, and its throttled interval then fires whenever the + * browser feels like it. `pageshow` and `focus` are listened to as well, because not + * every browser reports a returning page as a visibility change. + */ function startPolling(refresh, intervalMs) { let timer = null; + let lastTickAt = 0; const tick = () => { + lastTickAt = Date.now(); refresh() .then(() => setLiveIndicator(true, "last update " + formatTimestamp(new Date().toISOString()))) .catch((error) => setLiveIndicator(false, String(error))); }; const start = () => { - if (timer === null) { + stop(); + // several resume events can arrive together; one fetch is enough for all of them + if (Date.now() - lastTickAt >= RESUME_MIN_GAP_MS) { tick(); - timer = setInterval(tick, intervalMs); } + timer = setInterval(tick, intervalMs); }; const stop = () => { if (timer !== null) { @@ -206,7 +223,14 @@ function startPolling(refresh, intervalMs) { timer = null; } }; + const resume = () => { + if (!document.hidden) { + start(); + } + }; document.addEventListener("visibilitychange", () => (document.hidden ? stop() : start())); + window.addEventListener("pageshow", resume); + window.addEventListener("focus", resume); refreshNow = tick; start(); }