/* Service Worker for Seismo Fleet Manager PWA */ /* Network-first strategy with cache fallback for real-time data */ // IMPORTANT: bump this on every release that touches a precached or // runtime-cached static asset (event-modal.js, mobile.js, style.css, // templates served at /, etc.). The activate handler deletes any cache // not matching CACHE_VERSION, so old SW caches get evicted and mobile // PWA users actually receive the new bundles instead of being stuck on // the pre-bump version. Convention: keep it in sync with the Terra-View // version string in backend/main.py. const CACHE_VERSION = 'v0.13.2'; const STATIC_CACHE = `sfm-static-${CACHE_VERSION}`; const DYNAMIC_CACHE = `sfm-dynamic-${CACHE_VERSION}`; const DATA_CACHE = `sfm-data-${CACHE_VERSION}`; // Files to precache (critical app shell). event-modal.js is included // so its cache lifecycle is tied to the SW version bump explicitly. const STATIC_FILES = [ '/', '/static/style.css', '/static/mobile.css', '/static/mobile.js', '/static/offline-db.js', '/static/event-modal.js', '/static/manifest.json', 'https://cdn.tailwindcss.com', 'https://unpkg.com/htmx.org@1.9.10', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' ]; // Install event - cache static files self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(STATIC_CACHE) .then((cache) => { console.log('[SW] Precaching static files'); return cache.addAll(STATIC_FILES); }) .then(() => { console.log('[SW] Static files cached successfully'); return self.skipWaiting(); // Activate immediately }) .catch((error) => { console.error('[SW] Precaching failed:', error); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys() .then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { // Delete old caches that don't match current version if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName !== DATA_CACHE) { console.log('[SW] Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('[SW] Service worker activated'); return self.clients.claim(); // Take control of all pages }) ); }); // Fetch event - network-first strategy self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip chrome-extension and other non-http(s) requests if (!url.protocol.startsWith('http')) { return; } // API requests - network first, cache fallback if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirstStrategy(request, DATA_CACHE)); return; } // Static assets - cache first if (isStaticAsset(url.pathname)) { event.respondWith(cacheFirstStrategy(request, STATIC_CACHE)); return; } // HTML pages - network first with cache fallback if (request.headers.get('accept')?.includes('text/html')) { event.respondWith(networkFirstStrategy(request, DYNAMIC_CACHE)); return; } // Everything else - network first event.respondWith(networkFirstStrategy(request, DYNAMIC_CACHE)); }); // Network-first strategy async function networkFirstStrategy(request, cacheName) { try { // Try network first const networkResponse = await fetch(request); // Cache successful responses if (networkResponse && networkResponse.status === 200) { const cache = await caches.open(cacheName); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { // Network failed, try cache console.log('[SW] Network failed, trying cache:', request.url); const cachedResponse = await caches.match(request); if (cachedResponse) { console.log('[SW] Serving from cache:', request.url); return cachedResponse; } // No cache available, return offline page or error console.error('[SW] No cache available for:', request.url); // For HTML requests, return a basic offline page if (request.headers.get('accept')?.includes('text/html')) { return new Response( `
SFM requires an internet connection for this page.
Please check your connection and try again.