// Service worker. // Goal: keep the app usable on flaky/no network without ever serving stale DATA. // • App SHELL (HTML/CSS/JS/manifest) — network-first, fall back to cache so the // app still loads when offline. Successful fetches refresh the cache. // • API (/api/*) — never cached. The client handles offline itself (it queues // check-offs in localStorage and syncs on reconnect). const SW_VERSION = 'v5'; const SHELL_CACHE = 'shell-' + SW_VERSION; const SHELL = [ '/', '/index.html', '/app.js', '/styles.css', '/md.js', '/manifest.webmanifest', '/vendor/motion.global.js' ]; self.addEventListener('install', (event) => { event.waitUntil( // Per-URL (not atomic addAll): one missing/404 entry must not reject the whole batch and // leave the new shell cache empty — otherwise a hard-offline navigation right after a // version bump would fall through to the offline page. allSettled caches what it can. caches.open(SHELL_CACHE) .then((c) => Promise.allSettled(SHELL.map((u) => c.add(u)))) .catch(() => {}) ); self.skipWaiting(); // activate the new worker on next load }); self.addEventListener('activate', (event) => { event.waitUntil((async () => { const keys = await caches.keys(); await Promise.all(keys.filter((k) => k !== SHELL_CACHE).map((k) => caches.delete(k))); await self.clients.claim(); })()); }); const OFFLINE_HTML = 'Offline

Offline

The checklist needs an internet connection the first time. Try again when you\'re back online.

'; self.addEventListener('fetch', (event) => { const req = event.request; if (req.method !== 'GET') return; const url = new URL(req.url); if (url.origin !== self.location.origin) return; // third-party: passthrough if (url.pathname.startsWith('/api/')) return; // never cache API; client handles offline // Full-resolution equipment photos can be several MB each and pile up fast; caching them // in the shell risks blowing the (small, all-or-nothing) iOS cache quota and evicting the // app shell. They already carry an immutable 1-year HTTP cache header, so let the browser's // HTTP cache handle them and keep them out of the shell cache entirely. if (url.pathname.startsWith('/equip-images/')) return; event.respondWith((async () => { try { const fresh = await fetch(req); // Cache successful shell/static GETs for offline fallback. if (fresh && fresh.ok && fresh.type === 'basic') { const cache = await caches.open(SHELL_CACHE); cache.put(req, fresh.clone()); } return fresh; } catch (_) { const cached = await caches.match(req); if (cached) return cached; if (req.mode === 'navigate') { const idx = (await caches.match('/index.html')) || (await caches.match('/')); if (idx) return idx; return new Response(OFFLINE_HTML, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } throw new Error('Offline'); } })()); });