/* ============================================================ BAN YARO — Service Worker Offline-Cache + Push Notifications ============================================================ */ const CACHE_VERSION = 'by-v13'; const CACHE_STATIC = `${CACHE_VERSION}-static`; // Diese Dateien werden beim Install gecacht (App Shell) const STATIC_ASSETS = [ '/', '/css/design-system.css', '/css/layout.css', '/css/components.css', '/js/api.js', '/js/ui.js', '/js/app.js', '/manifest.json', '/icons/icon-192.png', ]; // ---------------------------------------------------------- // INSTALL — App Shell cachen // ---------------------------------------------------------- self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_STATIC) .then(cache => cache.addAll(STATIC_ASSETS)) .then(() => self.skipWaiting()) ); }); // ---------------------------------------------------------- // ACTIVATE — alte Caches aufräumen // ---------------------------------------------------------- self.addEventListener('activate', event => { event.waitUntil( caches.keys() .then(keys => Promise.all( keys.filter(k => k !== CACHE_STATIC).map(k => caches.delete(k)) )) .then(() => self.clients.claim()) ); }); // ---------------------------------------------------------- // FETCH — Cache-First für statische Assets, Network-First für API // ---------------------------------------------------------- self.addEventListener('fetch', event => { const url = new URL(event.request.url); // API-Calls: immer Network, kein Cache if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(event.request).catch(() => new Response(JSON.stringify({ detail: 'Offline — keine Verbindung.' }), { status: 503, headers: { 'Content-Type': 'application/json' } }) ) ); return; } // Statische Assets: Cache-First event.respondWith( caches.match(event.request) .then(cached => cached || fetch(event.request) .then(response => { // Erfolgreiche Responses für statische Assets cachen if (response.ok && event.request.method === 'GET') { const clone = response.clone(); caches.open(CACHE_STATIC).then(c => c.put(event.request, clone)); } return response; }) ) .catch(() => { // Offline-Fallback: App Shell zurückgeben if (event.request.mode === 'navigate') { return caches.match('/'); } }) ); }); // ---------------------------------------------------------- // PUSH NOTIFICATIONS // ---------------------------------------------------------- self.addEventListener('push', event => { if (!event.data) return; const data = event.data.json(); const options = { body: data.body || '', icon: data.icon || '/icons/icon-192.png', badge: data.badge || '/icons/icon-192.png', tag: data.tag || 'ban-yaro', data: data.data || {}, actions: data.actions || [], vibrate: data.vibrate || [100, 50, 100], requireInteraction: data.requireInteraction || false, }; // Giftköder-Alarm: besondere Darstellung if (data.type === 'poison_alert') { options.tag = 'poison-alert'; options.requireInteraction = true; options.vibrate = [200, 100, 200, 100, 200]; options.actions = [ { action: 'view', title: 'Auf Karte zeigen' }, { action: 'dismiss', title: 'Verstanden' }, ]; } event.waitUntil( self.registration.showNotification(data.title || 'Ban Yaro', options) ); }); // ---------------------------------------------------------- // NOTIFICATION CLICK // ---------------------------------------------------------- self.addEventListener('notificationclick', event => { event.notification.close(); const data = event.notification.data; const action = event.action; let url = '/'; if (action === 'view' || data?.page) { url = `/#${data.page || 'poison'}`; if (data.id) url += `?id=${data.id}`; } event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(windowClients => { // Offenes Fenster fokussieren for (const client of windowClients) { if (client.url.includes(self.location.origin)) { client.focus(); client.navigate(url); return; } } // Neues Fenster öffnen return clients.openWindow(url); }) ); });