/** * MobileApp Service Worker * Provides caching for PWA shell, assets, and offline workorder support. */ const CACHE_NAME = 'xinon-mobile-v2'; const ASSETS_TO_CACHE = [ '/MobileApp', '/mobile/app.js', '/mobile/shared/auth.js', '/mobile/shared/api.js', '/mobile/shared/base.css', '/mobile/shared/db.js', '/mobile/shared/offlineSettings.js', '/mobile/shared/syncQueue.js', '/mobile/shared/workorderOfflineService.js', '/mobile/shared/photoQueue.js', '/mobile/shared/syncManager.js', '/mobile/components/LoginScreen.js', '/mobile/components/MainMenu.js', '/mobile/components/OfflineIndicator.js', '/mobile/components/SyncStatus.js', '/mobile/modules/lager/LagerModule.js', '/mobile/modules/lager/inventur/StocktakeList.js', '/mobile/modules/lager/inventur/Scanner.js', '/mobile/modules/workorder/WorkorderModule.js', '/assets/images/xinon-full-transparent.png', '/assets/images/xinon-full-transparent-white.png', '/assets/images/xinon-sm.png' ]; // Workorder API endpoints that can be served from cache const WORKORDER_CACHEABLE_ENDPOINTS = [ '/MobileApp/Workorder/Workorder/get', '/MobileApp/Workorder/Workorder/getWorkorder', '/MobileApp/Workorder/Workorder/getWorkorderDetail', '/MobileApp/Workorder/Workorder/getDocumentation', '/MobileApp/Workorder/Workorder/getChecklist', '/MobileApp/Workorder/Workorder/getTenantConfig' ]; // Install: cache assets self.addEventListener('install', (event) => { console.log('[SW] Installing...'); event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(ASSETS_TO_CACHE)) .then(() => { console.log('[SW] Assets cached'); return self.skipWaiting(); }) .catch(err => { console.error('[SW] Cache failed:', err); }) ); }); // Activate: clean old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating...'); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name.startsWith('xinon-mobile-') && name !== CACHE_NAME) .map(name => { console.log('[SW] Deleting old cache:', name); return caches.delete(name); }) ); }).then(() => self.clients.claim()) ); }); // Fetch: handle requests with offline support self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); // Only handle same-origin requests if (url.origin !== location.origin) return; // Handle workorder API requests specially if (url.pathname.startsWith('/MobileApp/Workorder/')) { event.respondWith(handleWorkorderRequest(event.request, url)); return; } // Other API calls: network only (no caching) if (url.pathname.startsWith('/MobileApp/') && url.pathname !== '/MobileApp' && url.pathname !== '/MobileApp/') { return; } // Static assets: cache-first with background update if (event.request.method === 'GET') { event.respondWith(handleAssetRequest(event.request, url)); } }); /** * Handle workorder API requests with offline fallback */ async function handleWorkorderRequest(request, url) { const isGet = request.method === 'GET'; const isPost = request.method === 'POST'; const endpoint = url.pathname; // Check if offline mode is enabled via message or localStorage check const offlineEnabled = await isOfflineModeEnabled(); // For GET requests to cacheable endpoints if (isGet && WORKORDER_CACHEABLE_ENDPOINTS.some(ep => endpoint.startsWith(ep))) { try { // Try network first const response = await fetch(request); if (response.ok && offlineEnabled) { // Cache successful response for offline use const cache = await caches.open(CACHE_NAME + '-api'); cache.put(request, response.clone()); } return response; } catch (error) { // Network failed, try cache if (offlineEnabled) { const cached = await caches.match(request); if (cached) { console.log('[SW] Serving from cache:', endpoint); return cached; } } // Return offline error response return new Response(JSON.stringify({ success: false, error: 'Offline - keine gecachten Daten', offline: true }), { status: 503, headers: { 'Content-Type': 'application/json' } }); } } // For POST requests (mutations) - let them through, main thread handles queueing if (isPost) { try { return await fetch(request); } catch (error) { // Network error - return error response // The main thread WorkorderOfflineService handles queueing return new Response(JSON.stringify({ success: false, error: 'Netzwerkfehler', networkError: true }), { status: 503, headers: { 'Content-Type': 'application/json' } }); } } // Default: try network return fetch(request); } /** * Handle static asset requests with cache-first strategy */ async function handleAssetRequest(request, url) { const cached = await caches.match(request); if (cached) { // Return cached, update in background fetch(request).then(response => { if (response.ok) { caches.open(CACHE_NAME).then(cache => { cache.put(request, response); }); } }).catch(() => {}); return cached; } // Not cached, fetch from network try { const response = await fetch(request); if (response.ok && url.origin === location.origin) { const cache = await caches.open(CACHE_NAME); cache.put(request, response.clone()); } return response; } catch (error) { // Return offline page or error return new Response('Offline', { status: 503 }); } } /** * Check if offline mode is enabled * This is a simplified check - the main thread handles the full logic */ async function isOfflineModeEnabled() { // Try to get from clients const clients = await self.clients.matchAll(); for (const client of clients) { // Ask client for offline status // For now, assume enabled if we have cached API data const apiCache = await caches.open(CACHE_NAME + '-api'); const keys = await apiCache.keys(); return keys.length > 0; } return false; } // Background Sync event (Chrome/Edge only) self.addEventListener('sync', (event) => { console.log('[SW] Background sync triggered:', event.tag); if (event.tag === 'workorder-sync') { event.waitUntil(handleBackgroundSync()); } }); /** * Handle background sync * Notify main thread to process sync queue */ async function handleBackgroundSync() { const clients = await self.clients.matchAll({ type: 'window' }); for (const client of clients) { client.postMessage({ type: 'BACKGROUND_SYNC', tag: 'workorder-sync' }); } } // Message handler for communication with main thread self.addEventListener('message', (event) => { const { type, data } = event.data || {}; switch (type) { case 'SKIP_WAITING': self.skipWaiting(); break; case 'CLEAR_API_CACHE': caches.delete(CACHE_NAME + '-api').then(() => { console.log('[SW] API cache cleared'); event.ports[0]?.postMessage({ success: true }); }); break; case 'CACHE_WORKORDER_DATA': // Cache workorder data from main thread if (data && data.url && data.response) { caches.open(CACHE_NAME + '-api').then(cache => { const response = new Response(JSON.stringify(data.response), { headers: { 'Content-Type': 'application/json' } }); cache.put(data.url, response); console.log('[SW] Cached workorder data:', data.url); }); } break; case 'GET_CACHE_STATUS': getCacheStatus().then(status => { event.ports[0]?.postMessage(status); }); break; default: console.log('[SW] Unknown message type:', type); } }); /** * Get cache status for debugging */ async function getCacheStatus() { const assetCache = await caches.open(CACHE_NAME); const apiCache = await caches.open(CACHE_NAME + '-api'); const assetKeys = await assetCache.keys(); const apiKeys = await apiCache.keys(); return { cacheName: CACHE_NAME, assetsCached: assetKeys.length, apiCached: apiKeys.length, apiEndpoints: apiKeys.map(k => new URL(k.url).pathname) }; } // Periodic sync (if supported - experimental) self.addEventListener('periodicsync', (event) => { if (event.tag === 'workorder-periodic-sync') { console.log('[SW] Periodic sync triggered'); event.waitUntil(handleBackgroundSync()); } });