📝 Commit Details:
This commit is contained in:
432
backend/static/js/sw.js
Normal file
432
backend/static/js/sw.js
Normal file
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* 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(
|
||||
'<html><body><h1>Offline</h1><p>Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.</p></body></html>',
|
||||
{
|
||||
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');
|
Reference in New Issue
Block a user