diff --git a/index.html b/index.html index 4945a0a..8b91e4d 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ - + @@ -35,15 +35,34 @@
- - - - - + + + + + diff --git a/sw.js b/sw.js index 6def42e..227e1f3 100644 --- a/sw.js +++ b/sw.js @@ -1,10 +1,11 @@ -/* Baustelle PWA Service Worker. - * Cache-Strategie: - * - App-Shell (HTML/CSS/JS): cache-first, network update - * - API-Calls: network-first, kein offline-cache (da auth-pflichtig) +/* Baustelle PWA Service Worker — v9 + * Pattern nach claude-db #31: + * - Network-First für eigene Assets (immer aktuell, Fallback Cache) + * - skipWaiting() + clients.claim() damit Updates sofort greifen + * - Web Share Target via POST → share.html */ -const CACHE = 'baustelle-v8'; +const CACHE = 'baustelle-v9'; const SHELL = [ './', './index.html', @@ -18,13 +19,30 @@ const SHELL = [ './lib/router.js', './icons/icon-192.png', './icons/icon-512.png', + './icons/icon.svg', ]; -// Web Share Target: eingehende POSTs an share.html abfangen und in IDB zwischenspeichern +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 !== 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(); - // Alle bekannten Feldnamen durchprobieren — verschiedene Android-Gallery-Apps - // nutzen unterschiedliche Namen let files = []; for (const key of ['photos', 'file', 'files', 'image', 'images']) { const v = fd.getAll(key); @@ -32,10 +50,11 @@ async function handleShareTarget(request) { } // Fallback: alle File-Entries im FormData durchsuchen if (!files.length) { - for (const [k, v] of fd.entries()) { + 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); @@ -48,30 +67,19 @@ async function handleShareTarget(request) { req.onerror = () => rej(req.error); }); const tx = db.transaction('kv', 'readwrite'); - tx.objectStore('kv').put(files.map(f => ({ name: f.name, type: f.type, data: f })), 'shared_files'); - await new Promise(res => tx.oncomplete = res); + 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('install', (e) => { - e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL).catch(() => null))); - self.skipWaiting(); -}); - -self.addEventListener('activate', (e) => { - e.waitUntil( - caches.keys().then(keys => Promise.all( - keys.filter(k => k !== CACHE).map(k => caches.delete(k)) - )) - ); - self.clients.claim(); -}); - self.addEventListener('fetch', (e) => { const url = new URL(e.request.url); - // Web Share Target: POST auf share.html abfangen + // Web Share Target POST → share.html if (e.request.method === 'POST' && url.pathname.endsWith('/share.html')) { e.respondWith(handleShareTarget(e.request)); return; @@ -79,28 +87,36 @@ self.addEventListener('fetch', (e) => { // API-Requests: nicht cachen, durchreichen if (url.pathname.includes('/custom/bericht/api/')) { - return; // default network + return; } - // App-Shell: cache-first - if (e.request.method === 'GET' && url.origin === location.origin) { + // Nicht-GETs: durchreichen + if (e.request.method !== 'GET') { + return; + } + + // Eigene Assets: Network-First mit Cache-Fallback + if (url.origin === location.origin) { e.respondWith( - caches.match(e.request).then(hit => { - if (hit) { - // Background-Update - fetch(e.request).then(r => { - if (r.ok) caches.open(CACHE).then(c => c.put(e.request, r.clone())); - }).catch(() => null); - return hit; - } - return fetch(e.request).then(r => { - if (r.ok) { - const clone = r.clone(); - caches.open(CACHE).then(c => c.put(e.request, clone)); + 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 r; - }); - }) + 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(); } });