Files
thetool/public/mobile/sw.js
2026-01-27 10:35:15 +01:00

304 lines
9.4 KiB
JavaScript

/**
* 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());
}
});