diff --git a/web/public/icons/icon-192.png b/web/public/icons/icon-192.png new file mode 100644 index 0000000..1fa3d9d Binary files /dev/null and b/web/public/icons/icon-192.png differ diff --git a/web/public/icons/icon-512.png b/web/public/icons/icon-512.png new file mode 100644 index 0000000..3e6dfff Binary files /dev/null and b/web/public/icons/icon-512.png differ diff --git a/web/public/sw.js b/web/public/sw.js index 6dd568d..c98090b 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -1,58 +1,52 @@ -const CACHE_NAME = 'dumpster-v1'; -const STATIC_ASSETS = [ - '/', - '/index.html', - '/manifest.json', -]; +// ponytail: bump CACHE_NAME on each deploy, or stale HTML breaks assets. +// Network-first for navigation (index.html) keeps hashed asset refs fresh. +const CACHE_NAME = 'dumpster-v2'; -self.addEventListener('install', (event) => { - event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - return cache.addAll(STATIC_ASSETS); - }) - ); +self.addEventListener('install', () => { self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil( - caches.keys().then((cacheNames) => { - return Promise.all( - cacheNames - .filter((name) => name !== CACHE_NAME) - .map((name) => caches.delete(name)) - ); - }) + caches.keys().then((names) => + Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n))) + ) ); self.clients.claim(); }); self.addEventListener('fetch', (event) => { - // Skip non-GET requests if (event.request.method !== 'GET') return; + const url = new URL(event.request.url); - // Skip API requests - if (event.request.url.includes('/api/')) return; + // Skip API + WS + if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws')) return; - // Skip WebSocket - if (event.request.url.includes('/ws')) return; + // Network-first for navigation requests (index.html) — always fetch fresh HTML + // so hashed asset references stay current after deploys. + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((c) => c.put(event.request, clone)); + return response; + }) + .catch(() => caches.match('/index.html')) + ); + return; + } + // Cache-first for static assets (JS, CSS, images, fonts) event.respondWith( caches.match(event.request).then((cached) => { - // Return cached version, fetch new one in background const fetchPromise = fetch(event.request).then((response) => { - // Only cache successful responses if (response.ok) { const clone = response.clone(); - caches.open(CACHE_NAME).then((cache) => { - cache.put(event.request, clone); - }); + caches.open(CACHE_NAME).then((c) => c.put(event.request, clone)); } return response; - }).catch(() => { - // Return cached if fetch fails - return cached; - }); + }).catch(() => cached); return cached || fetchPromise; }) @@ -61,35 +55,27 @@ self.addEventListener('fetch', (event) => { self.addEventListener('push', (event) => { if (!event.data) return; - const data = event.data.json(); - const options = { - body: data.body, - icon: '/icons/icon-192.png', - badge: '/icons/badge.png', - vibrate: [100, 50, 100], - data: { - url: data.url || '/', - }, - }; - event.waitUntil( - self.registration.showNotification(data.title || 'Dumpster', options) + self.registration.showNotification(data.title || 'Dumpster', { + body: data.body, + icon: '/icons/icon-192.png', + badge: '/icons/icon-192.png', + vibrate: [100, 50, 100], + data: { url: data.url || '/' }, + }) ); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); - event.waitUntil( - clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { - // Focus existing window if available - for (const client of clientList) { + clients.matchAll({ type: 'window', includeUncontrolled: true }).then((list) => { + for (const client of list) { if (client.url.includes(event.notification.data.url) && 'focus' in client) { return client.focus(); } } - // Otherwise open new window return clients.openWindow(event.notification.data.url); }) );