const CACHE = "{{CACHE_VERSION}}"; const ASSETS = {{ASSETS_JSON}}; self.addEventListener("install", (event) => { event.waitUntil( caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting()) ); }); self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((names) => Promise.all(names.filter((n) => n !== CACHE).map((n) => caches.delete(n))) ).then(() => self.clients.claim()) ); }); self.addEventListener("message", (event) => { if (event.data?.type === "SKIP_WAITING") self.skipWaiting(); }); function isHashedAsset(pathname) { return pathname.startsWith("/assets/"); } async function networkOnly(request) { return fetch(request); } async function networkFirst(request, fallbackPath) { try { const response = await fetch(request); if (response.ok) return response; } catch { /* offline */ } const cached = await caches.match(request); if (cached) return cached; if (fallbackPath) { const fallback = await caches.match(fallbackPath); if (fallback) return fallback; } return new Response("Offline", { status: 503, statusText: "Offline" }); } async function cacheFirst(request) { const cached = await caches.match(request); if (cached) return cached; const response = await fetch(request); if (response.ok) { const cache = await caches.open(CACHE); await cache.put(request, response.clone()); } return response; } self.addEventListener("fetch", (event) => { const { request } = event; if (request.method !== "GET") return; const url = new URL(request.url); if (url.origin !== self.location.origin) return; // sw.js: never intercept — browser must revalidate with server if (url.pathname === "/sw.js") return; if (url.pathname.startsWith("/api/")) { event.respondWith( fetch(request).catch(() => new Response(JSON.stringify({ offline: true }), { status: 503, headers: { "Content-Type": "application/json" }, })) ); return; } if (url.pathname === "/version.json") { event.respondWith(networkOnly(request)); return; } if (request.mode === "navigate") { event.respondWith(networkFirst(request, "/index.html")); return; } if (isHashedAsset(url.pathname)) { event.respondWith(cacheFirst(request)); return; } if (url.pathname === "/index.html" || url.pathname === "/manifest.json") { event.respondWith(networkFirst(request)); return; } // Icons and other static root files event.respondWith(networkFirst(request)); }); self.addEventListener("push", (event) => { if (!event.data) return; let data = {}; try { data = event.data.json(); } catch { data = { body: event.data.text() }; } const options = { body: data.body || "Med-Time!", icon: "/icon-192x192.png", badge: "/icon-72x72.png", silent: data.silent !== false, tag: data.tag || "med-reminder", renotify: true, data: { slot_id: data.slot_id }, actions: [ { action: "take", title: "Genommen ✓" }, { action: "snooze15", title: "+15 Min" }, { action: "snooze30", title: "+30 Min" }, ], }; event.waitUntil( self.registration.showNotification(data.title || "TakeYourMeds", options) ); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); const slotId = event.notification.data?.slot_id; const action = event.action; if (action === "take" && slotId) { event.waitUntil(self.clients.openWindow(`/?action=take&slot=${slotId}`)); return; } if ((action === "snooze15" || action === "snooze30") && slotId) { const minutes = action === "snooze15" ? 15 : 30; event.waitUntil(self.clients.openWindow(`/?action=snooze&slot=${slotId}&minutes=${minutes}`)); return; } event.waitUntil(self.clients.openWindow("/")); }); self.addEventListener("sync", (event) => { if (event.tag === "sync-logs") { event.waitUntil( self.clients.matchAll().then((list) => { list.forEach((c) => c.postMessage({ type: "SYNC_QUEUE" })); }) ); } });