// Service Worker: hält die Programmdateien vor, damit die App auch ohne // Verbindung startet. Das Zwischenspeichern der Listendaten kommt in // Phase 4 - hier geht es nur um die Hülle. "use strict"; // Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet, // wann alte Zwischenspeicher verworfen werden. const VERSION = "v29"; const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`; const SHELL = [ "/", "/app.css", "/impressum.html", "/datenschutz.html", "/js/app.js", "/js/api.js", "/js/dom.js", "/js/store.js", "/js/appbar.js", "/js/db.js", "/js/sync.js", "/js/push.js", "/js/sortable.js", "/js/barcode.js", "/js/views/scanner.js", "/js/views/scan-result.js", "/js/views/articles.js", "/js/views/prices.js", "/js/views/settings.js", "/js/views/admin.js", "/js/views/welcome.js", "/js/views/auth.js", "/js/views/lists.js", "/js/views/list-detail.js", "/js/views/manage.js", "/js/views/share.js", "/js/views/public.js", "/icons/icon-192.png", "/icons/icon-512.png", ]; self.addEventListener("install", (event) => { event.waitUntil( caches.open(SHELL_CACHE) .then((cache) => cache.addAll(SHELL)) .then(() => self.skipWaiting()) ); }); self.addEventListener("activate", (event) => { event.waitUntil( caches.keys() .then((keys) => Promise.all( keys.filter((k) => k !== SHELL_CACHE).map((k) => caches.delete(k)) )) .then(() => self.clients.claim()) ); }); 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; // API-Antworten NICHT zwischenspeichern. Eine veraltete Liste aus dem // Cache wäre schlimmer als gar keine - in Phase 4 übernimmt IndexedDB // diese Aufgabe kontrolliert. if (url.pathname.startsWith("/api/")) return; // Der Ereigniskanal ist eine dauerhaft offene Verbindung. Ginge er // durch den Service Worker, würde die Antwort gepuffert und nie // ausgeliefert. if (request.headers.get("accept") === "text/event-stream") return; // Navigationen: erst Netz, bei Fehlschlag die zwischengespeicherte Hülle. if (request.mode === "navigate") { event.respondWith( fetch(request).catch(() => caches.match("/", { ignoreSearch: true })) ); return; } // Programmdateien: erst Cache, im Hintergrund auffrischen. event.respondWith( caches.match(request).then((cached) => { const network = fetch(request) .then((response) => { if (response.ok) { const copy = response.clone(); caches.open(SHELL_CACHE).then((cache) => cache.put(request, copy)); } return response; }) .catch(() => cached); return cached || network; }) ); }); // --------------------------------------------------------------------------- // Push-Benachrichtigungen // --------------------------------------------------------------------------- self.addEventListener("push", (event) => { let payload = {}; try { payload = event.data ? event.data.json() : {}; } catch { // Kein JSON - dann eben ohne Angaben anzeigen, statt zu schweigen. } const title = payload.title || "Einkaufsliste"; const options = { body: payload.body || "Es gibt Änderungen.", icon: "/icons/icon-192.png", badge: "/icons/icon-192.png", // Gleiches tag = die neue Meldung ersetzt die alte, statt sich zu // stapeln. Zusammen mit der Drosselung auf dem Server bleibt es // bei höchstens einer sichtbaren Meldung je Liste. tag: payload.tag || "einkaufsliste", renotify: false, data: { list_id: payload.list_id || null }, }; event.waitUntil(self.registration.showNotification(title, options)); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); const listId = event.notification.data?.list_id; const target = listId ? `/#/lists/${listId}` : "/"; event.waitUntil( (async () => { const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true, }); // Ein bereits offenes Fenster wiederverwenden, statt ein zweites // aufzumachen - sonst hat man nach einer Woche zehn Tabs. for (const client of windows) { if (new URL(client.url).origin === self.location.origin) { await client.navigate(target).catch(() => {}); return client.focus(); } } return self.clients.openWindow(target); })() ); });