353 lines
11 KiB
JavaScript
Raw Normal View History

2025-10-07 10:59:52 +01:00
// 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(`
<!DOCTYPE html>
<html>
<head>
<title>Control Center - Offline</title>
<style>
body {
font-family: system-ui, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #f3f4f6;
}
.offline {
text-align: center;
padding: 2rem;
background: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="offline">
<h1>You're Offline</h1>
<p>Please check your internet connection and try again.</p>
<button onclick="window.location.reload()">Retry</button>
</div>
</body>
</html>
`, {
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');