// maestro service worker. // // Goal: make the app installable and let the shell load offline. It is NOT a // data cache — anything under /api (HTTP and WebSocket upgrades) is always left // to the network, since a terminal orchestrator is useless without a live // daemon. Static assets are cached at runtime so we never need to know Vite's // hashed filenames ahead of time. const CACHE = "maestro-shell-v1"; self.addEventListener("install", (event) => { // Warm the cache with the app entry so a cold offline launch still paints. event.waitUntil( caches .open(CACHE) .then((cache) => cache.addAll(["/", "/manifest.webmanifest"])) .catch(() => {}), ); self.skipWaiting(); }); self.addEventListener("activate", (event) => { // Drop caches from previous versions, then take control immediately. event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))), ) .then(() => self.clients.claim()), ); }); self.addEventListener("fetch", (event) => { const req = event.request; if (req.method !== "GET") return; const url = new URL(req.url); // Only handle same-origin GETs; never touch the daemon API. if (url.origin !== self.location.origin) return; if (url.pathname.startsWith("/api/")) return; // SPA navigations: network-first, fall back to the cached shell offline. if (req.mode === "navigate") { event.respondWith( fetch(req) .then((res) => { const copy = res.clone(); caches.open(CACHE).then((c) => c.put("/", copy)).catch(() => {}); return res; }) .catch(() => caches.match("/").then((r) => r || caches.match(req))), ); return; } // Static assets (Vite emits content-hashed names): stale-while-revalidate. event.respondWith( caches.match(req).then((cached) => { const network = fetch(req) .then((res) => { if (res.ok && res.type === "basic") { const copy = res.clone(); caches.open(CACHE).then((c) => c.put(req, copy)).catch(() => {}); } return res; }) .catch(() => cached); return cached || network; }), ); });