ice/public/sw.js
Claude Code c13b61cd03 Add Progressive Web App functionality
- Add web app manifest for home screen installation
- Implement service worker with offline caching strategy
- Create offline fallback page with auto-reconnect
- Generate PWA icons in multiple sizes (72px-512px)
- Add PWA meta tags and Apple Touch icons to all pages
- Register service worker with graceful degradation
- Update documentation with PWA installation instructions
- Add browserconfig.xml for Windows tile support

Features:
- Installable on mobile and desktop
- Offline functionality with cached resources
- App-like experience in standalone mode
- Automatic updates when online
- Works seamlessly with existing progressive enhancement

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-06 00:46:00 -04:00

143 lines
No EOL
3.9 KiB
JavaScript

// Service Worker for Great Lakes Ice Report PWA
const CACHE_NAME = 'ice-report-v1';
const OFFLINE_URL = '/offline.html';
// Files to cache for offline functionality
const CACHE_FILES = [
'/',
'/index.html',
'/admin.html',
'/privacy.html',
'/style.css',
'/app.js',
'/admin.js',
'/utils.js',
'/manifest.json',
OFFLINE_URL
];
// Install event - cache essential files
self.addEventListener('install', (event) => {
console.log('Service Worker: Installing...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Service Worker: Caching files');
return cache.addAll(CACHE_FILES);
})
.then(() => {
console.log('Service Worker: Files cached successfully');
return self.skipWaiting();
})
.catch((error) => {
console.error('Service Worker: Cache failed:', error);
})
);
});
// 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((cacheName) => cacheName !== CACHE_NAME)
.map((cacheName) => {
console.log('Service Worker: Deleting old cache:', cacheName);
return caches.delete(cacheName);
})
);
})
.then(() => {
console.log('Service Worker: Activated');
return self.clients.claim();
})
);
});
// Fetch event - serve cached content when offline
self.addEventListener('fetch', (event) => {
// Only handle GET requests
if (event.request.method !== 'GET') {
return;
}
// Skip cross-origin requests
if (!event.request.url.startsWith(self.location.origin)) {
return;
}
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version if available
if (response) {
return response;
}
// Try to fetch from network
return fetch(event.request)
.then((response) => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response for caching
const responseToCache = response.clone();
// Cache successful responses for static assets
if (event.request.url.match(/\.(js|css|html|png|jpg|jpeg|gif|svg|ico|woff|woff2)$/)) {
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(() => {
// If network fails and we're requesting an HTML page, show offline page
if (event.request.headers.get('accept').includes('text/html')) {
return caches.match(OFFLINE_URL);
}
// For other requests, just fail
throw new Error('Network failed and no cache available');
});
})
);
});
// Background sync for when connection is restored
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
console.log('Service Worker: Background sync triggered');
// Could implement queued location submissions here
}
});
// Push notifications (for future use)
self.addEventListener('push', (event) => {
if (event.data) {
const data = event.data.json();
console.log('Service Worker: Push message received:', data);
// Could show notifications about severe weather warnings
const options = {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/icon-72.png',
tag: 'ice-report',
renotify: true
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
}
});