// TreeTrack Service Worker - PWA and Offline Support const VERSION = 1755116236; // Cache busting bump - force clients to fetch new static assets and header image change const CACHE_NAME = `treetrack-v${VERSION}`; const STATIC_CACHE = `static-v${VERSION}`; const API_CACHE = `api-v${VERSION}`; // Check if we're in development mode const isDevelopment = self.location.hostname === 'localhost' || self.location.hostname === '127.0.0.1'; const urlsToCache = [ '/static/', '/static/index.html', '/static/map.html', '/static/js/tree-track-app.js', '/static/js/modules/auth-manager.js', '/static/js/modules/api-client.js', '/static/js/modules/ui-manager.js', '/static/js/modules/form-manager.js', '/static/js/modules/autocomplete-manager.js', '/static/js/modules/media-manager.js', '/static/map.js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap' ]; // Install event - cache resources self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Opened cache'); return cache.addAll(urlsToCache.map(url => new Request(url, {cache: 'reload'}))); }) .catch(error => { console.log('Cache install failed:', error); }) ); }); // Fetch event - serve cached content when offline self.addEventListener('fetch', event => { // Skip non-GET requests if (event.request.method !== 'GET') { return; } // Skip requests to API endpoints - let them fail gracefully if (event.request.url.includes('/api/') || event.request.url.includes('/trees')) { return; } // In development mode, always fetch fresh content for static files // Also detect Hugging Face Spaces development environment const isHFDevelopment = self.location.hostname.includes('hf.space') || self.location.hostname.includes('huggingface.co'); if ((isDevelopment || isHFDevelopment) && event.request.url.includes('/static/')) { console.log('Development mode: bypassing cache for', event.request.url); event.respondWith( fetch(event.request, { cache: 'no-cache', headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }) .catch(() => { console.log('Network failed, using cache fallback for', event.request.url); return caches.match(event.request); }) ); return; } event.respondWith( caches.match(event.request) .then(response => { // In production, use cache-first strategy if (response && !isDevelopment) { return response; } // Fetch with cache busting in development const fetchRequest = isDevelopment ? new Request(event.request.url + '?_=' + Date.now()) : event.request; return fetch(fetchRequest).then(response => { // Check if we received a valid response if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // Clone the response const responseToCache = response.clone(); // Only cache in production or for offline fallbacks if (!isDevelopment) { caches.open(CACHE_NAME) .then(cache => { cache.put(event.request, responseToCache); }); } return response; }).catch(() => { // If fetch fails, try to return a cached fallback if (event.request.destination === 'document') { return caches.match('/static/index.html'); } }); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', event => { const cacheWhitelist = [CACHE_NAME]; event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); // Background sync for offline data submission self.addEventListener('sync', event => { if (event.tag === 'background-sync') { event.waitUntil(doBackgroundSync()); } }); async function doBackgroundSync() { // Handle any queued tree submissions when back online try { const cache = await caches.open('offline-data'); const requests = await cache.keys(); for (const request of requests) { if (request.url.includes('offline-tree-')) { const response = await cache.match(request); const treeData = await response.json(); // Try to submit the data try { const submitResponse = await fetch('/api/trees', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(treeData) }); if (submitResponse.ok) { // Remove from cache if successful await cache.delete(request); console.log('Offline tree data synced successfully'); } } catch (error) { console.log('Failed to sync offline data:', error); } } } } catch (error) { console.log('Background sync failed:', error); } } // Push notifications (for future enhancement) self.addEventListener('push', event => { const options = { body: event.data ? event.data.text() : 'New tree data available!', icon: '/static/icon-192x192.png', badge: '/static/badge-72x72.png', tag: 'treetrack-notification', data: { url: '/static/map.html' } }; event.waitUntil( self.registration.showNotification('TreeTrack', options) ); }); // Handle notification clicks self.addEventListener('notificationclick', event => { event.notification.close(); if (event.notification.data && event.notification.data.url) { event.waitUntil( clients.openWindow(event.notification.data.url) ); } });