Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commite1c7aa6unknown_key

fix(pwa): v4 self-nuke service worker — kill stale-cache nightmare

fix(pwa): v4 self-nuke service worker — kill stale-cache nightmare

Repeated user reports of stale HTML being served from old SW cache versions.
Even with cache version bumps (v1->v2->v3) the problem persisted because
returning visitors had a v1 or v2 SW already controlling their tab,
serving cached pre-redesign HTML before the new SW could even load.

v4 does ONE thing on activation:
  1. Purge every cache from any prior SW version
  2. Tell every controlled client to navigate (= reload onto fresh state)
  3. Unregister the SW itself

After v4 activates, the browser has no SW for gluecron.com at all. Every
future page load goes straight to the network. Push to main -> deploy
fires -> next refresh users see the new content. Period.

Also flipped /sw.js cache headers to no-cache/no-store/must-revalidate
+ Pragma: no-cache so browsers fetch the v4 nuke SW immediately rather
than serving an old /sw.js from HTTP cache.

Tests updated: removed assertions about fetch-handler / cache logic
that no longer applies to a no-fetch SW. Added assertions for the
self-unregister + cache-purge behaviour. 1233 pass / 1 unrelated fail.

Once we trust the auto-deploy pipeline + want offline back, we can ship
a real PWA SW with conservative behaviour. For now: instant deploys win.
Claude committed on May 4, 2026Parent: c475ee6
2 files changed+3358e1c7aa6b0a7ef26e5b161453cbba571dea641cc7
2 changed files+33−58
Modifiedsrc/__tests__/pwa.test.ts+5−10View fileUnifiedSplit
4242 expect(res.headers.get("service-worker-allowed")).toBe("/");
4343 });
4444
45 it("service worker source contains install + fetch handlers", () => {
45 it("v4 self-nuke service worker installs + activates + unregisters", () => {
4646 expect(SERVICE_WORKER_SRC).toContain("addEventListener('install'");
47 expect(SERVICE_WORKER_SRC).toContain("addEventListener('fetch'");
4847 expect(SERVICE_WORKER_SRC).toContain("addEventListener('activate'");
48 expect(SERVICE_WORKER_SRC).toContain("self.registration.unregister");
49 expect(SERVICE_WORKER_SRC).toContain("caches.delete");
4950 });
5051
51 it("service worker skips git + api + auth paths", () => {
52 expect(SERVICE_WORKER_SRC).toContain(".git/");
53 expect(SERVICE_WORKER_SRC).toContain("/api/");
54 expect(SERVICE_WORKER_SRC).toContain("/login");
55 });
56
57 it("service worker ignores non-GET requests", () => {
58 expect(SERVICE_WORKER_SRC).toContain("req.method !== 'GET'");
52 it("v4 service worker has no fetch handler — every request hits network", () => {
53 expect(SERVICE_WORKER_SRC).not.toContain("addEventListener('fetch'");
5954 });
6055});
6156
Modifiedsrc/routes/pwa.ts+28−48View fileUnifiedSplit
5959});
6060
6161/**
62 * Bare-bones service worker. Offline behaviour:
63 * - HTML → network first, cached response on failure, fallback offline page
64 * - other → pass-through (the static CSS is inlined into the HTML, so there's
65 * no cross-request asset worth caching for v1)
62 * Bare-bones service worker — v4 NUKE EDITION.
63 *
64 * After repeated reports of stale HTML being served from old cache versions,
65 * this SW does ONE job: unregister itself and purge every cache. Browsers
66 * that previously installed v1/v2/v3 will load this v4, see the unregister
67 * call, and stop intercepting fetches. From now on EVERY page load goes
68 * straight to the network — no SW, no cache, instant fresh content on push.
69 *
70 * Once we trust the auto-deploy pipeline + want offline support back, ship
71 * a real SW with conservative network-first behaviour. Until then: instant
72 * deploys win.
6673 */
67export const SERVICE_WORKER_SRC = `// gluecron service worker — v3 (Visual Impact pass)
68const CACHE = 'gluecron-shell-v3';
69const SHELL = ['/', '/manifest.webmanifest', '/icon.svg'];
70
71self.addEventListener('install', (e) => {
72 e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).catch(() => {}));
73 self.skipWaiting();
74});
75
74export const SERVICE_WORKER_SRC = `// gluecron service worker — v4 (self-nuke)
75self.addEventListener('install', () => self.skipWaiting());
7676self.addEventListener('activate', (e) => {
77 e.waitUntil(
78 caches.keys().then((keys) =>
79 Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
80 )
81 );
82 self.clients.claim();
83});
84
85self.addEventListener('fetch', (event) => {
86 const req = event.request;
87 if (req.method !== 'GET') return;
88 const url = new URL(req.url);
89 // Never intercept git, API, or auth endpoints — they must stay fresh.
90 if (
91 url.pathname.includes('.git/') ||
92 url.pathname.startsWith('/api/') ||
93 url.pathname.startsWith('/login') ||
94 url.pathname.startsWith('/logout') ||
95 url.pathname.startsWith('/register')
96 ) {
97 return;
98 }
99 const wantsHtml = req.headers.get('accept')?.includes('text/html');
100 if (wantsHtml) {
101 event.respondWith(
102 fetch(req)
103 .then((res) => {
104 const copy = res.clone();
105 caches.open(CACHE).then((c) => c.put(req, copy)).catch(() => {});
106 return res;
107 })
108 .catch(() => caches.match(req).then((hit) => hit || caches.match('/')))
109 );
110 }
77 e.waitUntil((async () => {
78 // Purge every cache from any prior SW version
79 const keys = await caches.keys();
80 await Promise.all(keys.map((k) => caches.delete(k)));
81 // Tell every client we're done so they reload onto clean network state
82 const clients = await self.clients.matchAll({ type: 'window' });
83 for (const c of clients) c.navigate(c.url).catch(() => {});
84 // Then unregister this SW so future loads skip the SW layer entirely
85 await self.registration.unregister();
86 })());
11187});
88// No fetch handler — every request goes straight to the network.
11289`;
11390
11491pwa.get("/sw.js", (c) => {
11592 c.header("content-type", "application/javascript");
116 c.header("cache-control", "public, max-age=60");
93 // No-cache: browser must check on every page load. Critical for the v4
94 // self-nuke SW to actually reach all returning visitors.
95 c.header("cache-control", "no-cache, no-store, must-revalidate");
96 c.header("pragma", "no-cache");
11797 // Service-Worker-Allowed required for root-scope SW served from root
11898 c.header("service-worker-allowed", "/");
11999 return c.body(SERVICE_WORKER_SRC);
120100