353 lines
11 KiB
JavaScript
353 lines
11 KiB
JavaScript
// 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'); |