From d01f51a7d02ab8f5d83bdf8cc466b0a8f711f565 Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Wed, 1 Jul 2026 04:43:33 +0800 Subject: [PATCH] fix: send SKIP_WAITING to waiting SW, not active controller, to break reload loop --- frontend/app.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 6111142..935176a 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -2796,7 +2796,12 @@ function pollBuildTag() { // PWA — service worker registration and update banner (WEB mode only) // ============================================================================ -function showUpdateBanner() { +let _updateBannerShown = false; +function showUpdateBanner(waitingSW) { + // Only show the banner once per page load + if (_updateBannerShown) return; + _updateBannerShown = true; + // Use a persistent, click-to-reload toast instead of the standard 2.2s one const t = document.createElement('div'); t.className = 'toast toast-update'; @@ -2804,13 +2809,15 @@ function showUpdateBanner() { const container = $('toastContainer'); container.appendChild(t); t.querySelector('.toast-reload-btn').addEventListener('click', () => { - // Tell the waiting SW to skip waiting, then reload once it takes control. - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' }); + if (waitingSW) { + // Signal the *waiting* SW (not the active controller) to take over. + // Once it does, controllerchange fires and we reload. + waitingSW.postMessage({ type: 'SKIP_WAITING' }); + navigator.serviceWorker.addEventListener('controllerchange', () => window.location.reload(), { once: true }); + } else { + // No waiting SW (e.g. build-tag or SW_UPDATE_AVAILABLE path) — just reload. + window.location.reload(); } - navigator.serviceWorker.addEventListener('controllerchange', () => window.location.reload(), { once: true }); - // Fallback: reload after a short delay in case controllerchange already fired - setTimeout(() => window.location.reload(), 500); }); } @@ -2821,20 +2828,21 @@ async function registerServiceWorker() { // If a new SW is already waiting (e.g. user refreshed after an update), // show the banner right away. - if (reg.waiting) { showUpdateBanner(); return; } + if (reg.waiting) { showUpdateBanner(reg.waiting); return; } // Listen for a new SW installing after the page is open. reg.addEventListener('updatefound', () => { const sw = reg.installing; if (!sw) return; sw.addEventListener('statechange', () => { - if (sw.state === 'installed' && reg.waiting) showUpdateBanner(); + if (sw.state === 'installed' && reg.waiting) showUpdateBanner(reg.waiting); }); }); - // The SW can also broadcast SW_UPDATE_AVAILABLE on its own activate + // The SW can also broadcast SW_UPDATE_AVAILABLE on its own activate. + // In this case the new SW is already active, so just reload directly. navigator.serviceWorker.addEventListener('message', (e) => { - if (e.data && e.data.type === 'SW_UPDATE_AVAILABLE') showUpdateBanner(); + if (e.data && e.data.type === 'SW_UPDATE_AVAILABLE') showUpdateBanner(null); }); // Check for updates in the background (useful for long-lived sessions)