// public/sw-field.js — Tuxedo field-worker service worker. // // Cycle 6 sub-PWA, scoped to /water. Tighter cache strategy than // the main shell SW because: // - The field app is a thin surface (PIN, headgate list, log form, // time tab). Few async chunks; mostly static + the latest data // snapshot. // - "Offline" here means "still see the cached PIN screen + headgate // list" — water entry without connectivity is a degraded but // non-blocking experience (the form queue handles drafts). // // Cache names are deliberately prefixed with `field-` so they never // collide with the main `rc-shell-v*` / `rc-data-v*` caches. const SHELL_CACHE = "field-shell-v1"; const DATA_CACHE = "field-data-v1"; const OFFLINE_URL = "/offline.html"; const SHELL_ASSETS = [ "/water", "/manifest-field.json", "/favicon.svg", "/og-default.svg", "/offline.html", "/icons/icon-192x192.png", "/icons/icon-512x512.png", ]; self.addEventListener("install", (event) => { event.waitUntil( caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS)) ); self.skipWaiting(); }); self.addEventListener("activate", (event) => { // Only manage field-* caches — never touch the main app's caches. event.waitUntil( Promise.all([ caches.keys().then((cacheNames) => { const toDelete = []; for (const name of cacheNames) { if ( name !== SHELL_CACHE && name !== DATA_CACHE && name.startsWith("field-") ) { toDelete.push(caches.delete(name)); } } return Promise.all(toDelete); }), self.clients.claim(), ]) ); }); 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; // Out-of-scope requests: skip (let the main app SW handle them // if it's installed for that scope; fall through to network). if (!url.pathname.startsWith("/water")) return; // Navigations → network-first, fall back to cache, fall back to offline page. if (req.mode === "navigate") { event.respondWith( fetch(req) .then((res) => { const clone = res.clone(); caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone)); return res; }) .catch(() => caches.match(req).then( (cached) => cached ?? caches.match(OFFLINE_URL), ), ), ); return; } // Static assets → cache-first if ( url.pathname.startsWith("/_next/static/") || url.pathname.startsWith("/icons/") || url.pathname.endsWith(".svg") || url.pathname.endsWith(".woff2") ) { event.respondWith( caches.match(req).then((cached) => { if (cached) return cached; return fetch(req).then((res) => { const clone = res.clone(); if (res.status === 200) { caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone)); } return res; }); }), ); return; } // /api/water* GETs → stale-while-revalidate (so the headgate list // and worker availability can hydrate from cache while we re-fetch). if (url.pathname.startsWith("/api/water")) { event.respondWith( caches.open(DATA_CACHE).then((cache) => cache.match(req).then((cached) => { const network = fetch(req) .then((res) => { if (res.status === 200) cache.put(req, res.clone()); return res; }) .catch(() => cached); return cached ?? network; }), ), ); return; } // Default: try network, fall back to cache. event.respondWith(fetch(req).catch(() => caches.match(req))); });