PageSourceSearch

https://smartclinician.ai/service-worker.js

js smartclinician.ai collected 2026-10-05 15:05:41 UTC 8,535 bytes, 297 lines download raw bytes

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.