// 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(`
Please check your internet connection and try again.