// Beszel Service Worker const CACHE_NAME = "beszel-v1" const STATIC_ASSETS = ["/", "/index.html", "/manifest.json", "/favicon.ico", "/favicon.svg"] // Install event - cache static assets self.addEventListener("install", (event) => { event.waitUntil( caches .open(CACHE_NAME) .then((cache) => { return cache.addAll(STATIC_ASSETS) }) .then(() => self.skipWaiting()) ) }) // Activate event - clean up old caches self.addEventListener("activate", (event) => { event.waitUntil( caches .keys() .then((cacheNames) => { return Promise.all(cacheNames.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name))) }) .then(() => self.clients.claim()) ) }) // Fetch event - serve from cache or network self.addEventListener("fetch", (event) => { const { request } = event const url = new URL(request.url) // Skip non-GET requests if (request.method !== "GET") { return } // Skip API requests if (url.pathname.startsWith("/api/")) { return } // Skip PocketBase API if (url.pathname.startsWith("/_/")) { return } event.respondWith( caches .match(request) .then((cached) => { if (cached) { // Return cached version and update in background fetch(request).then((response) => { if (response.ok) { caches.open(CACHE_NAME).then((cache) => { cache.put(request, response) }) } }) return cached } // Fetch from network return fetch(request).then((response) => { if (!response || response.status !== 200 || response.type !== "basic") { return response } const responseToCache = response.clone() caches.open(CACHE_NAME).then((cache) => { cache.put(request, responseToCache) }) return response }) }) .catch(() => { // Return offline page if available return caches.match("/offline.html") }) ) }) // Push notification event self.addEventListener("push", (event) => { if (!event.data) { return } const data = event.data.json() const options = { body: data.body || "New notification", icon: data.icon || "/favicon-192x192.png", badge: data.badge || "/favicon-72x72.png", tag: data.tag || "default", requireInteraction: data.requireInteraction || false, data: data.data || {}, actions: data.actions || [ { action: "open", title: "Open" }, { action: "close", title: "Dismiss" }, ], } event.waitUntil(self.registration.showNotification(data.title || "Beszel Alert", options)) }) // Notification click event self.addEventListener("notificationclick", (event) => { event.notification.close() const { data } = event.notification const urlToOpen = data?.url || "/" event.waitUntil( clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => { // Check if there's already a window open for (const client of clientList) { if (client.url === urlToOpen && "focus" in client) { return client.focus() } } // Open new window if not found if (clients.openWindow) { return clients.openWindow(urlToOpen) } }) ) }) // Background sync for offline support self.addEventListener("sync", (event) => { if (event.tag === "background-sync") { event.waitUntil(doBackgroundSync()) } }) function doBackgroundSync() { // Retry any pending API requests stored in IndexedDB // This is a placeholder - implement with actual pending request logic console.log("Background sync executed") } // Periodic background sync (if supported) self.addEventListener("periodicsync", (event) => { if (event.tag === "update-check") { event.waitUntil(checkForUpdates()) } }) function checkForUpdates() { // Check for new data and show notifications if needed console.log("Periodic sync executed") }