/* ObsiGate Service Worker - PWA v2 with IndexedDB offline support */ const CACHE_VERSION = 'obsigate-v2-pwa'; const STATIC_CACHE = CACHE_VERSION + '-static'; const DYNAMIC_CACHE = CACHE_VERSION + '-dynamic'; const MAX_DYNAMIC_CACHE_SIZE = 100; const STATIC_ASSETS = [ '/', '/static/index.html', '/static/app.js', '/static/style.css', '/static/manifest.json' ]; /* ── Install ── */ self.addEventListener('install', (event) => { console.log('[SW] Installing v' + CACHE_VERSION); 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] Cache error:', err)) .then(() => self.skipWaiting()) ); }); /* ── Activate ── */ self.addEventListener('activate', (event) => { console.log('[SW] Activating...'); 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 ── */ self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); if (request.method !== 'GET') return; if (url.pathname === '/api/events') return; if (url.pathname.startsWith('/api/auth/')) return; // API requests: Network first, tell client to use IndexedDB if offline if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirstAPI(request)); return; } // Static assets: Cache first event.respondWith(cacheFirstStatic(request, url)); }); /* ── Network First for API ── */ async function networkFirstAPI(request) { try { const response = await fetch(request); // Cache successful GETs in dynamic cache if (response && response.status === 200) { const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, response.clone()); limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE); } return response; } catch (error) { console.log('[SW] Offline — checking cache for:', request.url); // Try dynamic cache first const cached = await caches.match(request); if (cached) { // Add offline header so the page can detect const headers = new Headers(cached.headers); headers.set('X-ObsiGate-Offline', 'true'); return new Response(cached.body, { status: cached.status, statusText: cached.statusText, headers }); } // Fallback: tell the page to use IndexedDB return new Response(JSON.stringify({ offline: true, endpoint: new URL(request.url).pathname, message: 'Vous etes hors-ligne. Les donnees locales sont utilisees.' }), { status: 200, headers: { 'Content-Type': 'application/json', 'X-ObsiGate-Offline': 'true' } }); } } /* ── Cache First for Static ── */ async function cacheFirstStatic(request, url) { const cached = await caches.match(request); if (cached) return cached; try { const response = await fetch(request); if (response && response.status === 200) { const cache = await caches.open(STATIC_CACHE); cache.put(request, response.clone()); } return response; } catch { // Offline fallback for HTML navigation if (request.mode === 'navigate' || url.pathname === '/' || url.pathname.endsWith('/')) { const offlinePage = await caches.match('/static/index.html'); if (offlinePage) return offlinePage; } return new Response('Offline', { status: 503 }); } } async function limitCacheSize(cacheName, maxSize) { const cache = await caches.open(cacheName); const keys = await cache.keys(); if (keys.length > maxSize) { for (let i = 0; i < keys.length - maxSize; i++) { await cache.delete(keys[i]); } } } /* ── Messages ── */ self.addEventListener('message', (event) => { const data = event.data; if (!data || !data.type) return; switch (data.type) { case 'SKIP_WAITING': self.skipWaiting(); break; case 'CLEAR_CACHE': event.waitUntil( caches.keys().then(names => Promise.all(names.map(n => caches.delete(n)))) ); break; case 'CLAIM_CLIENTS': event.waitUntil(self.clients.claim()); break; } }); /* ── Sync ── */ self.addEventListener('sync', (event) => { console.log('[SW] Background sync:', event.tag); if (event.tag === 'obsigate-sync') { event.waitUntil( self.clients.matchAll().then(clients => { clients.forEach(client => { client.postMessage({ type: 'SYNC_TRIGGERED' }); }); }) ); } }); /* ── Push ── */ self.addEventListener('push', (event) => { const options = { body: event.data ? event.data.text() : 'Nouvelle mise a jour', icon: '/static/icons/icon-192x192.svg', badge: '/static/icons/icon-72x72.svg', vibrate: [200, 100, 200], actions: [ { action: 'open', title: 'Ouvrir ObsiGate' }, { action: 'close', title: 'Fermer' } ] }; event.waitUntil(self.registration.showNotification('ObsiGate', options)); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); if (event.action === 'open') { event.waitUntil(clients.openWindow('/')); } });