/** * MYP Platform Service Worker * Offline-First Caching Strategy für Mercedes-Benz 3D-Druck-Management */ // Cache-Namen für verschiedene Ressourcen-Typen const CACHE_NAME = 'myp-platform-cache-v1'; const STATIC_CACHE = 'myp-static-v1'; const DYNAMIC_CACHE = 'myp-dynamic-v1'; // Statische Assets die gecacht werden sollen 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' ]; // Patterns für statische Dateien const STATIC_PATTERNS = [ /\.css$/, /\.js$/, /\.svg$/, /\.png$/, /\.ico$/, /\.woff2?$/ ]; // API-Request-Patterns die nicht gecacht werden sollen const API_PATTERNS = [ /^\/api\//, /^\/auth\// ]; // API-Endpoints die gecacht werden können const API_CACHE_PATTERNS = [ /^\/api\/dashboard/, /^\/api\/printers/, /^\/api\/jobs/, /^\/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 mit korrekter Response-Behandlung self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Nur GET-Requests und HTTP/HTTPS unterstützen if (request.method !== 'GET' || (url.protocol !== 'http:' && url.protocol !== 'https:')) { return; // Lasse andere Requests durch } // Chrome-Extension-URLs ignorieren if (url.protocol === 'chrome-extension:') { return; } // Request-Typ bestimmen und entsprechend behandeln if (isStaticFile(url.pathname)) { event.respondWith(handleStaticFile(request)); } else if (isAPIRequest(url.pathname)) { event.respondWith(handleAPIRequest(request)); } else if (isPageRequest(request)) { event.respondWith(handlePageRequest(request)); } else { // Fallback für andere Requests event.respondWith(handleGenericRequest(request)); } }); // Statische Dateien behandeln - Cache First Strategy async function handleStaticFile(request) { try { // Zuerst im Cache suchen const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Netzwerk-Request versuchen const networkResponse = await fetch(request); // Erfolgreiche Responses cachen if (networkResponse && 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); // Cache-Fallback versuchen const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Offline-Fallback Response return new Response('Offline - Datei nicht verfügbar', { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); } } // API-Requests behandeln - Network First mit Cache-Fallback async function handleAPIRequest(request) { try { // Netzwerk-Request versuchen const networkResponse = await fetch(request); if (networkResponse && networkResponse.ok) { // GET-Requests für bestimmte Endpoints cachen if (request.method === 'GET' && shouldCacheAPIResponse(request.url)) { 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'); // Cache-Fallback für GET-Requests if (request.method === 'GET') { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } } // Offline-Response für API-Requests 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; charset=utf-8' } }); } } // Seiten-Requests behandeln - Network First mit Offline-Fallback async function handlePageRequest(request) { try { // Netzwerk-Request versuchen const networkResponse = await fetch(request); if (networkResponse && networkResponse.ok) { // Erfolgreiche Seiten-Responses cachen 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'); // Cache-Fallback versuchen const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Offline-Seite als Fallback return new Response(` MYP Platform - Offline

Offline

Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.

`, { status: 200, statusText: 'OK', headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } } // Generische Request-Behandlung async function handleGenericRequest(request) { try { return await fetch(request); } catch (error) { console.error('Service Worker: Generic request failed', error); return new Response('Request failed', { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); } } // Hilfsfunktionen für Request-Typ-Erkennung function isStaticFile(pathname) { return STATIC_PATTERNS.some(pattern => pattern.test(pathname)); } function isAPIRequest(pathname) { return API_PATTERNS.some(pattern => pattern.test(pathname)); } function isPageRequest(request) { return request.mode === 'navigate' || request.headers.get('accept')?.includes('text/html'); } function shouldCacheAPIResponse(url) { const pathname = new URL(url).pathname; return API_CACHE_PATTERNS.some(pattern => pattern.test(pathname)); } // Background Sync für Offline-Aktionen self.addEventListener('sync', (event) => { console.log('Service Worker: Background sync triggered', event.tag); if (event.tag === 'background-sync') { event.waitUntil(doBackgroundSync()); } }); async function doBackgroundSync() { try { // Pending Requests aus IndexedDB oder localStorage holen 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); } } // Placeholder-Funktionen für IndexedDB-Integration async function getPendingRequests() { return []; } async function removePendingRequest(id) { 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 vom 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) ); } }); // Spezifische URLs cachen 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 (falls unterstützt) self.addEventListener('periodicsync', (event) => { console.log('Service Worker: Periodic sync triggered', event.tag); if (event.tag === 'content-sync') { event.waitUntil(syncContent()); } }); // Content periodisch synchronisieren async function syncContent() { try { const endpoints = ['/api/dashboard', '/api/jobs']; for (const endpoint of endpoints) { try { const response = await fetch(endpoint); if (response && 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 successfully');