/* Baustelle PWA Service Worker * Pattern nach claude-db #201 (filemtime-basiert): * - Cache-Version kommt aus URL-Query ?v=, kein manuelles Hochzählen * - Network-First für eigene Assets (immer aktuell, Fallback Cache) * - skipWaiting() + clients.claim() damit Updates sofort greifen * - Web Share Target via POST → share.html */ // Cache-Version dynamisch aus ?v= — wird von index.php gesetzt const SW_VERSION = (new URL(self.location.href)).searchParams.get('v') || 'static'; const CACHE = 'baustelle-' + SW_VERSION; const SHELL = [ './', './index.php', './share.html', './icons/icon-192.png', './icons/icon-512.png', './icons/icon.svg', // CSS/JS werden mit ?v= geladen und dynamisch gecached // beim ersten fetch — nicht in SHELL damit Pre-Cache nicht mit alten v's läuft ]; self.addEventListener('install', (e) => { e.waitUntil( caches.open(CACHE) .then(c => c.addAll(SHELL).catch(() => null)) .then(() => self.skipWaiting()) ); }); self.addEventListener('activate', (e) => { e.waitUntil( caches.keys() .then(keys => Promise.all( keys.filter(k => k.startsWith('baustelle-') && k !== CACHE) .map(k => caches.delete(k)) )) .then(() => self.clients.claim()) ); }); /* Web Share Target: POST auf share.html abfangen */ async function handleShareTarget(request) { const fd = await request.formData(); let files = []; for (const key of ['photos', 'file', 'files', 'image', 'images']) { const v = fd.getAll(key); if (v && v.length) files = files.concat(v); } // Fallback: alle File-Entries im FormData durchsuchen if (!files.length) { for (const [, v] of fd.entries()) { if (v && typeof v === 'object' && v.size !== undefined) files.push(v); } } if (files.length) { const db = await new Promise((res, rej) => { const req = indexedDB.open('baustelle-pwa-v1', 1); req.onupgradeneeded = () => { const d = req.result; if (!d.objectStoreNames.contains('kv')) d.createObjectStore('kv'); if (!d.objectStoreNames.contains('queue')) d.createObjectStore('queue', { keyPath: 'id', autoIncrement: true }); }; req.onsuccess = () => res(req.result); req.onerror = () => rej(req.error); }); const tx = db.transaction('kv', 'readwrite'); tx.objectStore('kv').put( files.map(f => ({ name: f.name || 'photo.jpg', type: f.type || 'image/jpeg', data: f })), 'shared_files' ); await new Promise(res => { tx.oncomplete = res; }); } return Response.redirect('./share.html', 303); } self.addEventListener('fetch', (e) => { const url = new URL(e.request.url); // Web Share Target POST → share.html if (e.request.method === 'POST' && url.pathname.endsWith('/share.html')) { e.respondWith(handleShareTarget(e.request)); return; } // API-Requests: nicht cachen, durchreichen if (url.pathname.includes('/custom/bericht/api/')) { return; } // Nicht-GETs: durchreichen if (e.request.method !== 'GET') { return; } // Eigene Assets: Network-First mit Cache-Fallback if (url.origin === location.origin) { e.respondWith( fetch(e.request) .then(response => { // Cache aktualisieren (nur erfolgreiche responses) if (response && response.ok) { const clone = response.clone(); caches.open(CACHE).then(c => c.put(e.request, clone)).catch(() => null); } return response; }) .catch(() => caches.match(e.request)) ); return; } }); /* Message-Handler: Client kann "SKIP_WAITING" schicken um ein wartenden SW * sofort zu aktivieren (für In-App-Update-Button). */ self.addEventListener('message', (e) => { if (e.data && e.data.type === 'SKIP_WAITING') { self.skipWaiting(); } });