1const CACHE_NAME = "curis-360-v91"; 2 3const CORE_ASSETS = [ 4 "/", 5 "/index.html", 6 "/manifest.json", 7 "/patient-manifest.json", 8 "/patient-login.html", 9 "/patient-hub.html", 10 "/tailwind.css", 11 "/main.css", 12 "/firebase-init.js", 13 "/demo-mode.js", 14 "/access-guard.js", 15 "/branch-sync.js", 16 "/logo.webp", 17 "/Curis360 logo.png", 18 "/icons/icon-192.png", 19 "/icons/icon-512.png", 20 "/icons/maskable-192.png", 21 "/icons/maskable-512.png", 22 "/icons/apple-touch-icon.png", 23 "/offline.html", // ð Added dedicated offline page 24 "/404.html" 25]; 26 27// Helper to check if URL should be ignored by the Service Worker Cache 28function isExternalHost(url) { 29 const hosts = [ 30 'firestore.googleapis.com', 31 'identitytoolkit.googleapis.com', 32 'securetoken.googleapis.com', 33 'ui-avatars.com', 34 'firebaseinstallations.googleapis.com', 35 'apis.google.com', 36 'accounts.google.com', 37 'firebasestorage.googleapis.com', // ð SECURED: Prevents caching of patient PDFs & images 38 // Signed Storage URLs land here â training videos, attendance selfies, any 39 // getSignedUrl output. They MUST bypass the service worker: the stale-while- 40 // revalidate branch below calls respondWith() on a media element's range 41 // requests and tries to cache the 206, which breaks seeking and can hand a 42 // video element a stale, already-expired response on the next open. 43 'storage.googleapis.com' 44 ]; 45 return hosts.some(host => url.hostname.includes(host)); 46} 47 48function isFirebaseAuthHelper(url) { 49 return url.origin === self.location.origin && url.pathname.startsWith('/__/auth/'); 50} 51 52// Learning Portal media broker. Every hit 302-redirects to a short-lived signed 53// URL minted for ONE viewer, and video playback drives it with byte-range 54// requests â both are things a cache must never touch. Letting the SW through 55// also keeps the browser's native media pipeline in charge of ranges, which is 56// what makes long training videos seek and resume correctly. 57function isTrainingMedia(url) { 58 return url.pathname.startsWith('/training-media') 59 || url.hostname.endsWith('cloudfunctions.net'); 60} 61 62self.addEventListener("install", (event) => { 63 // Pre-cache core assets 64 event.waitUntil( 65 caches.open(CACHE_NAME).then((cache) => { 66 const requests = CORE_ASSETS.map(url => { 67 return fetch(new Request(url, { cache: 'no-cache' })).then(response => { 68 if (!response.ok && response.type !== 'opaque') { 69 throw new TypeError(`Request failed with status ${response.status}`); 70 } 71 return cache.put(url, response); 72 }).catch(err => console.warn('Failed to precache', url, err)); 73 }); 74 return Promise.all(requests); 75 }).then(() => self.skipWaiting()) 76 ); 77}); 78 79self.addEventListener("activate", (event) => { 80 event.waitUntil( 81 caches.keys().then((cacheNames) => { 82 return Promise.all( 83 cacheNames.map((cacheName) => { 84 if (cacheName !== CACHE_NAME) { 85 console.log("Service Worker: Clearing Old Cache", cacheName); 86 return caches.delete(cacheName); 87 } 88 }) 89 ); 90 }).then(() => self.clients.claim()) 91 ); 92}); 93 94self.addEventListener("fetch", (event) => { 95 const url = new URL(event.request.url); 96 97 // Skip caching for non-GET requests, external API/auth domains, and the 98 // training media broker (per-viewer redirects + byte-range video traffic). 99 if (event.request.method !== 'GET' || isExternalHost(url) 100 || isFirebaseAuthHelper(url) || isTrainingMedia(url)) { 101 return; 102 } 103 104 // HTML Requests Strategy: Network First, fallback to Cache 105 // `accept` can be absent on some requests, and .includes on null throws inside 106 // the handler. Kept from the previous revision because it is a crash guard, 107 // not part of the reverted speed work. 108 const accept = event.request.headers.get('accept') || ''; 109 if (event.request.mode === 'navigate' || accept.includes('text/html')) { 110 event.respondWith( 111 fetch(event.request) 112 .then((networkResponse) => { 113 const resClone = networkResponse.clone(); 114 caches.open(CACHE_NAME).then((cache) => cache.put(event.request, resClone)); 115 return networkResponse; 116 }) 117 .catch(async () => { 118 // If offline, serve from cache if available, else fallback to offline.html 119 const cachedResponse = await caches.match(event.request); 120 return cachedResponse || await caches.match('/offline.html'); 121 }) 122 ); 123 return; 124 } 125 126 // Static Assets Strategy: Stale-While-Revalidate (Cache First, refresh in background) 127 event.respondWith( 128 caches.match(event.request).then((cachedResponse) => { 129 const fetchPromise = fetch(event.request)
130 .then((networkResponse) => { 131 // Important: Clone the response. A response is a stream and can only be consumed once. 132 if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') { 133 if (networkResponse && networkResponse.type === 'opaque') { 134 const resClone = networkResponse.clone(); 135 caches.open(CACHE_NAME).then((cache) => cache.put(event.request, resClone)); 136 } 137 return networkResponse; 138 } 139 const resClone = networkResponse.clone(); 140 caches.open(CACHE_NAME).then((cache) => cache.put(event.request, resClone)); 141 return networkResponse; 142 }) 143 .catch(() => null); 144 145 return cachedResponse || fetchPromise; 146 }) 147 ); 148});
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.