const CACHE_NAME = "roast-planner-static-__ASSET_VERSION__"; // Only /app is precached as a navigable page: it is a data-free authenticated shell (draft // data lives only in account-scoped local storage, and logout clears that namespace), so it is // the one page that is both safe and useful to relaunch offline. The marketing page, auth // pages, account, admin, inventory, and cupping all depend on the network (auth checks, live // data) and are deliberately left off this list rather than cached in a way that could show // stale content. const APP_SHELL = [ "/app", "/app.css?v=__ASSET_VERSION__", "/worksheet.css?v=__ASSET_VERSION__", "/manifest.webmanifest?v=__ASSET_VERSION__", "/icon.svg?v=__ASSET_VERSION__", "/brand-mark.svg?v=__ASSET_VERSION__", "/js/main.js?v=__ASSET_VERSION__", "/js/api.js?v=__ASSET_VERSION__", "/js/nav.js?v=__ASSET_VERSION__", "/js/toast.js?v=__ASSET_VERSION__", "/js/why-panels.js?v=__ASSET_VERSION__", "/js/field-help.js?v=__ASSET_VERSION__", "/js/field-help-content.js?v=__ASSET_VERSION__", "/js/lot-picker.js?v=__ASSET_VERSION__", "/js/plan-chat-ui.js?v=__ASSET_VERSION__", "/js/prefill-ui.js?v=__ASSET_VERSION__", "/js/alog-ui.js?v=__ASSET_VERSION__", "/js/print.js?v=__ASSET_VERSION__", "/shared/fields.js?v=__ASSET_VERSION__", "/shared/ledger.js?v=__ASSET_VERSION__", "/shared/time.js?v=__ASSET_VERSION__", "/shared/reference-data.js?v=__ASSET_VERSION__", "/shared/curve.js?v=__ASSET_VERSION__", "/shared/cupping.js?v=__ASSET_VERSION__", ]; self.addEventListener("install", (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)), ); }); self.addEventListener("activate", (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all( keys .filter((key) => key !== CACHE_NAME) .map((key) => caches.delete(key)), ), ), ); self.clients.claim(); }); self.addEventListener("fetch", (event) => { const { request } = event; let url; try { url = new URL(request.url); } catch { return; } if ( request.method !== "GET" || url.origin !== self.location.origin || url.pathname.startsWith("/api/") || // Media streams use Range requests (206 responses the Cache API rejects) and are far // too large to belong in the static cache — let the browser handle them natively. url.pathname.startsWith("/academy-audio/") ) return; if (request.mode === "navigate") { // Only /app has a meaningful offline shell (see APP_SHELL comment above); every other // page requires the network anyway, so let those navigations fail normally when offline // rather than risk serving stale auth/account/admin content. if (url.pathname !== "/app") return; event.respondWith( fetch(request).catch( async () => (await caches.match("/app")) || Response.error(), ), ); return; } // Prefer fresh JavaScript and styles so installed clients receive UI/security updates // immediately; use the cache only when offline. event.respondWith( fetch(request) .then((response) => { // Only full 200 responses: cache.put() throws on 206 partials. if (response.status === 200) caches .open(CACHE_NAME) .then((cache) => cache.put(request, response.clone())) .catch(() => {}); return response; }) .catch(() => caches.match(request)), ); }); self.addEventListener("message", (event) => { if (event.data === "SKIP_WAITING") self.skipWaiting(); });