Files
MindSift/frontend/public/sw.js
T
sandrodachandClaude Opus 5.5 38b8c92899
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m15s
feat(notify): web push as a notification channel
- VAPID keys from VAPID_* or generated once (push_keys, migration 9);
  devices in push_subscriptions, forgotten on 404/410 from the push service
- push column for every event (also due tasks) and the daily summary,
  bundles as one notification; /push/key, /devices, /subscribe,
  /unsubscribe, /test
- settings: switch push on per device, test it, see the devices; the
  service worker shows notifications and focuses MindSift on click
- tests decrypt a sent push like a browser (RFC 8291) and check VAPID

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:21:55 +01:00

129 lines
4.3 KiB
JavaScript

// MindSift service worker: keeps the app shell so the PWA also starts without network.
// - Page loads (navigation): network first, so a new release is picked up right away; offline the
// last loaded index.html is used.
// - Built bundles (/assets/*, file names contain a content hash): from the cache, loaded once.
// - Icons and manifest: from the cache, refreshed in the background.
// - API requests are never touched; offline data comes from the app's own cache (lib/offline.ts).
// Raise VERSION when this file changes in a way that needs fresh caches.
const VERSION = 'v2';
const SHELL_CACHE = `mindsift-shell-${VERSION}`;
const ASSET_CACHE = `mindsift-assets-${VERSION}`;
const MAX_ASSETS = 80;
const SHELL_URLS = [
'/',
'/index.html',
'/manifest.json',
'/favicon.ico',
'/icon.svg',
'/icon-192x192.png',
'/icon-512x512.png',
'/apple-touch-icon.png',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(SHELL_CACHE)
.then((cache) => Promise.all(SHELL_URLS.map((url) => cache.add(new Request(url, { cache: 'reload' })).catch(() => {}))))
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== SHELL_CACHE && k !== ASSET_CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim())
);
});
// Old bundles pile up with every release; keep only the newest entries
async function trimAssets() {
const cache = await caches.open(ASSET_CACHE);
const keys = await cache.keys();
await Promise.all(keys.slice(0, Math.max(0, keys.length - MAX_ASSETS)).map((k) => cache.delete(k)));
}
async function networkFirstPage(request) {
const cache = await caches.open(SHELL_CACHE);
try {
const response = await fetch(request, { cache: 'no-store' });
if (response.ok && response.type === 'basic') cache.put('/index.html', response.clone());
return response;
} catch {
return (await cache.match('/index.html')) || (await cache.match('/')) || Response.error();
}
}
async function cacheFirstAsset(request) {
const cache = await caches.open(ASSET_CACHE);
const hit = await cache.match(request);
if (hit) return hit;
const response = await fetch(request);
if (response.ok && response.type === 'basic') {
await cache.put(request, response.clone());
trimAssets();
}
return response;
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(SHELL_CACHE);
const hit = await cache.match(request);
const refresh = fetch(request)
.then((response) => {
if (response.ok && response.type === 'basic') cache.put(request, response.clone());
return response;
})
.catch(() => hit || Response.error());
return hit || refresh;
}
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;
const url = new URL(request.url);
if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return;
if (request.mode === 'navigate') {
event.respondWith(networkFirstPage(request));
} else if (url.pathname.startsWith('/assets/')) {
event.respondWith(cacheFirstAsset(request));
} else if (SHELL_URLS.includes(url.pathname)) {
event.respondWith(staleWhileRevalidate(request));
}
});
// Web push: show the notification; a click focuses an open MindSift window or opens a new one
self.addEventListener('push', (event) => {
let msg = {};
try {
msg = event.data ? event.data.json() : {};
} catch {
msg = { body: event.data ? event.data.text() : '' };
}
event.waitUntil(
self.registration.showNotification(msg.title || 'MindSift', {
body: msg.body || '',
icon: '/icon-192x192.png',
badge: '/icon-192x192.png',
tag: msg.tag || undefined,
data: { url: msg.url || '/' },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const target = new URL((event.notification.data && event.notification.data.url) || '/', self.location.origin);
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windows) => {
for (const w of windows) {
if (new URL(w.url).origin === target.origin && 'focus' in w) return w.focus();
}
return self.clients.openWindow(target.href);
})
);
});