const CACHE_NAME = 'fina-v1'; const STATIC_CACHE = 'fina-static-v1'; const DYNAMIC_CACHE = 'fina-dynamic-v1'; // Assets to cache on install const STATIC_ASSETS = [ '/', '/static/css/style.css', '/static/js/script.js', '/static/js/chart.min.js', '/static/images/fina-logo.png' ]; // Install event - cache static assets self.addEventListener('install', event => { console.log('[Service Worker] Installing...'); event.waitUntil( caches.open(STATIC_CACHE) .then(cache => { console.log('[Service Worker] Caching static assets'); return cache.addAll(STATIC_ASSETS); }) .then(() => self.skipWaiting()) ); }); // Activate event - clean up old caches self.addEventListener('activate', event => { console.log('[Service Worker] Activating...'); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== STATIC_CACHE && name !== DYNAMIC_CACHE) .map(name => caches.delete(name)) ); }).then(() => self.clients.claim()) ); }); // Fetch event - network first, then cache self.addEventListener('fetch', event => { const { request } = event; // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip chrome extension and other non-http(s) requests if (!request.url.startsWith('http')) { return; } // API requests - network first, cache fallback if (request.url.includes('/api/')) { event.respondWith( fetch(request) .then(response => { const responseClone = response.clone(); caches.open(DYNAMIC_CACHE).then(cache => { cache.put(request, responseClone); }); return response; }) .catch(() => caches.match(request)) ); return; } // Static assets - cache first, network fallback if ( request.url.includes('/static/') || request.url.endsWith('.css') || request.url.endsWith('.js') || request.url.endsWith('.png') || request.url.endsWith('.jpg') || request.url.endsWith('.jpeg') || request.url.endsWith('.gif') || request.url.endsWith('.svg') ) { event.respondWith( caches.match(request) .then(cachedResponse => { if (cachedResponse) { return cachedResponse; } return fetch(request).then(response => { const responseClone = response.clone(); caches.open(STATIC_CACHE).then(cache => { cache.put(request, responseClone); }); return response; }); }) ); return; } // HTML pages - network first, cache fallback event.respondWith( fetch(request) .then(response => { const responseClone = response.clone(); caches.open(DYNAMIC_CACHE).then(cache => { cache.put(request, responseClone); }); return response; }) .catch(() => { return caches.match(request).then(cachedResponse => { if (cachedResponse) { return cachedResponse; } // Return offline page if available return caches.match('/'); }); }) ); }); // Handle messages from clients self.addEventListener('message', event => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); // Background sync for offline form submissions self.addEventListener('sync', event => { if (event.tag === 'sync-expenses') { event.waitUntil(syncExpenses()); } }); async function syncExpenses() { // Placeholder for syncing offline data console.log('[Service Worker] Syncing expenses...'); } // Push notifications support (for future feature) self.addEventListener('push', event => { const options = { body: event.data ? event.data.text() : 'New notification from FINA', icon: '/static/images/fina-logo.png', badge: '/static/images/fina-logo.png', vibrate: [200, 100, 200] }; event.waitUntil( self.registration.showNotification('FINA', options) ); }); // Notification click handler self.addEventListener('notificationclick', event => { event.notification.close(); event.waitUntil( clients.openWindow('/') ); });