added on local at 2026-07-01 13:47:45
| 1 | // AffSoft service worker -- minimal network-first with offline fallback. | |
| 2 | // Keeps the mobile dashboard usable while the device is briefly offline | |
| 3 | // (commute, elevator, etc.) without staling out fresh stats when online. | |
| 4 | const CACHE = 'affsoft-v1'; | |
| 5 | const STATIC_ASSETS = [ | |
| 6 | '/manifest.webmanifest', | |
| 7 | '/assets/css/site.css', | |
| 8 | ]; | |
| 9 | ||
| 10 | self.addEventListener('install', e => { | |
| 11 | e.waitUntil(caches.open(CACHE).then(c => c.addAll(STATIC_ASSETS)).then(() => self.skipWaiting())); | |
| 12 | }); | |
| 13 | ||
| 14 | self.addEventListener('activate', e => { | |
| 15 | e.waitUntil( | |
| 16 | caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))) | |
| 17 | .then(() => self.clients.claim()) | |
| 18 | ); | |
| 19 | }); | |
| 20 | ||
| 21 | // Web Push: show notification when AffSoft pings us with new earnings. | |
| 22 | self.addEventListener('push', e => { | |
| 23 | let data = { title: 'AffSoft', body: 'You have new activity.' }; | |
| 24 | try { data = e.data.json(); } catch(_) { try { data.body = e.data.text(); } catch(_){} } | |
| 25 | const title = data.title || 'AffSoft'; | |
| 26 | const body = data.body || ''; | |
| 27 | const url = data.url || '/m.cgi'; | |
| 28 | e.waitUntil(self.registration.showNotification(title, { | |
| 29 | body, | |
| 30 | icon: '/assets/icon-192.png', | |
| 31 | badge: '/assets/icon-192.png', | |
| 32 | data: { url }, | |
| 33 | vibrate: [120, 60, 120], | |
| 34 | tag: data.tag || 'affsoft-notify', | |
| 35 | })); | |
| 36 | }); | |
| 37 | ||
| 38 | self.addEventListener('notificationclick', e => { | |
| 39 | e.notification.close(); | |
| 40 | const url = (e.notification.data && e.notification.data.url) || '/m.cgi'; | |
| 41 | e.waitUntil(clients.matchAll({type:'window'}).then(list => { | |
| 42 | for (const c of list) { | |
| 43 | if (c.url.includes(url.split('?')[0]) && 'focus' in c) return c.focus(); | |
| 44 | } | |
| 45 | return clients.openWindow(url); | |
| 46 | })); | |
| 47 | }); | |
| 48 | ||
| 49 | self.addEventListener('fetch', e => { | |
| 50 | const req = e.request; | |
| 51 | if (req.method !== 'GET') return; | |
| 52 | const url = new URL(req.url); | |
| 53 | if (url.origin !== self.location.origin) return; | |
| 54 | ||
| 55 | // Network-first for CGI; falls back to cache only if offline. | |
| 56 | // Static assets: cache-first, populate from network. | |
| 57 | if (url.pathname.endsWith('.cgi') || url.pathname === '/m.cgi' || url.pathname === '/') { | |
| 58 | e.respondWith( | |
| 59 | fetch(req).then(res => { | |
| 60 | if (res && res.ok) { | |
| 61 | const clone = res.clone(); | |
| 62 | caches.open(CACHE).then(c => c.put(req, clone)).catch(()=>{}); | |
| 63 | } | |
| 64 | return res; | |
| 65 | }).catch(() => caches.match(req).then(c => c || new Response( | |
| 66 | '<h1>Offline</h1><p>Reconnect to refresh your stats.</p>', | |
| 67 | { headers: { 'Content-Type': 'text/html' } } | |
| 68 | ))) | |
| 69 | ); | |
| 70 | return; | |
| 71 | } | |
| 72 | ||
| 73 | // Static: cache-first | |
| 74 | e.respondWith( | |
| 75 | caches.match(req).then(cached => cached || fetch(req).then(res => { | |
| 76 | if (res && res.ok) { | |
| 77 | const clone = res.clone(); | |
| 78 | caches.open(CACHE).then(c => c.put(req, clone)).catch(()=>{}); | |
| 79 | } | |
| 80 | return res; | |
| 81 | })) | |
| 82 | ); | |
| 83 | }); |