diff --git a/ApiCore-Frontend/public/sw.js b/ApiCore-Frontend/public/sw.js index 8ef9e48..cd1afb7 100644 --- a/ApiCore-Frontend/public/sw.js +++ b/ApiCore-Frontend/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = "apicore-shell-v2"; +const CACHE_NAME = "apicore-shell-v3"; self.addEventListener("install", () => { self.skipWaiting(); @@ -23,14 +23,33 @@ self.addEventListener("fetch", (event) => { } const url = new URL(request.url); - if (url.origin !== self.location.origin || url.pathname.startsWith("/api")) { + + // /api/runtime-config tells the app which backend origin to use - it + // must always be fetched live. A stale cached value would point the app + // at the wrong (or no longer running) backend, which is worse than no + // cache at all. + if (url.pathname === "/api/runtime-config") { + return; + } + + // Same-origin static assets/pages, or the real backend API. The backend + // runs on its own origin (API_URL, injected at runtime via + // /api/runtime-config) rather than this app's own origin, so it can't be + // matched by origin the way static assets are - matched by path instead, + // since every backend call goes through /api/v1/. Without this, colonies, + // apiaries, tasks etc. were never cached at all: a beekeeper opening the + // app with no signal at an apiary saw nothing, not even data they had + // already loaded minutes earlier on the same device. + const isCacheable = + url.origin === self.location.origin || url.pathname.includes("/api/v1/"); + if (!isCacheable) { return; } // Page navigations must never be answered with a synthetic Response.error() - // Chrome/Edge render that as a native "This page couldn't load" interstitial // instead of the app. Let the browser handle navigations directly; only - // static assets (JS/CSS/images) go through the offline-cache fallback below. + // static assets/API data go through the offline-cache fallback below. if (request.mode === "navigate") { return; } @@ -38,6 +57,12 @@ self.addEventListener("fetch", (event) => { event.respondWith( fetch(request) .then((response) => { + // Never cache an error response (e.g. a 401 hit right as an access + // token expired, before refresh could kick in) - it would otherwise + // get served back later as if it were still the current state. + if (!response.ok) { + return response; + } const responseClone = response.clone(); return caches .open(CACHE_NAME) diff --git a/ApiCore-Frontend/src/lib/auth/auth-context.tsx b/ApiCore-Frontend/src/lib/auth/auth-context.tsx index 3ffb2f2..6565a04 100644 --- a/ApiCore-Frontend/src/lib/auth/auth-context.tsx +++ b/ApiCore-Frontend/src/lib/auth/auth-context.tsx @@ -91,6 +91,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { await logoutRequest(); storeAccessToken(null); setUser(null); + // The service worker cache now holds real business data (colonies, + // tasks, etc.) for offline viewing - on a shared/handed-off device + // that must not survive into the next person's session. + if ("caches" in window) { + const keys = await caches.keys(); + await Promise.all(keys.map((key) => caches.delete(key))); + } }, []); const hasPermission = useCallback(