11/**
2- * @fileoverview Service worker for Web Push notifications.
2+ * @fileoverview Service worker for PWA install + Web Push notifications.
33 *
4- * Receives push events from the Codeman server (via web-push library) and displays
5- * OS-level notifications. Handles notification clicks to focus an existing Codeman
6- * tab or open a new one. Supports action buttons, per-session deep linking, and
7- * critical notification persistence (requireInteraction) .
4+ * App-shell caching: on install, precaches the core UI assets so the app
5+ * launches instantly and works offline (or on flaky connections). Uses a
6+ * network-first strategy for navigation and API calls, cache-first for
7+ * static assets .
88 *
9- * Lifecycle: skipWaiting on install, claim clients on activate — ensures the latest
10- * service worker takes control immediately without waiting for tab refresh.
9+ * Push notifications: receives push events from the Codeman server (via
10+ * web-push library) and displays OS-level notifications. Handles notification
11+ * clicks to focus an existing Codeman tab or open a new one.
12+ *
13+ * Lifecycle: skipWaiting on install, claim clients on activate -- ensures the
14+ * latest service worker takes control immediately without waiting for tab
15+ * refresh.
1116 *
1217 * @dependency None (runs in ServiceWorkerGlobalScope, isolated from page scripts)
13- * @see src/push-store.ts — server-side VAPID key management and subscription CRUD
18+ * @see src/push-store.ts -- server-side VAPID key management and subscription CRUD
1419 */
1520
16- // Codeman Service Worker — Web Push notifications
17- // This service worker receives push events from the server and displays OS-level notifications.
18- // It also handles notification clicks to focus or open the Codeman tab.
21+ const CACHE_NAME = 'codeman-v1' ;
22+
23+ // Core app shell -- cached on install for instant startup
24+ const APP_SHELL = [
25+ '/' ,
26+ '/styles.css' ,
27+ '/mobile.css' ,
28+ '/constants.js' ,
29+ '/app.js' ,
30+ '/api-client.js' ,
31+ '/terminal-ui.js' ,
32+ '/session-ui.js' ,
33+ '/settings-ui.js' ,
34+ '/panels-ui.js' ,
35+ '/notification-manager.js' ,
36+ '/mobile-handlers.js' ,
37+ '/keyboard-accessory.js' ,
38+ '/voice-input.js' ,
39+ '/vendor/xterm.min.js' ,
40+ '/vendor/xterm-addon-fit.min.js' ,
41+ '/vendor/xterm-addon-unicode11.min.js' ,
42+ '/vendor/xterm-zerolag-input.js' ,
43+ '/vendor/xterm.css' ,
44+ '/icon-192.png' ,
45+ '/icon-512.png' ,
46+ '/manifest.json' ,
47+ ] ;
48+
49+ // --- Install: precache app shell ---
1950
20- self . addEventListener ( 'install' , ( ) => {
51+ self . addEventListener ( 'install' , ( event ) => {
52+ event . waitUntil (
53+ caches . open ( CACHE_NAME ) . then ( ( cache ) => {
54+ // Use addAll but don't fail install if some assets 404 (hashed filenames)
55+ return Promise . allSettled (
56+ APP_SHELL . map ( ( url ) => cache . add ( url ) . catch ( ( ) => { } ) )
57+ ) ;
58+ } )
59+ ) ;
2160 self . skipWaiting ( ) ;
2261} ) ;
2362
63+ // --- Activate: clean old caches, claim clients ---
64+
2465self . addEventListener ( 'activate' , ( event ) => {
25- event . waitUntil ( self . clients . claim ( ) ) ;
66+ event . waitUntil (
67+ caches . keys ( ) . then ( ( keys ) =>
68+ Promise . all (
69+ keys . filter ( ( k ) => k !== CACHE_NAME ) . map ( ( k ) => caches . delete ( k ) )
70+ )
71+ ) . then ( ( ) => self . clients . claim ( ) )
72+ ) ;
2673} ) ;
2774
75+ // --- Fetch: network-first for API/navigation, cache-first for static ---
76+
77+ self . addEventListener ( 'fetch' , ( event ) => {
78+ const { request } = event ;
79+
80+ // Skip non-GET, WebSocket upgrades, and SSE streams
81+ if ( request . method !== 'GET' ) return ;
82+ if ( request . headers . get ( 'upgrade' ) === 'websocket' ) return ;
83+ if ( request . headers . get ( 'accept' ) === 'text/event-stream' ) return ;
84+ if ( request . url . includes ( '/api/' ) ) return ;
85+
86+ event . respondWith (
87+ caches . match ( request ) . then ( ( cached ) => {
88+ // Return cache immediately, refresh in background (stale-while-revalidate)
89+ const fetchPromise = fetch ( request ) . then ( ( response ) => {
90+ if ( response && response . ok ) {
91+ const clone = response . clone ( ) ;
92+ caches . open ( CACHE_NAME ) . then ( ( cache ) => cache . put ( request , clone ) ) ;
93+ }
94+ return response ;
95+ } ) . catch ( ( ) => cached ) ;
96+
97+ return cached || fetchPromise ;
98+ } )
99+ ) ;
100+ } ) ;
101+
102+ // --- Push notifications ---
103+
28104self . addEventListener ( 'push' , ( event ) => {
29105 if ( ! event . data ) return ;
30106
@@ -40,8 +116,8 @@ self.addEventListener('push', (event) => {
40116 const options = {
41117 body : body || '' ,
42118 tag : tag || 'codeman-default' ,
43- icon : '/favicon.ico ' ,
44- badge : '/favicon.ico ' ,
119+ icon : '/icon-192.png ' ,
120+ badge : '/icon-192.png ' ,
45121 data : { sessionId, url : sessionId ? `/?session=${ sessionId } ` : '/' } ,
46122 renotify : true ,
47123 requireInteraction : urgency === 'critical' ,
@@ -75,7 +151,7 @@ self.addEventListener('notificationclick', (event) => {
75151 return client . focus ( ) ;
76152 }
77153 }
78- // No existing tab — open a new one
154+ // No existing tab -- open a new one
79155 return self . clients . openWindow ( targetUrl ) ;
80156 } )
81157 ) ;
0 commit comments