Prompt to reload when a new version is available
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.
This commit is contained in:
+63
-2
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Shown when a newer build's service worker is waiting. "Reload" activates
|
||||
it and refreshes onto the new assets; "Later" dismisses until next time.
|
||||
Suppressed on the very first install (see app.js). -->
|
||||
<div id="update-banner" class="update-banner" hidden role="status" aria-live="polite">
|
||||
<span class="update-banner-msg">A new version is available</span>
|
||||
<span class="update-banner-actions">
|
||||
<button type="button" id="update-reload" class="update-banner-btn">Reload</button>
|
||||
<button type="button" id="update-later" class="update-banner-btn ghostish">Later</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Login / register gate. Shown until the session check succeeds; the app
|
||||
(#app) stays hidden behind it so no puppy data paints while logged out. -->
|
||||
<div id="auth-screen" class="auth-screen" hidden>
|
||||
|
||||
@@ -663,3 +663,36 @@ input.switch::after {
|
||||
}
|
||||
input.switch:checked { background: var(--accent); }
|
||||
input.switch:checked::after { transform: translateX(18px); }
|
||||
|
||||
/* ---------- update banner ---------- */
|
||||
.update-banner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 70;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: calc(10px + env(safe-area-inset-top, 0)) 16px 10px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.update-banner[hidden] { display: none; }
|
||||
.update-banner-msg { font-size: 0.9rem; font-weight: 600; }
|
||||
.update-banner-actions { display: flex; gap: 8px; }
|
||||
.update-banner-btn {
|
||||
background: #fff;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
.update-banner-btn.ghostish {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.update-banner-btn:hover { filter: brightness(0.97); }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE = "puppy-tracker-v8";
|
||||
const CACHE = "puppy-tracker-v9";
|
||||
const PHOTO_CACHE = "puppy-tracker-photos-v1";
|
||||
const ASSETS = [
|
||||
"./",
|
||||
@@ -13,7 +13,13 @@ self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE).then((cache) => cache.addAll(ASSETS))
|
||||
);
|
||||
self.skipWaiting();
|
||||
// No skipWaiting() here: a new worker stays in "waiting" while an old one is
|
||||
// controlling a tab, so the page can prompt before swapping assets out from
|
||||
// under it. The page tells us to activate via a SKIP_WAITING message.
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
|
||||
Reference in New Issue
Block a user