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

Merge PR #48: v4 self-nuke service worker

Merge PR #48: v4 self-nuke service worker

fix(pwa): v4 self-nuke SW kills stale-cache forever</title>
<parameter name="body">Returning visitors had v1/v2 SW serving cached pre-redesign HTML before the new SW could even load. v4 SW purges all caches + unregisters itself on activate — every future page load goes straight to network. Pushes are now instantly visible.</parameter>
</invoke>
CC LABS App committed on May 4, 2026Parents: 7e6e7b1 e1c7aa6
2 files changed+3358b5f1b5fad1c6b0c7792bd553057e47759f6f8002
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