added offline mode for xinon app
This commit is contained in:
+255
-36
@@ -1,84 +1,303 @@
|
||||
/**
|
||||
* MobileApp Service Worker
|
||||
* Provides basic caching for the PWA shell and assets.
|
||||
* Provides caching for PWA shell, assets, and offline workorder support.
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'xinon-mobile-v1';
|
||||
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(() => self.skipWaiting())
|
||||
.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 !== CACHE_NAME)
|
||||
.map(name => caches.delete(name))
|
||||
.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: network-first for API, cache-first for assets
|
||||
// Fetch: handle requests with offline support
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Only handle GET requests
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// API calls: network only
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Everything else: cache-first, falling back to network
|
||||
event.respondWith(
|
||||
caches.match(event.request).then(cached => {
|
||||
if (cached) {
|
||||
// Return cached, but update in background
|
||||
fetch(event.request).then(response => {
|
||||
if (response.ok) {
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
cache.put(event.request, response);
|
||||
});
|
||||
}
|
||||
}).catch(() => {});
|
||||
return cached;
|
||||
}
|
||||
|
||||
return fetch(event.request).then(response => {
|
||||
if (response.ok && url.origin === location.origin) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
cache.put(event.request, clone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
// 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());
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user