Spaces:
Sleeping
Sleeping
// 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) | |
); | |
} | |
}); | |