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 @@
+ + +