// LocalGreenChain Service Worker // Version: 1.0.0 const CACHE_NAME = 'lgc-cache-v1'; const OFFLINE_URL = '/offline.html'; // Core files to cache for offline access const PRECACHE_ASSETS = [ '/', '/offline.html', '/manifest.json', '/favicon.ico', '/icons/icon-192x192.png', '/icons/icon-512x512.png' ]; // Install event - cache core assets self.addEventListener('install', (event) => { event.waitUntil( (async () => { const cache = await caches.open(CACHE_NAME); // Cache offline page first await cache.add(new Request(OFFLINE_URL, { cache: 'reload' })); // Cache other assets await cache.addAll(PRECACHE_ASSETS); // Skip waiting to activate immediately self.skipWaiting(); })() ); }); // Activate event - cleanup old caches self.addEventListener('activate', (event) => { event.waitUntil( (async () => { // Claim all clients immediately await self.clients.claim(); // Remove old caches const cacheNames = await caches.keys(); await Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => caches.delete(name)) ); })() ); }); // Fetch event - serve from cache or network self.addEventListener('fetch', (event) => { // Skip non-GET requests if (event.request.method !== 'GET') { return; } // Skip cross-origin requests if (!event.request.url.startsWith(self.location.origin)) { return; } // Skip API requests - always fetch from network if (event.request.url.includes('/api/')) { event.respondWith( fetch(event.request).catch(() => { return new Response( JSON.stringify({ error: 'Offline', offline: true }), { headers: { 'Content-Type': 'application/json' }, status: 503 } ); }) ); return; } // For navigation requests, try network first if (event.request.mode === 'navigate') { event.respondWith( (async () => { try { // Try network first const networkResponse = await fetch(event.request); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(event.request, networkResponse.clone()); } return networkResponse; } catch (error) { // If offline, try cache const cachedResponse = await caches.match(event.request); if (cachedResponse) { return cachedResponse; } // Fallback to offline page return caches.match(OFFLINE_URL); } })() ); return; } // For other requests, try cache first, then network event.respondWith( (async () => { const cachedResponse = await caches.match(event.request); if (cachedResponse) { // Return cached response and update cache in background event.waitUntil( (async () => { try { const networkResponse = await fetch(event.request); if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(event.request, networkResponse); } } catch (e) { // Network failed, cached version will be used } })() ); return cachedResponse; } try { const networkResponse = await fetch(event.request); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(event.request, networkResponse.clone()); } return networkResponse; } catch (error) { // For images, return a placeholder if (event.request.destination === 'image') { return new Response( 'Offline', { headers: { 'Content-Type': 'image/svg+xml' } } ); } throw error; } })() ); }); // Handle push notifications self.addEventListener('push', (event) => { if (!event.data) return; const data = event.data.json(); const options = { body: data.body, icon: '/icons/icon-192x192.png', badge: '/icons/icon-72x72.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: data.id || 1, url: data.url || '/' }, actions: data.actions || [ { action: 'view', title: 'View' }, { action: 'dismiss', title: 'Dismiss' } ] }; event.waitUntil( self.registration.showNotification(data.title || 'LocalGreenChain', options) ); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { event.notification.close(); if (event.action === 'dismiss') { return; } const urlToOpen = event.notification.data?.url || '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => { // Check if there's already a window open for (const client of windowClients) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // Open new window if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) ); }); // Background sync for offline actions self.addEventListener('sync', (event) => { if (event.tag === 'sync-plants') { event.waitUntil(syncPlants()); } else if (event.tag === 'sync-transport') { event.waitUntil(syncTransport()); } }); async function syncPlants() { // Get pending plant registrations from IndexedDB and sync them try { const pendingPlants = await getPendingFromIDB('pending-plants'); for (const plant of pendingPlants) { await fetch('/api/plants/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(plant) }); await removeFromIDB('pending-plants', plant.id); } } catch (error) { console.error('Plant sync failed:', error); } } async function syncTransport() { // Get pending transport events from IndexedDB and sync them try { const pendingEvents = await getPendingFromIDB('pending-transport'); for (const event of pendingEvents) { await fetch('/api/transport/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }); await removeFromIDB('pending-transport', event.id); } } catch (error) { console.error('Transport sync failed:', error); } } // IndexedDB helpers (simplified) function getPendingFromIDB(storeName) { return new Promise((resolve) => { // In production, implement proper IndexedDB operations resolve([]); }); } function removeFromIDB(storeName, id) { return new Promise((resolve) => { // In production, implement proper IndexedDB operations resolve(); }); }