/** * Hermes WebUI Service Worker * Minimal PWA service worker — enables "Add to Home Screen". * No offline caching of API responses (the UI requires a live backend). * Caches only static shell assets so the app shell loads fast on repeat visits. */ // Cache version is injected by the server at request time (routes.py /sw.js handler). // Bumps automatically whenever the git commit changes — no manual edits needed. const CACHE_NAME = 'hermes-shell-__WEBUI_VERSION__'; // Static assets that form the app shell. // // Versioned assets (CSS + JS) include `?v=__WEBUI_VERSION__` to match the // query string the page sends — see index.html. Without the version query // here, every cache lookup against `?v=...` URLs would miss and fall through // to network, defeating the pre-cache. // // Do not pre-cache './' or login assets here: under password auth they can be // either the authenticated app shell or login code, and stale cached responses // can make valid password submits fail until the user clears browser cache. // Navigations populate './' only after a successful non-redirect network load. const VQ = '?v=__WEBUI_VERSION__'; const SHELL_ASSETS = [ './static/style.css' + VQ, './static/boot.js' + VQ, './static/ui.js' + VQ, './static/messages.js' + VQ, './static/sessions.js' + VQ, './static/panels.js' + VQ, './static/commands.js' + VQ, './static/icons.js' + VQ, './static/i18n.js' + VQ, './static/workspace.js' + VQ, './static/terminal.js' + VQ, './static/onboarding.js' + VQ, './static/favicon.svg', './static/favicon-32.png', './manifest.json', ]; // Install: pre-cache the app shell self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(SHELL_ASSETS).catch((err) => { // Non-fatal: if any asset fails, still activate console.warn('[sw] Shell pre-cache partial failure:', err); }); }) ); self.skipWaiting(); }); // Activate: clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)) ) ) ); self.clients.claim(); }); // Fetch strategy: // - API calls (/api/*, /stream) → always network (never cache) // - Login assets → always network (never cache stale auth code) // - Page navigations → network-first so auth redirects/cookies are honored // - Shell assets → network-first with cache fallback // - Everything else → network-only self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); // Never intercept cross-origin requests if (url.origin !== self.location.origin) return; // Never intercept the service worker script itself. Returning a cached sw.js // prevents the browser from seeing a new cache version after local patches. if (url.pathname.endsWith('/sw.js')) return; // Login assets must always hit the network. Older login.js builds have had // subpath-sensitive auth POST paths; if the service worker caches one, the // password can keep failing until the user manually clears browser cache. if ( url.pathname.endsWith('/login') || url.pathname.endsWith('/static/login.js') ) { return; } // API and streaming endpoints — always go to network. // The WebUI may be mounted under a subpath such as /hermes/, so API // requests can look like /hermes/api/sessions rather than /api/sessions. if ( url.pathname.startsWith('/api/') || url.pathname.includes('/api/') || url.pathname.includes('/stream') || url.pathname.startsWith('/health') || url.pathname.includes('/health') ) { return; // let browser handle normally } // Page navigations must be network-first. A stale cached './' response can // otherwise hide the server's 302-to-login after auth expiry, or ignore a // freshly set login cookie until the user manually refreshes. if (event.request.mode === 'navigate') { event.respondWith( fetch(event.request).then((response) => { if ( event.request.method === 'GET' && response.status === 200 && !response.redirected ) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put('./', clone)); } return response; }).catch(() => { return caches.match('./').then((cached) => cached || new Response( '' + '

You are offline

' + '

Hermes requires a server connection. Please check your network and try again.

' + '', { headers: { 'Content-Type': 'text/html' } } )); }) ); return; } // Only explicit shell assets are cached. Everything else should hit the // network so stale one-off files (especially auth/login scripts) do not get // trapped in CacheStorage until a manual cache clear. const scopePath = new URL(self.registration.scope).pathname; const relPath = url.pathname.startsWith(scopePath) ? url.pathname.slice(scopePath.length) : url.pathname.replace(/^\/+/, ''); const shellPath = './' + relPath.replace(/^\/+/, '') + url.search; if (!SHELL_ASSETS.includes(shellPath)) return; // Shell assets: network-first with cache fallback. This keeps offline support // but avoids executing stale JS/CSS after a local hotfix when WEBUI_VERSION // has not changed yet (e.g. before a guarded restart updates the ?v token). event.respondWith( fetch(event.request).then((response) => { if ( event.request.method === 'GET' && response.status === 200 ) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); } return response; }).catch(() => caches.match(event.request).then((cached) => cached || new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }))) ); });