1// SmartClinician AI - Service Worker 2// Version 1.0.3 3// 4// Bump these on every frontend release. Static assets are served cache-first, 5// so a stale version here means users keep running old JS after a deploy. 6const CACHE_NAME = 'smartclinician-v1.0.3'; 7const RUNTIME_CACHE = 'smartclinician-runtime-v1.0.3'; 8const API_CACHE = 'smartclinician-api-v1'; 9 10// Files to cache immediately on install 11const STATIC_ASSETS = [ 12 '/', 13 '/index.html', 14 '/smartclinician.html', 15 '/login.html', 16 '/register.html', 17 '/assets/css/main.css', 18 '/assets/css/compact-header.css', 19 '/assets/css/mobile.css', 20 '/assets/js/app.js', 21 '/assets/js/dashboard.js', 22 '/assets/js/header.js', 23 '/assets/js/mobile.js', 24 '/assets/js/pwa.js', 25 '/integration/auto-populate.js', 26 '/manifest.json', 27 '/assets/icons/icon-192x192.png', 28 '/assets/icons/icon-512x512.png' 29]; 30 31// API endpoints to cache (with network-first strategy) 32const API_ENDPOINTS = [ 33 '/api/medical-databases', 34 '/api/preferences', 35 '/api/chatbox/topics' 36]; 37 38// Install event - cache static assets 39self.addEventListener('install', (event) => { 40 console.log('ð§ Service Worker: Installing...'); 41 42 event.waitUntil( 43 caches.open(CACHE_NAME) 44 .then((cache) => { 45 console.log('ð¦ Service Worker: Caching static assets'); 46 return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' }))); 47 }) 48 .then(() => { 49 console.log('â Service Worker: Installation complete'); 50 return self.skipWaiting(); // Activate immediately 51 }) 52 .catch((error) => { 53 console.error('â Service Worker: Installation failed', error); 54 }) 55 ); 56}); 57 58// Activate event - clean up old caches 59self.addEventListener('activate', (event) => { 60 console.log('ð Service Worker: Activating...'); 61 62 event.waitUntil( 63 caches.keys() 64 .then((cacheNames) => { 65 return Promise.all( 66 cacheNames.map((cacheName) => { 67 if (cacheName !== CACHE_NAME && cacheName !== RUNTIME_CACHE && cacheName !== API_CACHE) { 68 console.log('ðï¸ Service Worker: Deleting old cache:', cacheName); 69 return caches.delete(cacheName); 70 } 71 }) 72 ); 73 }) 74 .then(() => { 75 console.log('â Service Worker: Activation complete'); 76 return self.clients.claim(); // Take control immediately 77 }) 78 ); 79}); 80 81// Fetch event - serve from cache or network 82self.addEventListener('fetch', (event) => { 83 const { request } = event; 84 const url = new URL(request.url); 85 86 // Skip non-GET requests 87 if (request.method !== 'GET') { 88 return; 89 } 90 91 // Skip cross-origin requests (except API) 92 if (url.origin !== location.origin && !url.pathname.startsWith('/api/')) { 93 return; 94 } 95 96 // Handle API requests with network-first strategy 97 if (url.pathname.startsWith('/api/')) { 98 event.respondWith(networkFirstStrategy(request)); 99 return; 100 } 101 102 // Navigation requests: network-only with offline fallback 103 if (request.mode === 'navigate') { 104 event.respondWith(networkOnlyWithOffline(request)); 105 return; 106 } 107 108 // Handle static assets with cache-first strategy 109 event.respondWith(cacheFirstStrategy(request)); 110}); 111 112// Network-only strategy with offline fallback (for navigations) 113async function networkOnlyWithOffline(request) { 114 try { 115 return await fetch(request); 116 } catch (error) { 117 console.error('â Navigation fetch failed:', error); 118 try { 119 const offlinePage = await caches.match('/offline.html'); 120 if (offlinePage) { 121 return offlinePage; 122 } 123 } catch (cacheReadError) { 124 console.warn('â ï¸ Offline cache read failed:', cacheReadError); 125 } 126 return new Response( 127 '<html><body><h1>Offline</h1><p>You are currently offline. Please check your internet connection.</p></body></html>', 128 { headers: { 'Content-Type': 'text/html' } } 129 ); 130 } 131} 132 133// Cache-first strategy (for static assets) 134async function cacheFirstStrategy(request) { 135 let cachedResponse; 136 137 try { 138 cachedResponse = await caches.match(request); 139 } catch (cacheReadError) { 140 console.warn('â ï¸ Cache read failed:', cacheReadError); 141 } 142 143 if (cachedResponse) { 144 console.log('ð¦ Cache hit:', request.url); 145 return cachedResponse; 146 } 147 148 try { 149 // If not in cache, fetch from network 150 console.log('ð Fetching from network:', request.url); 151 const networkResponse = await fetch(request); 152 153 // Cache the response for future use (best-effort) 154 if (networkResponse.ok) {
155 try { 156 const cache = await caches.open(RUNTIME_CACHE); 157 await cache.put(request, networkResponse.clone()); 158 } catch (cacheError) { 159 console.warn('â ï¸ Cache write failed:', cacheError); 160 } 161 } 162 163 return networkResponse; 164 } catch (error) { 165 console.error('â Fetch failed:', error); 166 167 // Return offline page if available 168 try { 169 const offlinePage = await caches.match('/offline.html'); 170 if (offlinePage) { 171 return offlinePage; 172 } 173 } catch (cacheReadError) { 174 console.warn('â ï¸ Offline cache read failed:', cacheReadError); 175 } 176 177 // Return a basic offline response 178 return new Response( 179 '<html><body><h1>Offline</h1><p>You are currently offline. Please check your internet connection.</p></body></html>', 180 { headers: { 'Content-Type': 'text/html' } } 181 ); 182 } 183} 184 185// Network-first strategy (for API requests) 186async function networkFirstStrategy(request) { 187 try { 188 // Try network first 189 const networkResponse = await fetch(request); 190 191 // Cache successful API responses (except auth endpoints) - best-effort 192 if (networkResponse.ok && !request.url.includes('/auth/')) { 193 try { 194 const cache = await caches.open(API_CACHE); 195 await cache.put(request, networkResponse.clone()); 196 } catch (cacheError) { 197 console.warn('â ï¸ API cache write failed:', cacheError); 198 } 199 } 200 201 return networkResponse; 202 } catch (error) { 203 console.log('ð Network failed, trying cache:', request.url); 204 205 // Fall back to cache if network fails 206 try { 207 const cachedResponse = await caches.match(request); 208 if (cachedResponse) { 209 console.log('ð¦ Serving from cache (offline):', request.url); 210 return cachedResponse; 211 } 212 } catch (cacheReadError) { 213 console.warn('â ï¸ API cache read failed:', cacheReadError); 214 } 215 216 // Return error response 217 return new Response( 218 JSON.stringify({ error: 'Network request failed and no cached version available' }), 219 { 220 status: 503, 221 headers: { 'Content-Type': 'application/json' } 222 } 223 ); 224 } 225} 226 227// Background sync for offline actions (future enhancement) 228self.addEventListener('sync', (event) => { 229 console.log('ð Background sync:', event.tag); 230 231 if (event.tag === 'sync-cases') { 232 event.waitUntil(syncCases()); 233 } 234}); 235 236async function syncCases() { 237 // Future: Sync offline case data when connection is restored 238 console.log('ð Syncing cases...'); 239} 240 241// Push notifications (future enhancement) 242self.addEventListener('push', (event) => { 243 console.log('ð Push notification received:', event); 244 245 const data = event.data ? event.data.json() : {}; 246 const title = data.title || 'SmartClinician AI'; 247 const options = { 248 body: data.body || 'New notification', 249 icon: '/assets/icons/icon-192x192.png', 250 badge: '/assets/icons/icon-72x72.png', 251 vibrate: [200, 100, 200], 252 data: data.url || '/', 253 actions: [ 254 { action: 'open', title: 'Open' }, 255 { action: 'close', title: 'Close' } 256 ] 257 }; 258 259 event.waitUntil( 260 self.registration.showNotification(title, options) 261 ); 262}); 263 264// Notification click handler 265self.addEventListener('notificationclick', (event) => { 266 console.log('ð Notification clicked:', event); 267 268 event.notification.close(); 269 270 if (event.action === 'open') { 271 event.waitUntil( 272 clients.openWindow(event.notification.data) 273 ); 274 } 275}); 276 277// Message handler (for communication with main app) 278self.addEventListener('message', (event) => { 279 console.log('ð¨ Message received:', event.data); 280 281 if (event.data.action === 'skipWaiting') { 282 self.skipWaiting(); 283 } 284 285 if (event.data.action === 'clearCache') { 286 event.waitUntil( 287 caches.keys().then((cacheNames) => { 288 return Promise.all( 289 cacheNames.map((cacheName) => caches.delete(cacheName)) 290 ); 291 }) 292 ); 293 } 294}); 295 296console.log('ð¯ Service Worker: Script loaded'); 297
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.