mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-16 23:00:51 +00:00
386 lines
12 KiB
Text
386 lines
12 KiB
Text
/* eslint-disable no-restricted-globals */
|
|
|
|
// OneUptime Progressive Web App Service Worker
|
|
// Handles push notifications and caching for PWA functionality
|
|
|
|
console.log('[ServiceWorker] OneUptime PWA Service Worker Loaded');
|
|
|
|
// Cache configuration - Updated dynamically during build
|
|
// Version format: oneuptime-v{APP_VERSION}-{GIT_SHA}
|
|
// This ensures cache invalidation on every deployment
|
|
const CACHE_VERSION = 'oneuptime-v{{APP_VERSION}}-{{GIT_SHA}}'; // Auto-generated version
|
|
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
|
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
|
|
|
|
// Cache duration configuration (in milliseconds)
|
|
const CACHE_DURATIONS = {
|
|
static: 7 * 24 * 60 * 60 * 1000, // 7 days for static assets
|
|
dynamic: 24 * 60 * 60 * 1000, // 1 day for dynamic content
|
|
};
|
|
|
|
// Assets to cache immediately during install
|
|
const STATIC_ASSETS = [
|
|
'/dashboard/',
|
|
'/dashboard/manifest.json',
|
|
'/dashboard/offline.html',
|
|
'/dashboard/assets/img/favicons/favicon.ico',
|
|
'/dashboard/assets/img/favicons/android-chrome-192x192.png',
|
|
'/dashboard/assets/img/favicons/android-chrome-512x512.png',
|
|
// Add other critical assets as needed
|
|
];
|
|
|
|
// Install event - cache static assets
|
|
self.addEventListener('install', function(event) {
|
|
console.log('[ServiceWorker] Installing...');
|
|
|
|
event.waitUntil(
|
|
Promise.all([
|
|
// Cache static assets
|
|
caches.open(STATIC_CACHE).then(function(cache) {
|
|
console.log('[ServiceWorker] Pre-caching static assets');
|
|
return cache.addAll(STATIC_ASSETS.filter(url => url !== '/dashboard/'));
|
|
}),
|
|
|
|
// Skip waiting to activate immediately
|
|
self.skipWaiting()
|
|
])
|
|
);
|
|
});
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', function(event) {
|
|
console.log('[ServiceWorker] Activating...');
|
|
|
|
event.waitUntil(
|
|
Promise.all([
|
|
// Clean up old caches
|
|
caches.keys().then(function(cacheNames) {
|
|
return Promise.all(
|
|
cacheNames.map(function(cacheName) {
|
|
if (cacheName.startsWith('oneuptime-') &&
|
|
!cacheName.startsWith(CACHE_VERSION)) {
|
|
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
|
return caches.delete(cacheName);
|
|
}
|
|
})
|
|
);
|
|
}),
|
|
|
|
// Claim all clients
|
|
self.clients.claim()
|
|
])
|
|
);
|
|
});
|
|
|
|
// Fetch event - implement caching strategies
|
|
self.addEventListener('fetch', function(event) {
|
|
const request = event.request;
|
|
const url = new URL(request.url);
|
|
|
|
// Skip non-GET requests
|
|
if (request.method !== 'GET') {
|
|
return;
|
|
}
|
|
|
|
// Skip chrome-extension and other non-http(s) requests
|
|
if (!url.protocol.startsWith('http')) {
|
|
return;
|
|
}
|
|
|
|
event.respondWith(handleRequest(request));
|
|
});
|
|
|
|
// Request handling with different caching strategies
|
|
async function handleRequest(request) {
|
|
const url = new URL(request.url);
|
|
const pathname = url.pathname;
|
|
|
|
try {
|
|
// Strategy 1: Network First for HTML pages (with fallback)
|
|
if (pathname.endsWith('/') || pathname.endsWith('.html') ||
|
|
pathname === '/dashboard' || pathname.startsWith('/dashboard/') && !pathname.includes('.')) {
|
|
return await networkFirstWithFallback(request, DYNAMIC_CACHE);
|
|
}
|
|
|
|
// Strategy 2: Cache First for JavaScript, CSS, and other static assets
|
|
if (pathname.includes('/dist/') || pathname.match(/\.(js|css|woff|woff2|ttf|otf|eot)$/)) {
|
|
return await cacheFirstWithUpdate(request, STATIC_CACHE);
|
|
}
|
|
|
|
// Strategy 3: Cache First for images and other media
|
|
if (pathname.match(/\.(png|jpe?g|gif|svg|ico|webp|avif)$/)) {
|
|
return await cacheFirstWithUpdate(request, STATIC_CACHE);
|
|
}
|
|
|
|
|
|
// Strategy 5: Network First for everything else
|
|
return await networkFirstWithFallback(request, DYNAMIC_CACHE);
|
|
|
|
} catch (error) {
|
|
console.error('[ServiceWorker] Request handling error:', error);
|
|
|
|
// Return offline page for navigation requests
|
|
if (request.mode === 'navigate') {
|
|
const offlineResponse = await caches.match('/dashboard/offline.html');
|
|
if (offlineResponse) {
|
|
return offlineResponse;
|
|
}
|
|
}
|
|
|
|
// Return a basic offline response
|
|
return new Response('Offline - Please check your internet connection', {
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
});
|
|
}
|
|
}
|
|
|
|
// Caching Strategy 1: Network First with Fallback (for HTML)
|
|
async function networkFirstWithFallback(request, cacheName) {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
|
|
if (networkResponse.ok) {
|
|
// Cache successful responses
|
|
const cache = await caches.open(cacheName);
|
|
cache.put(request, networkResponse.clone());
|
|
}
|
|
|
|
return networkResponse;
|
|
} catch (error) {
|
|
console.log('[ServiceWorker] Network failed, trying cache:', request.url);
|
|
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Caching Strategy 2: Cache First with Background Update (for static assets)
|
|
async function cacheFirstWithUpdate(request, cacheName) {
|
|
const cachedResponse = await caches.match(request);
|
|
|
|
if (cachedResponse) {
|
|
// Return cached version immediately
|
|
|
|
// Background update if cache is old
|
|
const cacheDate = new Date(cachedResponse.headers.get('date') || 0);
|
|
const now = new Date();
|
|
const age = now.getTime() - cacheDate.getTime();
|
|
|
|
if (age > CACHE_DURATIONS.static) {
|
|
// Background update - don't await
|
|
updateCacheInBackground(request, cacheName);
|
|
}
|
|
|
|
return cachedResponse;
|
|
}
|
|
|
|
// Not in cache, fetch from network
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
|
|
if (networkResponse.ok) {
|
|
const cache = await caches.open(cacheName);
|
|
cache.put(request, networkResponse.clone());
|
|
}
|
|
|
|
return networkResponse;
|
|
} catch (error) {
|
|
console.error('[ServiceWorker] Failed to fetch asset:', request.url, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Background cache update
|
|
async function updateCacheInBackground(request, cacheName) {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
|
|
if (networkResponse.ok) {
|
|
const cache = await caches.open(cacheName);
|
|
await cache.put(request, networkResponse);
|
|
console.log('[ServiceWorker] Background cache update:', request.url);
|
|
}
|
|
} catch (error) {
|
|
console.log('[ServiceWorker] Background update failed:', request.url, error);
|
|
}
|
|
}
|
|
|
|
// Handle push subscription changes
|
|
self.addEventListener('pushsubscriptionchange', function(event) {
|
|
console.log('[ServiceWorker] Push subscription changed:', event);
|
|
|
|
// Re-subscribe to push notifications
|
|
event.waitUntil(
|
|
self.registration.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: null // This should be set by your application
|
|
})
|
|
.then(function(newSubscription) {
|
|
console.log('[ServiceWorker] New push subscription:', newSubscription);
|
|
// Send new subscription to your server
|
|
return updatePushSubscription(newSubscription);
|
|
})
|
|
.catch(function(error) {
|
|
console.error('[ServiceWorker] Failed to resubscribe to push:', error);
|
|
})
|
|
);
|
|
});
|
|
|
|
// Handle push notifications
|
|
self.addEventListener('push', function(event) {
|
|
console.log('[ServiceWorker] Push received:', event);
|
|
console.log('[ServiceWorker] Event data available:', !!event.data);
|
|
|
|
if (event.data) {
|
|
try {
|
|
const dataText = event.data.text();
|
|
console.log('[ServiceWorker] Raw push data:', dataText);
|
|
|
|
const data = event.data.json();
|
|
console.log('[ServiceWorker] Push data (parsed):', data);
|
|
|
|
const options = {
|
|
body: data.body,
|
|
icon: data.icon || '/dashboard/assets/img/favicons/android-chrome-192x192.png',
|
|
badge: data.badge || '/dashboard/assets/img/favicons/favicon-32x32.png',
|
|
tag: data.tag || 'oneuptime-notification',
|
|
requireInteraction: data.requireInteraction || false,
|
|
actions: data.actions || [],
|
|
data: data.data || {},
|
|
silent: false,
|
|
renotify: true,
|
|
vibrate: [100, 50, 100],
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
console.log('[ServiceWorker] Showing notification with options:', options);
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(data.title, options)
|
|
.then(() => {
|
|
console.log('[ServiceWorker] Notification shown successfully');
|
|
})
|
|
.catch((error) => {
|
|
console.error('[ServiceWorker] Error showing notification:', error);
|
|
})
|
|
);
|
|
} catch (error) {
|
|
console.error('[ServiceWorker] Error parsing push data:', error);
|
|
const rawData = event.data ? event.data.text() : 'No data';
|
|
console.log('[ServiceWorker] Raw event data:', rawData);
|
|
|
|
// Show fallback notification
|
|
event.waitUntil(
|
|
self.registration.showNotification('OneUptime Notification', {
|
|
body: 'You have a new notification from OneUptime',
|
|
icon: '/dashboard/assets/img/favicons/android-chrome-192x192.png',
|
|
tag: 'oneuptime-fallback',
|
|
data: { url: '/dashboard' }
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
console.log('[ServiceWorker] Push event received but no data');
|
|
|
|
// Show default notification
|
|
event.waitUntil(
|
|
self.registration.showNotification('OneUptime', {
|
|
body: 'You have a new notification',
|
|
icon: '/dashboard/assets/img/favicons/android-chrome-192x192.png',
|
|
tag: 'oneuptime-default',
|
|
data: { url: '/dashboard' }
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
// Handle notification clicks
|
|
self.addEventListener('notificationclick', function(event) {
|
|
console.log('[ServiceWorker] Notification clicked:', event.notification.tag);
|
|
event.notification.close();
|
|
|
|
const clickAction = event.action;
|
|
const notificationData = event.notification.data || {};
|
|
|
|
let targetUrl = '/dashboard';
|
|
|
|
if (clickAction && notificationData[clickAction]) {
|
|
// Handle action button clicks
|
|
targetUrl = notificationData[clickAction].url || targetUrl;
|
|
} else {
|
|
// Handle main notification click
|
|
targetUrl = notificationData.url ||
|
|
notificationData.clickAction ||
|
|
notificationData.link ||
|
|
targetUrl;
|
|
}
|
|
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
.then(function(clientList) {
|
|
// Check if there's already a OneUptime window open
|
|
for (let i = 0; i < clientList.length; i++) {
|
|
const client = clientList[i];
|
|
const clientUrl = new URL(client.url);
|
|
|
|
if (clientUrl.origin === self.location.origin && 'focus' in client) {
|
|
// Navigate to the target URL and focus the window
|
|
return client.navigate(targetUrl)
|
|
.then(() => client.focus());
|
|
}
|
|
}
|
|
|
|
// If no OneUptime window is open, open a new one
|
|
if (clients.openWindow) {
|
|
return clients.openWindow(targetUrl);
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
console.error('[ServiceWorker] Error handling notification click:', error);
|
|
})
|
|
);
|
|
});
|
|
|
|
// Handle notification close events
|
|
self.addEventListener('notificationclose', function(event) {
|
|
console.log('[ServiceWorker] Notification closed:', event.notification.tag);
|
|
});
|
|
|
|
// Handle background sync - removed offline functionality
|
|
self.addEventListener('sync', function(event) {
|
|
console.log('[ServiceWorker] Background sync:', event.tag);
|
|
// Background sync events can still be handled but no offline caching
|
|
});
|
|
|
|
// Handle messages from the main thread
|
|
self.addEventListener('message', function(event) {
|
|
console.log('[ServiceWorker] Message received:', event.data);
|
|
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
} else if (event.data && event.data.type === 'GET_VERSION') {
|
|
event.ports[0].postMessage({ version: CACHE_VERSION });
|
|
}
|
|
});
|
|
|
|
// Helper function to update push subscription
|
|
function updatePushSubscription(subscription) {
|
|
return fetch('/api/push-subscription', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(subscription)
|
|
})
|
|
.catch(function(error) {
|
|
console.error('[ServiceWorker] Error updating push subscription:', error);
|
|
});
|
|
}
|
|
|
|
console.log('[ServiceWorker] OneUptime PWA Service Worker Ready');
|