From dbcac0653edbf30dd6b36c12a0d5da37d5d87af9 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Fri, 10 Jul 2026 12:30:30 +0000 Subject: [PATCH] Prompt to reload when a new version is available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The service worker used to skipWaiting() on install and claim clients on activate, so a new build's assets swapped in silently and a long-open tab kept running stale JS. Switch to the standard update flow: the worker now waits until the page sends it a SKIP_WAITING message, and the page shows a "A new version is available — Reload / Later" banner when a new worker reaches "installed" while one is already controlling the tab (that controller check suppresses the first-install prompt). Reload posts SKIP_WAITING and reloads on controllerchange (guarded against a reload loop and against the initial clients.claim on a fresh install); Later dismisses until the next update. Since browsers only auto-check on navigation, also poll registration.update() hourly and on visibilitychange. Bump the cache to v9 so the old cache is cleaned up on activate. --- src/app.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/index.html | 11 +++++++++ src/style.css | 33 +++++++++++++++++++++++++ src/sw.js | 10 ++++++-- 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/src/app.js b/src/app.js index d29cdc0..d14c712 100644 --- a/src/app.js +++ b/src/app.js @@ -1619,10 +1619,71 @@ window.addEventListener("online", () => { setStatus(); sync(); syncConfig(); }); window.addEventListener("offline", () => setStatus()); - // Service worker (independent of auth). + // Service worker (independent of auth). The worker no longer auto-activates a + // new build; instead we detect the waiting worker and let the user choose when + // to swap onto fresh assets, so a long-open tab isn't left running stale JS. if ("serviceWorker" in navigator) { + const updateBanner = document.getElementById("update-banner"); + const updateReload = document.getElementById("update-reload"); + const updateLater = document.getElementById("update-later"); + let waitingWorker = null; + + // Whether there was already a controlling worker when the page loaded. On a + // brand-new install there isn't, and clients.claim() fires an initial + // controllerchange we must NOT reload on (there's nothing to refresh to). + const hadController = !!navigator.serviceWorker.controller; + let reloadRequested = false; // user pressed Reload → controllerchange reloads + let refreshing = false; // guard against a reload loop + + navigator.serviceWorker.addEventListener("controllerchange", () => { + if (refreshing) return; + if (!hadController && !reloadRequested) return; // first-install claim + refreshing = true; + location.reload(); + }); + + function showUpdateBanner(worker) { + waitingWorker = worker; + updateBanner.hidden = false; + } + + updateReload.addEventListener("click", () => { + reloadRequested = true; + updateBanner.hidden = true; + // Tell the waiting worker to activate; controllerchange then reloads us. + if (waitingWorker) waitingWorker.postMessage({ type: "SKIP_WAITING" }); + }); + // "Later" just dismisses; the next update (or reload) surfaces it again. + updateLater.addEventListener("click", () => { updateBanner.hidden = true; }); + + // Only prompt when a *previous* worker was already in control — that check + // is what suppresses the banner on the very first install. + function trackInstalling(worker) { + worker.addEventListener("statechange", () => { + if (worker.state === "installed" && navigator.serviceWorker.controller) { + showUpdateBanner(worker); + } + }); + } + + function watchForUpdate(reg) { + // A worker may already be waiting from a previous session's update. + if (reg.waiting && navigator.serviceWorker.controller) showUpdateBanner(reg.waiting); + reg.addEventListener("updatefound", () => { + if (reg.installing) trackInstalling(reg.installing); + }); + // Browsers only auto-check for a new worker on navigation, so also poll + // hourly and whenever the tab becomes visible again. + setInterval(() => reg.update().catch(() => {}), 60 * 60 * 1000); + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible") reg.update().catch(() => {}); + }); + } + window.addEventListener("load", () => { - navigator.serviceWorker.register("sw.js").catch(err => console.error("SW", err)); + navigator.serviceWorker.register("sw.js") + .then(watchForUpdate) + .catch(err => console.error("SW", err)); }); } diff --git a/src/index.html b/src/index.html index 0e23c46..8b89353 100644 --- a/src/index.html +++ b/src/index.html @@ -21,6 +21,17 @@ + + +