// Service Worker for RelevantReflex Panel Management // Version: 2.1.0 // Optimized for mobile devices and offline functionality const CACHE_NAME = 'relevantreflex-v2.1.0'; const OFFLINE_CACHE = 'relevantreflex-offline-v2.1.0'; const DYNAMIC_CACHE = 'relevantreflex-dynamic-v2.1.0'; // Resources to cache for offline functionality const STATIC_ASSETS = [ '/', '/index.php', '/assets/css/style.css', '/assets/js/app.js', '/manifest.json', '/offline.html', // Fallback page // Critical fonts (subset for mobile) 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap', // Essential icons 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css', // App icons '/icons/icon-192x192.png', '/icons/icon-512x512.png', // Offline assets '/images/offline-illustration.svg' ]; // Pages that should work offline const OFFLINE_PAGES = [ '/', '/index.php', '/analytics.php', '/users.php', '/panel.php', '/finance.php', '/tickets.php', '/settings.php' ]; // Network-first resources (always try network) const NETWORK_FIRST = [ '/api/', '/ajax/', '/login.php', '/logout.php' ]; // Cache-first resources (images, fonts, etc.) const CACHE_FIRST = [ '.png', '.jpg', '.jpeg', '.svg', '.webp', '.woff', '.woff2', '.ttf', '.css', '.js' ]; // Install Service Worker self.addEventListener('install', (event) => { console.log('🔧 Service Worker installing...'); event.waitUntil( Promise.all([ // Cache static assets caches.open(CACHE_NAME).then((cache) => { console.log('📦 Caching static assets'); return cache.addAll(STATIC_ASSETS); }), // Cache offline pages caches.open(OFFLINE_CACHE).then((cache) => { console.log('📱 Caching offline pages'); return cache.addAll(OFFLINE_PAGES); }) ]).then(() => { console.log('✅ Service Worker installed successfully'); // Force activation of new service worker return self.skipWaiting(); }).catch((error) => { console.error('❌ Service Worker installation failed:', error); }) ); }); // Activate Service Worker self.addEventListener('activate', (event) => { console.log('🚀 Service Worker activating...'); event.waitUntil( Promise.all([ // Clean up old caches caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME && cacheName !== OFFLINE_CACHE && cacheName !== DYNAMIC_CACHE) { console.log('🗑️ Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }), // Take control of all clients self.clients.claim() ]).then(() => { console.log('✅ Service Worker activated successfully'); }) ); }); // Fetch event handler self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); // Skip non-GET requests and chrome-extension requests if (request.method !== 'GET' || url.protocol === 'chrome-extension:') { return; } // Handle different request types with appropriate strategies if (isNetworkFirst(url.pathname)) { event.respondWith(handleNetworkFirst(request)); } else if (isCacheFirst(url.pathname)) { event.respondWith(handleCacheFirst(request)); } else if (isOfflinePage(url.pathname)) { event.respondWith(handleStaleWhileRevalidate(request)); } else { event.respondWith(handleDefault(request)); } }); // Network-first strategy (for dynamic content) async function handleNetworkFirst(request) { try { console.log('🌐 Network-first:', request.url); // Add timeout for mobile networks const networkResponse = await Promise.race([ fetch(request), new Promise((_, reject) => setTimeout(() => reject(new Error('Network timeout')), 3000) ) ]); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.log('📱 Network failed, trying cache:', request.url); // Fallback to cache const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline page for navigation requests if (request.mode === 'navigate') { return caches.match('/offline.html'); } // Return generic offline response return new Response('Offline - Content not available', { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'text/plain' } }); } } // Cache-first strategy (for static assets) async function handleCacheFirst(request) { console.log('📦 Cache-first:', request.url); const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } try { const networkResponse = await fetch(request); if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.log('❌ Cache-first failed:', request.url); // Return placeholder for images if (request.destination === 'image') { return new Response( 'Image unavailable', { headers: { 'Content-Type': 'image/svg+xml' } } ); } throw error; } } // Stale-while-revalidate strategy (for app pages) async function handleStaleWhileRevalidate(request) { console.log('🔄 Stale-while-revalidate:', request.url); const cache = await caches.open(OFFLINE_CACHE); const cachedResponse = await cache.match(request); // Fetch in background and update cache const networkResponsePromise = fetch(request).then(async (networkResponse) => { if (networkResponse.ok) { cache.put(request, networkResponse.clone()); } return networkResponse; }).catch(() => null); // Return cached version immediately if available if (cachedResponse) { return cachedResponse; } // Wait for network if no cached version const networkResponse = await networkResponsePromise; if (networkResponse) { return networkResponse; } // Fallback to offline page return caches.match('/offline.html'); } // Default strategy async function handleDefault(request) { console.log('🔧 Default strategy:', request.url); try { const networkResponse = await fetch(request); return networkResponse; } catch (error) { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } if (request.mode === 'navigate') { return caches.match('/offline.html'); } throw error; } } // Helper functions function isNetworkFirst(pathname) { return NETWORK_FIRST.some(pattern => pathname.includes(pattern)); } function isCacheFirst(pathname) { return CACHE_FIRST.some(ext => pathname.includes(ext)); } function isOfflinePage(pathname) { return OFFLINE_PAGES.some(page => pathname === page || pathname === page.replace('.php', '') ); } // Background sync for mobile self.addEventListener('sync', (event) => { console.log('🔄 Background sync triggered:', event.tag); if (event.tag === 'background-sync') { event.waitUntil(doBackgroundSync()); } }); async function doBackgroundSync() { try { // Sync pending data when back online console.log('📡 Performing background sync...'); // Get any stored offline actions const offlineActions = await getStoredOfflineActions(); for (const action of offlineActions) { try { await fetch(action.url, { method: action.method, headers: action.headers, body: action.body }); // Remove successfully synced action await removeStoredOfflineAction(action.id); console.log('✅ Synced action:', action.id); } catch (error) { console.log('❌ Failed to sync action:', action.id, error); } } } catch (error) { console.error('❌ Background sync failed:', error); } } // Push notifications for mobile self.addEventListener('push', (event) => { console.log('📱 Push notification received'); const options = { body: event.data ? event.data.text() : 'New update available', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'explore', title: 'View', icon: '/icons/checkmark.png' }, { action: 'close', title: 'Close', icon: '/icons/xmark.png' } ], tag: 'relevantreflex-notification' }; event.waitUntil( self.registration.showNotification('RelevantReflex', options) ); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { console.log('🔔 Notification clicked:', event.action); event.notification.close(); if (event.action === 'explore') { event.waitUntil( clients.openWindow('/') ); } }); // Periodic background sync (for mobile PWAs) self.addEventListener('periodicsync', (event) => { console.log('⏰ Periodic sync triggered:', event.tag); if (event.tag === 'content-sync') { event.waitUntil( updateCriticalData() ); } }); async function updateCriticalData() { try { console.log('📊 Updating critical data...'); // Update dashboard data const dashboardResponse = await fetch('/api/dashboard-summary'); if (dashboardResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE); cache.put('/api/dashboard-summary', dashboardResponse.clone()); } // Update notifications count const notificationsResponse = await fetch('/api/notifications/count'); if (notificationsResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE); cache.put('/api/notifications/count', notificationsResponse.clone()); } console.log('✅ Critical data updated'); } catch (error) { console.error('❌ Failed to update critical data:', error); } } // Cache management for mobile (prevent cache overflow) async function manageCacheSize() { const cache = await caches.open(DYNAMIC_CACHE); const requests = await cache.keys(); // Remove oldest entries if cache is too large (mobile memory management) if (requests.length > 50) { const oldestRequests = requests.slice(0, requests.length - 40); await Promise.all( oldestRequests.map(request => cache.delete(request)) ); console.log(`🗑️ Cleaned up ${oldestRequests.length} old cache entries`); } } // Run cache management periodically setInterval(manageCacheSize, 300000); // Every 5 minutes // Utility functions for offline actions async function getStoredOfflineActions() { try { const cache = await caches.open('offline-actions'); const response = await cache.match('/offline-actions.json'); if (response) { return await response.json(); } } catch (error) { console.error('Failed to get stored offline actions:', error); } return []; } async function removeStoredOfflineAction(actionId) { try { const actions = await getStoredOfflineActions(); const updatedActions = actions.filter(action => action.id !== actionId); const cache = await caches.open('offline-actions'); await cache.put('/offline-actions.json', new Response(JSON.stringify(updatedActions))); } catch (error) { console.error('Failed to remove offline action:', error); } } // Error handling self.addEventListener('error', (event) => { console.error('💥 Service Worker error:', event.error); }); self.addEventListener('unhandledrejection', (event) => { console.error('💥 Service Worker unhandled rejection:', event.reason); }); console.log('📱 RelevantReflex Service Worker loaded - optimized for mobile');