1// Kometri service worker. 2// 3// v1 scope: PWA installability + push notification reception. 4// Deliberately NO caching (no fetch handler) â adding caching naively is 5// the easiest way to ship stale JS/CSS post-deploy. When we want offline 6// support later, plan: 7// - HTML: network-first (always get latest app shell) 8// - JS/CSS hashed assets: cache-first (immutable per-deploy) 9// - API: network-only (do NOT cache user data) 10// - manifest.json, sw.js itself: network-only 11// 12// Push payload contract (sent from backend/app/services/push_service.py 13// when that ships): 14// { 15// "title": "ð¥ Day 5's drop is ready", 16// "body": "Today's theme: café Italian â 3 min", 17// "url": "/discover", // optional, defaults to /dashboard 18// "tag": "daily-drop", // optional, prevents stacking duplicates 19// "icon": "/logo-512.svg", // optional, defaults below 20// "badge": "/logo-512.svg" // optional, defaults below 21// } 22 23self.addEventListener('install', () => { 24 self.skipWaiting() 25}) 26 27self.addEventListener('activate', (event) => { 28 event.waitUntil(self.clients.claim()) 29}) 30 31self.addEventListener('push', (event) => { 32 // Defensive parse â if backend sends malformed JSON or a plain string 33 // we still want SOMETHING to show, not a silent drop. 34 let payload = {} 35 if (event.data) { 36 try { 37 payload = event.data.json() 38 } catch (e) { 39 payload = { title: 'Kometri', body: event.data.text() || '' } 40 } 41 } 42 43 const title = payload.title || 'Kometri' 44 const options = { 45 body: payload.body || '', 46 icon: payload.icon || '/logo-512.svg', 47 badge: payload.badge || '/logo-512.svg', 48 tag: payload.tag, // dedupes â new push with same tag replaces previous 49 renotify: !!payload.tag, // ring/buzz even if same tag (intentional re-alert) 50 data: { url: payload.url || '/dashboard' }, 51 // requireInteraction: false (default) â fades after a few seconds. 52 // Set to true via payload for "streak ends in 4 hours" urgency notifs. 53 requireInteraction: payload.requireInteraction === true, 54 } 55 56 event.waitUntil(self.registration.showNotification(title, options)) 57}) 58 59self.addEventListener('notificationclick', (event) => { 60 event.notification.close() 61 const targetUrl = (event.notification.data && event.notification.data.url) || '/dashboard' 62 63 event.waitUntil( 64 self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { 65 // If a Kometri tab is already open, focus it and navigate. 66 for (const client of clientList) { 67 if (client.url.includes(self.location.origin)) { 68 client.focus() 69 if ('navigate' in client) { 70 return client.navigate(targetUrl) 71 } 72 return 73 } 74 } 75 // Otherwise open a fresh tab at the target URL. 76 return self.clients.openWindow(targetUrl) 77 }) 78 ) 79})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.