// OpenClaw Control – Service Worker // Handles offline caching and push notifications. const CACHE_NAME = "openclaw-control-v1"; // Minimal app-shell files to precache. const PRECACHE_URLS = ["./"]; self.addEventListener("install", (event) => { event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))); self.skipWaiting(); }); self.addEventListener("activate", (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), ), ); self.clients.claim(); }); self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); // Skip non-GET and cross-origin requests. if (event.request.method !== "GET" || url.origin !== self.location.origin) { return; } // Skip non-UI routes — API, RPC, and plugin routes should never be cached. if ( url.pathname.startsWith("/api/") || url.pathname.startsWith("/rpc") || url.pathname.startsWith("/plugins/") ) { return; } // Cache-first for hashed assets; network-first for HTML/other. if (url.pathname.includes("/assets/")) { event.respondWith( caches.match(event.request).then( (cached) => cached || fetch(event.request).then((response) => { if (response.ok) { const clone = response.clone(); void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); } return response; }), ), ); } else { event.respondWith( fetch(event.request) .then((response) => { if (response.ok) { const clone = response.clone(); void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); } return response; }) .catch(() => caches.match(event.request)), ); } }); // --- Web Push --- self.addEventListener("push", (event) => { if (!event.data) { return; } let data; try { data = event.data.json(); } catch { data = { title: "OpenClaw", body: event.data.text() }; } const title = data.title || "OpenClaw"; const options = { body: data.body || "", icon: "./apple-touch-icon.png", badge: "./favicon-32.png", tag: data.tag || "openclaw-notification", data: { url: data.url || "./" }, }; event.waitUntil(self.registration.showNotification(title, options)); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); const targetUrl = event.notification.data?.url || "./"; event.waitUntil( self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clients) => { // Focus an existing window if one is open. for (const client of clients) { if (new URL(client.url).pathname === new URL(targetUrl, self.location.origin).pathname) { return client.focus(); } } return self.clients.openWindow(targetUrl); }), ); });