From ebfc3be9d1f402bf29780d4a8f43286f6e1c8e99 Mon Sep 17 00:00:00 2001 From: Sami Date: Sun, 17 May 2026 01:41:47 -0700 Subject: [PATCH] fix(build): derive sw.js cache tag from bundle content hash The service worker uses staleWhileRevalidate on /dist/*, so after a release it would serve old bundles from cache indefinitely (cache is only wiped when the cache *name* changes, which was hardcoded to 'dashcaddy-shell-v10'). Result: dashboard appears unchanged after a self-update until the user manually unregisters the SW. build.js now hashes the concatenated dist bundles and writes `dashcaddy-shell-<10-hex-chars>` into sw.js. Any change in dist/ produces a fresh cache name; on the next page load the SW's activate handler deletes all older caches and the new bundles are fetched. --- status/build.js | 26 +++++++++++++++++++++++++- status/sw.js | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/status/build.js b/status/build.js index c33e3ee..8713f13 100644 --- a/status/build.js +++ b/status/build.js @@ -6,6 +6,7 @@ const esbuild = require('esbuild'); const JS = (...parts) => path.join(__dirname, 'js', ...parts); const DIST = path.join(__dirname, 'dist'); const INDEX_HTML = path.join(__dirname, 'index.html'); +const SW_JS = path.join(__dirname, 'sw.js'); // Bundle definitions — files are concatenated in order, then minified const bundles = { @@ -129,6 +130,7 @@ async function build() { } const cspHash = updateInlineScriptCspHash(); + const cacheTag = updateServiceWorkerCache(); // Summary console.log('\n DashCaddy Frontend Build\n'); @@ -143,7 +145,29 @@ async function build() { console.log(' ─────────────────────────────────────────'); console.log(` ${'Total'.padEnd(18)} ${totalRaw.toFixed(1).padStart(6)} KB ${totalMin.toFixed(1).padStart(6)} KB`); console.log(`\n Output: ${DIST}`); - console.log(` CSP Hash: sha256-${cspHash}\n`); + console.log(` CSP Hash: sha256-${cspHash}`); + console.log(` SW Cache: dashcaddy-shell-${cacheTag}\n`); +} + +// Rewrites the CACHE constant in sw.js to a tag derived from the bundle +// contents. Every change in dist/ produces a new cache name; on next load +// the SW's activate handler wipes all older caches, so users never get +// stuck on stale precached bundles after a release. +function updateServiceWorkerCache() { + const sw = fs.readFileSync(SW_JS, 'utf8'); + const hash = crypto.createHash('sha256'); + for (const name of Object.keys(bundles)) { + hash.update(fs.readFileSync(path.join(DIST, name))); + } + const tag = hash.digest('hex').slice(0, 10); + const updated = sw.replace( + /const CACHE = 'dashcaddy-shell-[^']+';/, + `const CACHE = 'dashcaddy-shell-${tag}';` + ); + if (updated !== sw) { + fs.writeFileSync(SW_JS, updated); + } + return tag; } // Watch mode diff --git a/status/sw.js b/status/sw.js index 8bfc8ed..99a5ddb 100644 --- a/status/sw.js +++ b/status/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'dashcaddy-shell-v10'; +const CACHE = 'dashcaddy-shell-c6600af35d'; const PRECACHE = [ '/', '/index.html',