/** * MYP Platform Service Worker * Offline-First Caching Strategy */ // MYP Platform Service Worker const CACHE_NAME = 'myp-platform-cache-v1'; const STATIC_CACHE = 'myp-static-v1'; const DYNAMIC_CACHE = 'myp-dynamic-v1'; const ASSETS_TO_CACHE = [ '/', '/dashboard', '/static/css/tailwind.min.css', '/static/css/tailwind-dark.min.css', '/static/js/ui-components.js', '/static/js/offline-app.js', '/static/icons/mercedes-logo.svg', '/static/icons/icon-144x144.png', '/static/favicon.ico' ]; // Static files patterns const STATIC_PATTERNS = [ /\.css$/, /\.js$/, /\.svg$/, /\.png$/, /\.ico$/, /\.woff2?$/ ]; // API request patterns to avoid caching const API_PATTERNS = [ /^\/api\//, /^\/auth\//, /^\/api\/jobs/, /^\/api\/printers/, /^\/api\/stats/ ]; // Install event - cache core assets self.addEventListener('install', (event) => { console.log('Service Worker: Installing...'); event.waitUntil( caches.open(STATIC_CACHE) .then((cache) => { console.log('Service Worker: Caching static files'); return cache.addAll(ASSETS_TO_CACHE); }) .then(() => { console.log('Service Worker: Static files cached'); return self.skipWaiting(); }) .catch((error) => { console.error('Service Worker: Error caching static files', 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.map((cacheName) => { if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) { console.log('Service Worker: Deleting old cache', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('Service Worker: Activated'); return self.clients.claim(); }) ); }); // Fetch event - handle requests self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Unterstütze sowohl HTTP als auch HTTPS if (request.method !== 'GET' || (url.protocol !== 'http:' && url.protocol !== 'https:')) { return; } // Simple network-first approach event.respondWith( fetch(request) .then((response) => { // Cache successful static responses if (response && response.status === 200 && isStaticFile(url.pathname) && (url.protocol === 'http:' || url.protocol === 'https:')) { const responseClone = response.clone(); caches.open(STATIC_CACHE).then((cache) => { cache.put(request, responseClone); }).catch(err => { console.warn('Failed to cache response:', err); }); } return response; }) .catch(() => { // Fallback to cache if network fails return caches.match(request); }) ); }); // Check if request is for a static file function isStaticFile(pathname) { return STATIC_PATTERNS.some(pattern => pattern.test(pathname)); } // Check if request is an API request function isAPIRequest(pathname) { return API_PATTERNS.some(pattern => pattern.test(pathname)); } // Check if request is for a page function isPageRequest(request) { return request.mode === 'navigate'; } // MYP Platform Service Worker const STATIC_FILES = [ '/', '/static/css/tailwind.min.css', '/static/css/tailwind-dark.min.css', '/static/js/ui-components.js', '/static/js/offline-app.js', '/login', '/dashboard' ]; // API endpoints to cache const API_CACHE_PATTERNS = [ /^\/api\/dashboard/, /^\/api\/printers/, /^\/api\/jobs/, /^\/api\/stats/ ]; // Handle static file requests - Cache First strategy async function handleStaticFile(request) { try { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } const networkResponse = await fetch(request); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(STATIC_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.error('Service Worker: Error handling static file', error); // Return cached version if available const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline fallback return new Response('Offline - Datei nicht verfügbar', { status: 503, statusText: 'Service Unavailable' }); } } // Handle API requests - Network First with cache fallback async function handleAPIRequest(request) { const url = new URL(request.url); // Skip caching for chrome-extension URLs if (url.protocol === 'chrome-extension:') { try { return await fetch(request); } catch (error) { console.error('Failed to fetch from chrome-extension:', error); return new Response(JSON.stringify({ error: 'Fehler beim Zugriff auf chrome-extension', offline: true }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } } try { // Try network first const networkResponse = await fetch(request); if (networkResponse.ok) { // Cache successful GET responses for specific endpoints if (request.method === 'GET' && shouldCacheAPIResponse(url.pathname)) { const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } throw new Error(`HTTP ${networkResponse.status}`); } catch (error) { console.log('Service Worker: Network failed for API request, trying cache'); // Try cache fallback for GET requests const cachedResponse = await caches.match(request); if (cachedResponse) { // Add offline header to indicate cached response const response = cachedResponse.clone(); response.headers.set('X-Served-By', 'ServiceWorker-Cache'); return response; } // Return offline response return new Response(JSON.stringify({ error: 'Offline - Daten nicht verfügbar', offline: true, timestamp: new Date().toISOString() }), { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json', 'X-Served-By': 'ServiceWorker-Offline' } }); } } // Handle page requests - Network First with offline fallback async function handlePageRequest(request) { try { const networkResponse = await fetch(request); if (networkResponse.ok) { // Cache successful page responses const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, networkResponse.clone()); return networkResponse; } throw new Error(`HTTP ${networkResponse.status}`); } catch (error) { console.log('Service Worker: Network failed for page request, trying cache'); // Try cache fallback const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Fallback offline response return caches.match('/offline.html') || new Response( '
Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.
', { status: 200, headers: { 'Content-Type': 'text/html' } } ); } } // Check if API response should be cached function shouldCacheAPIResponse(pathname) { return API_CACHE_PATTERNS.some(pattern => pattern.test(pathname)); } // Background sync for offline actions self.addEventListener('sync', (event) => { console.log('Service Worker: Background sync triggered', event.tag); if (event.tag === 'background-sync') { event.waitUntil(doBackgroundSync()); } }); // Perform background sync async function doBackgroundSync() { try { // Get pending requests from IndexedDB or localStorage const pendingRequests = await getPendingRequests(); for (const request of pendingRequests) { try { await fetch(request.url, request.options); await removePendingRequest(request.id); console.log('Service Worker: Synced request', request.url); } catch (error) { console.error('Service Worker: Failed to sync request', request.url, error); } } } catch (error) { console.error('Service Worker: Background sync failed', error); } } // Get pending requests (placeholder - implement with IndexedDB) async function getPendingRequests() { // This would typically use IndexedDB to store pending requests // For now, return empty array return []; } // Remove pending request (placeholder - implement with IndexedDB) async function removePendingRequest(id) { // This would typically remove the request from IndexedDB console.log('Service Worker: Removing pending request', id); } // Push notification handling self.addEventListener('push', (event) => { console.log('Service Worker: Push notification received'); const options = { body: 'Sie haben neue Benachrichtigungen', icon: '/static/icons/icon-192x192.png', badge: '/static/icons/badge-72x72.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'explore', title: 'Anzeigen', icon: '/static/icons/checkmark.png' }, { action: 'close', title: 'Schließen', icon: '/static/icons/xmark.png' } ] }; event.waitUntil( self.registration.showNotification('MYP Platform', options) ); }); // Notification click handling self.addEventListener('notificationclick', (event) => { console.log('Service Worker: Notification clicked'); event.notification.close(); if (event.action === 'explore') { event.waitUntil( clients.openWindow('/dashboard') ); } }); // Message handling from main thread self.addEventListener('message', (event) => { console.log('Service Worker: Message received', event.data); if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CACHE_URLS') { event.waitUntil( cacheUrls(event.data.urls) ); } }); // Cache specific URLs async function cacheUrls(urls) { try { const cache = await caches.open(DYNAMIC_CACHE); await cache.addAll(urls); console.log('Service Worker: URLs cached', urls); } catch (error) { console.error('Service Worker: Error caching URLs', error); } } // Periodic background sync (if supported) self.addEventListener('periodicsync', (event) => { console.log('Service Worker: Periodic sync triggered', event.tag); if (event.tag === 'content-sync') { event.waitUntil(syncContent()); } }); // Sync content periodically async function syncContent() { try { // Sync critical data in background const endpoints = ['/api/dashboard', '/api/jobs']; for (const endpoint of endpoints) { try { const response = await fetch(endpoint); if (response.ok) { const cache = await caches.open(DYNAMIC_CACHE); cache.put(endpoint, response.clone()); } } catch (error) { console.error('Service Worker: Error syncing', endpoint, error); } } } catch (error) { console.error('Service Worker: Content sync failed', error); } } console.log('Service Worker: Script loaded');