From 2847ec19cd1e92183a5dbadfda37c9318fed7809 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Tue, 8 Sep 2026 10:31:26 -0700 Subject: [PATCH 1/4] Add navigationRoute that allows for auth redirects with offline fallback --- ui/src/sw.js | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/ui/src/sw.js b/ui/src/sw.js index 2096874cd..4b8de49f2 100644 --- a/ui/src/sw.js +++ b/ui/src/sw.js @@ -1,34 +1,31 @@ /// import { clientsClaim } from 'workbox-core' -import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' +import { cleanupOutdatedCaches, matchPrecache, precacheAndRoute } from 'workbox-precaching' import { NavigationRoute, registerRoute } from 'workbox-routing' -// self.__WB_MANIFEST is the default injection point -precacheAndRoute(self.__WB_MANIFEST) +// directoryIndex/cleanURLs off so the precache doesn't serve a /dashboard/ navigation from cache - the route below owns navigations +precacheAndRoute(self.__WB_MANIFEST, { + directoryIndex: null, + cleanURLs: false +}) // clean old assets cleanupOutdatedCaches() -/** @type {RegExp[] | undefined} */ -const denylist = [] +const NETWORK_TIMEOUT_MS = 5000 -// in dev mode, do not precache anything -if (import.meta.env.DEV) { - // don't precache anything - console.log('Development mode, not pre-caching anything') - denylist.push(/.*/) -} else { - // don't precache anything where the urls pathname ends with a slash (including times when the url has a query string) - // this permits the request to be handled by the server which will do a redirect as required - const configPath = self.location.pathname.split('/')[1] - denylist.push(new RegExp(`/${configPath}/[^?]*/(\\?.*)*$`)) -} - -// to allow work offline for allowed routes only -registerRoute(new NavigationRoute( - createHandlerBoundToURL('index.html'), - { denylist } -)) +// Network-first so an auth proxy's login redirect is followed - cached shell as offline fallback +registerRoute(new NavigationRoute(async ({ request }) => { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS) + try { + return await fetch(request, { signal: controller.signal }) + } catch { + return (await matchPrecache('index.html')) || Response.error() + } finally { + clearTimeout(timeout) + } +})) self.skipWaiting() // https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim From dfa3000e5d7f9ea1c8c5c5f3b65d84875edd5fb9 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Tue, 8 Sep 2026 11:12:22 -0700 Subject: [PATCH 2/4] Skip precaching in dev builds so the service worker doesn't serve stale assets during local development --- vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.js b/vite.config.js index e0ce35fc0..0e2d94816 100644 --- a/vite.config.js +++ b/vite.config.js @@ -32,7 +32,7 @@ export default defineConfig({ injectManifest: { maximumFileSizeToCacheInBytes: process.env.NODE_ENV === 'development' ? 6000000 : 3350000, - globPatterns: ['**/*.{js,css,html,svg,png,ico,ttf,eot,woff,woff2}'] + globPatterns: process.env.NODE_ENV === 'development' ? [] : ['**/*.{js,css,html,svg,png,ico,ttf,eot,woff,woff2}'] }, devOptions: { From 38fa759037c89e7859f4807e3cebe93c9ef78b5d Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Mon, 14 Sep 2026 12:19:54 -0700 Subject: [PATCH 3/4] Add networkOnly, use mode in config, update base path --- ui/src/main.mjs | 3 ++- ui/src/sw.js | 32 ++++++++++++-------------------- vite.config.js | 10 +++++----- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/ui/src/main.mjs b/ui/src/main.mjs index 5c0778b70..f19e1dd10 100644 --- a/ui/src/main.mjs +++ b/ui/src/main.mjs @@ -89,7 +89,8 @@ const host = new URL(window.location.href) function getDashboardReloadUrl () { const setupBasePath = store.state.setup.setup?.basePath const currentDashboardPath = window.location.pathname.match(/^(.+?\/dashboard)(?:\/|$)/)?.[1] - const basePath = setupBasePath || currentDashboardPath || '/dashboard' + const rawBasePath = setupBasePath || currentDashboardPath || '/dashboard' + const basePath = rawBasePath.endsWith('/') ? rawBasePath : rawBasePath + '/' return new URL(basePath, window.location.origin) } diff --git a/ui/src/sw.js b/ui/src/sw.js index 4b8de49f2..6c80412dc 100644 --- a/ui/src/sw.js +++ b/ui/src/sw.js @@ -2,31 +2,23 @@ import { clientsClaim } from 'workbox-core' import { cleanupOutdatedCaches, matchPrecache, precacheAndRoute } from 'workbox-precaching' import { NavigationRoute, registerRoute } from 'workbox-routing' +import { NetworkOnly } from 'workbox-strategies' -// directoryIndex/cleanURLs off so the precache doesn't serve a /dashboard/ navigation from cache - the route below owns navigations -precacheAndRoute(self.__WB_MANIFEST, { - directoryIndex: null, - cleanURLs: false -}) +// Network-first (keeps 'navigate' mode so an auth proxy's login redirect is followed), cached shell +// on failure. Registered before the precache route so it owns every navigation. +registerRoute(new NavigationRoute(new NetworkOnly({ + networkTimeoutSeconds: 5, + plugins: [{ + handlerDidError: async () => (await matchPrecache('index.html')) || Response.error() + }] +}))) + +// self.__WB_MANIFEST is the default injection point +precacheAndRoute(self.__WB_MANIFEST) // clean old assets cleanupOutdatedCaches() -const NETWORK_TIMEOUT_MS = 5000 - -// Network-first so an auth proxy's login redirect is followed - cached shell as offline fallback -registerRoute(new NavigationRoute(async ({ request }) => { - const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS) - try { - return await fetch(request, { signal: controller.signal }) - } catch { - return (await matchPrecache('index.html')) || Response.error() - } finally { - clearTimeout(timeout) - } -})) - self.skipWaiting() // https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim clientsClaim() diff --git a/vite.config.js b/vite.config.js index 0e2d94816..23d1dcd21 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,7 +7,7 @@ import { VitePWA } from 'vite-plugin-pwa' */ // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ mode }) => ({ resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js' @@ -31,8 +31,8 @@ export default defineConfig({ manifest: false, injectManifest: { - maximumFileSizeToCacheInBytes: process.env.NODE_ENV === 'development' ? 6000000 : 3350000, - globPatterns: process.env.NODE_ENV === 'development' ? [] : ['**/*.{js,css,html,svg,png,ico,ttf,eot,woff,woff2}'] + maximumFileSizeToCacheInBytes: mode === 'development' ? 6000000 : 3350000, + globPatterns: mode === 'development' ? [] : ['**/*.{js,css,html,svg,png,ico,ttf,eot,woff,woff2}'] }, devOptions: { @@ -45,7 +45,7 @@ export default defineConfig({ ], root: 'ui', build: { - minify: process.env.NODE_ENV === 'development' ? false : undefined, + minify: mode === 'development' ? false : undefined, outDir: '../dist', emptyOutDir: true, rollupOptions: { @@ -60,4 +60,4 @@ export default defineConfig({ } }, base: './' -}) +})) From 27627f1f169fa9275a402be5f77bd818f4bb0c20 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Mon, 14 Sep 2026 12:52:17 -0700 Subject: [PATCH 4/4] Add 500 self healing --- ui/src/sw.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/src/sw.js b/ui/src/sw.js index 6c80412dc..e7414876f 100644 --- a/ui/src/sw.js +++ b/ui/src/sw.js @@ -9,6 +9,10 @@ import { NetworkOnly } from 'workbox-strategies' registerRoute(new NavigationRoute(new NetworkOnly({ networkTimeoutSeconds: 5, plugins: [{ + // 5xx -> serve the shell so it can self-heal. Match >= 500 not !response.ok, so the + // status-0 opaqueredirect (the auth redirect) passes through. + fetchDidSucceed: async ({ response }) => + response.status >= 500 ? (await matchPrecache('index.html')) || response : response, handlerDidError: async () => (await matchPrecache('index.html')) || Response.error() }] })))