// Service Worker for Control Center UI // Version: 1.0.0 const CACHE_NAME = 'control-center-ui-v1.0.0'; const STATIC_CACHE = `${CACHE_NAME}-static`; const DYNAMIC_CACHE = `${CACHE_NAME}-dynamic`; // Static assets to cache on install const STATIC_ASSETS = [ '/', '/index.html', '/manifest.json', '/style/input.css', '/assets/icon-192.png', '/assets/icon-512.png' ]; // API endpoints that should be cached with network-first strategy const API_ENDPOINTS = [ '/api/health', '/api/dashboard', '/api/servers', '/api/clusters' ]; // Assets that should never be cached const NO_CACHE_PATTERNS = [ '/api/auth/', '/api/logout', '/api/websocket' ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(STATIC_CACHE) .then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_ASSETS); }) .then(() => { console.log('[SW] Installation complete'); return self.skipWaiting(); // Force activation }) .catch((error) => { console.error('[SW] Installation failed:', error); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys() .then((cacheNames) => { return Promise.all( cacheNames .filter(cacheName => cacheName.startsWith('control-center-ui-') && cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE ) .map(cacheName => { console.log('[SW] Deleting old cache:', cacheName); return caches.delete(cacheName); }) ); }) .then(() => { console.log('[SW] Activation complete'); return self.clients.claim(); // Take control of all clients }) ); }); // Fetch event - handle network requests self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); // Skip non-GET requests and chrome-extension requests if (request.method !== 'GET' || url.protocol === 'chrome-extension:') { return; } // Skip requests that should never be cached if (NO_CACHE_PATTERNS.some(pattern => url.pathname.includes(pattern))) { return; } // Handle different types of requests if (isStaticAsset(url)) { event.respondWith(handleStaticAsset(request)); } else if (isAPIRequest(url)) { event.respondWith(handleAPIRequest(request)); } else { event.respondWith(handleNavigation(request)); } }); // Check if request is for a static asset function isStaticAsset(url) { const staticExtensions = ['.js', '.css', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.woff', '.woff2']; return staticExtensions.some(ext => url.pathname.endsWith(ext)) || url.pathname.includes('/assets/'); } // Check if request is for API function isAPIRequest(url) { return url.pathname.startsWith('/api/'); } // Handle static assets with cache-first strategy async function handleStaticAsset(request) { try { const cachedResponse = await caches.match(request); if (cachedResponse) { console.log('[SW] Serving static asset from cache:', request.url); return cachedResponse; } // Fetch from network and cache const networkResponse = await fetch(request); if (networkResponse.ok) { const cache = await caches.open(STATIC_CACHE); cache.put(request, networkResponse.clone()); console.log('[SW] Cached static asset:', request.url); } return networkResponse; } catch (error) { console.error('[SW] Failed to handle static asset:', error); // Return a cached fallback if available const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return a generic error response for images if (request.url.includes('.png') || request.url.includes('.jpg') || request.url.includes('.svg')) { return new Response('', { status: 404, statusText: 'Image not found' }); } throw error; } } // Handle API requests with network-first strategy async function handleAPIRequest(request) { try { // Always try network first for API requests const networkResponse = await fetch(request); if (networkResponse.ok && API_ENDPOINTS.some(endpoint => request.url.includes(endpoint))) { // Cache successful responses for specific endpoints const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, networkResponse.clone()); console.log('[SW] Cached API response:', request.url); } return networkResponse; } catch (error) { console.error('[SW] Network request failed, trying cache:', error); // Fallback to cache if network fails const cachedResponse = await caches.match(request); if (cachedResponse) { console.log('[SW] Serving API response from cache:', request.url); return cachedResponse; } // Return a generic offline response return new Response( JSON.stringify({ error: 'Offline', message: 'This request is not available offline' }), { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json' } } ); } } // Handle navigation requests (SPA routing) async function handleNavigation(request) { try { // Try network first const networkResponse = await fetch(request); return networkResponse; } catch (error) { console.log('[SW] Network failed for navigation, serving index.html from cache'); // For navigation requests, serve index.html from cache (SPA routing) const cachedResponse = await caches.match('/index.html'); if (cachedResponse) { return cachedResponse; } // Fallback offline page return new Response(` Control Center - Offline

You're Offline

Please check your internet connection and try again.

`, { status: 200, statusText: 'OK', headers: { 'Content-Type': 'text/html' } }); } } // Handle messages from the main thread self.addEventListener('message', (event) => { const { type, payload } = event.data; switch (type) { case 'SKIP_WAITING': self.skipWaiting(); break; case 'CLEAR_CACHE': clearAllCaches(); break; case 'GET_CACHE_STATUS': getCacheStatus().then(status => { event.ports[0].postMessage(status); }); break; default: console.log('[SW] Unknown message type:', type); } }); // Clear all caches async function clearAllCaches() { try { const cacheNames = await caches.keys(); await Promise.all( cacheNames.map(cacheName => caches.delete(cacheName)) ); console.log('[SW] All caches cleared'); } catch (error) { console.error('[SW] Failed to clear caches:', error); } } // Get cache status async function getCacheStatus() { try { const cacheNames = await caches.keys(); const status = {}; for (const cacheName of cacheNames) { const cache = await caches.open(cacheName); const keys = await cache.keys(); status[cacheName] = keys.length; } return status; } catch (error) { console.error('[SW] Failed to get cache status:', error); return {}; } } // Handle background sync (if supported) self.addEventListener('sync', (event) => { if (event.tag === 'background-sync') { console.log('[SW] Background sync triggered'); event.waitUntil(performBackgroundSync()); } }); // Perform background sync async function performBackgroundSync() { try { // Sync any pending data when back online const clients = await self.clients.matchAll(); clients.forEach(client => { client.postMessage({ type: 'BACKGROUND_SYNC' }); }); } catch (error) { console.error('[SW] Background sync failed:', error); } } // Handle push notifications (if needed in the future) self.addEventListener('push', (event) => { if (event.data) { const data = event.data.json(); const options = { body: data.body, icon: '/assets/icon-192.png', badge: '/assets/icon-72.png', tag: 'control-center-notification', requireInteraction: true, actions: data.actions || [] }; event.waitUntil( self.registration.showNotification(data.title, options) ); } }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil( clients.openWindow('/') ); }); console.log('[SW] Service worker script loaded successfully');