mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
2856ee6637
Opus advisor flagged that the conflict-marker resolution from PR #1525's merge had not actually landed — static/sw.js still contained the literal <<<<<<< HEAD / ======= / >>>>>>> pr-1525 markers, which made the file fail to parse as JavaScript even though the substring-based source-string tests still passed (the __WEBUI_VERSION__ token was present, just inside the conflict block). Concrete impact pre-fix when shipped: - Service worker install handler would throw on script load - SW would never reach activated state - Old SW (from v0.50.278) would keep controlling the page indefinitely - Frontend cache-bust pathway silently broken - The INFLIGHT[sid] clear in static/sessions.js (the frontend half of PR #1525's stale-stream cleanup) would never deliver to existing browsers because the new SW would never activate Fix: - Resolve sw.js conflict to keep CACHE_NAME = 'hermes-shell-__WEBUI_VERSION__' (the post-#1517 rename, with the manual -stale-stream-cleanup1 suffix dropped as redundant — natural version-token bump invalidates old caches). - Add tests/test_pwa_manifest_sw.py::test_sw_js_has_no_merge_conflict_markers regression guard that scans for <<<<<<<, =======, >>>>>>> in sw.js source. - Update tests/test_stale_stream_cleanup.py::test_service_worker_cache_ bumped_for_frontend_fix_delivery to assert the canonical version-token CACHE_NAME pattern instead of the (now-removed) -stale-stream-cleanup1 manual suffix. 3945 → 3946 tests passing (+1 from the new conflict-marker guard). This issue would have shipped a broken service worker if Opus hadn't caught it. The new test_sw_js_has_no_merge_conflict_markers test would have flagged it earlier in the pipeline. Caught-by: Opus advisor pass on stage-279 brief Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
125 lines
4.4 KiB
JavaScript
125 lines
4.4 KiB
JavaScript
/**
|
|
* 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.
|
|
//
|
|
// Unversioned assets (`./`, manifest.json, favicons) are referenced from
|
|
// index.html without a cache-bust query, so they stay unversioned here too.
|
|
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)
|
|
// - Shell assets → cache-first with network fallback
|
|
// - Everything else → network-first, fall back to offline page
|
|
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;
|
|
|
|
// 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
|
|
}
|
|
|
|
// Shell assets: cache-first
|
|
event.respondWith(
|
|
caches.match(event.request).then((cached) => {
|
|
if (cached) return cached;
|
|
return fetch(event.request).then((response) => {
|
|
// Cache successful GET responses for shell assets
|
|
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(() => {
|
|
// Offline fallback for navigation requests.
|
|
// Note: caches.match() returns a Promise (always truthy in a `||` check),
|
|
// so we must await/then to unwrap it — otherwise the `new Response(...)`
|
|
// branch is dead code and the browser falls back to its default offline page.
|
|
if (event.request.mode === 'navigate') {
|
|
return caches.match('./').then((cached) => cached || new Response(
|
|
'<html><body style="font-family:sans-serif;padding:2rem;background:#1a1a1a;color:#ccc">' +
|
|
'<h2>You are offline</h2>' +
|
|
'<p>Hermes requires a server connection. Please check your network and try again.</p>' +
|
|
'</body></html>',
|
|
{ headers: { 'Content-Type': 'text/html' } }
|
|
));
|
|
}
|
|
});
|
|
})
|
|
);
|
|
});
|