/* ObsiGate Service Worker - PWA Support */ const CACHE_VERSION = 'obsigate-v1.4.0'; const STATIC_CACHE = `${CACHE_VERSION}-static`; const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`; const MAX_DYNAMIC_CACHE_SIZE = 50; // Assets to cache on install const STATIC_ASSETS = [ '/', '/static/index.html', '/static/app.js', '/static/style.css', '/static/manifest.json' ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(STATIC_CACHE) .then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' }))); }) .catch((err) => { console.error('[SW] Failed to cache static assets:', err); }) .then(() => self.skipWaiting()) ); }); // 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 .filter((name) => name.startsWith('obsigate-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE) .map((name) => { console.log('[SW] Deleting old cache:', name); return caches.delete(name); }) ); }) .then(() => self.clients.claim()) ); }); // Fetch event - serve from cache, fallback to network self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip SSE connections if (url.pathname === '/api/events') { return; } // Skip authentication endpoints if (url.pathname.startsWith('/api/auth/')) { return; } // API requests - Network first, cache fallback if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirstStrategy(request)); return; } // Static assets - Cache first, network fallback event.respondWith(cacheFirstStrategy(request)); }); // Cache first strategy (for static assets) async function cacheFirstStrategy(request) { try { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } const networkResponse = await fetch(request); // Cache successful responses if (networkResponse && networkResponse.status === 200) { const cache = await caches.open(STATIC_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.error('[SW] Cache first strategy failed:', error); // Return offline page if available const offlinePage = await caches.match('/static/index.html'); if (offlinePage) { return offlinePage; } return new Response('Offline - Unable to fetch resource', { status: 503, statusText: 'Service Unavailable', headers: new Headers({ 'Content-Type': 'text/plain' }) }); } } // Network first strategy (for API calls) async function networkFirstStrategy(request) { try { const networkResponse = await fetch(request); // Cache successful GET responses if (networkResponse && networkResponse.status === 200 && request.method === 'GET') { const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, networkResponse.clone()); // Limit dynamic cache size limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE); } return networkResponse; } catch (error) { console.log('[SW] Network failed, trying cache:', request.url); const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline response return new Response(JSON.stringify({ error: 'Offline', message: 'Unable to fetch data. Please check your connection.' }), { status: 503, statusText: 'Service Unavailable', headers: new Headers({ 'Content-Type': 'application/json' }) }); } } // Limit cache size async function limitCacheSize(cacheName, maxSize) { const cache = await caches.open(cacheName); const keys = await cache.keys(); if (keys.length > maxSize) { // Delete oldest entries const deleteCount = keys.length - maxSize; for (let i = 0; i < deleteCount; i++) { await cache.delete(keys[i]); } } } // Message event - handle messages from clients self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CLEAR_CACHE') { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((name) => caches.delete(name)) ); }) ); } }); // Sync event - background sync for offline actions self.addEventListener('sync', (event) => { console.log('[SW] Background sync:', event.tag); if (event.tag === 'sync-data') { event.waitUntil(syncData()); } }); async function syncData() { // Placeholder for background sync logic console.log('[SW] Syncing data...'); } // Push notification event self.addEventListener('push', (event) => { const options = { body: event.data ? event.data.text() : 'New update available', icon: '/static/icons/icon-192x192.png', badge: '/static/icons/icon-72x72.png', vibrate: [200, 100, 200], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'explore', title: 'Ouvrir ObsiGate' }, { action: 'close', title: 'Fermer' } ] }; event.waitUntil( self.registration.showNotification('ObsiGate', options) ); }); // Notification click event self.addEventListener('notificationclick', (event) => { event.notification.close(); if (event.action === 'explore') { event.waitUntil( clients.openWindow('/') ); } });