feat: Implement service worker generation script and update build process for Dashboard

This commit is contained in:
Simon Larsen 2025-07-30 11:18:55 +01:00
parent 0a6cdd11af
commit 5eee900fd3
No known key found for this signature in database
GPG key ID: 96C5DCA24769DBCA
7 changed files with 620 additions and 7 deletions

View file

@ -0,0 +1,144 @@
#!/usr/bin/env node
/**
* Universal Service Worker Generator for OneUptime Services
*
* This script can be used by any OneUptime service to generate
* a service worker from a template with dynamic versioning.
*
* Usage:
* node generate-service-worker.js [template-path] [output-path]
*
* Example:
* node generate-service-worker.js sw.js.template public/sw.js
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// Default values
const DEFAULT_APP_VERSION = '1.0.0';
const DEFAULT_GIT_SHA = 'local';
/**
* Get app version from environment or package.json
*/
function getAppVersion(packageJsonPath) {
// First try environment variable (Docker build)
if (process.env.APP_VERSION) {
return process.env.APP_VERSION;
}
// Fallback to package.json version
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version || DEFAULT_APP_VERSION;
} catch (error) {
console.warn('Could not read package.json, using default version');
return DEFAULT_APP_VERSION;
}
}
/**
* Get git SHA from environment
*/
function getGitSha() {
// Try environment variable first (Docker build)
if (process.env.GIT_SHA) {
return process.env.GIT_SHA.substring(0, 8); // Short SHA
}
// Try to get from git command if available
try {
const { execSync } = require('child_process');
const gitSha = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
return gitSha;
} catch (error) {
// Fallback to timestamp-based hash for local development
const timestamp = Date.now().toString();
const hash = crypto.createHash('md5').update(timestamp).digest('hex');
return hash.substring(0, 8);
}
}
/**
* Generate service worker from template
*/
function generateServiceWorker(templatePath, outputPath, serviceName = 'OneUptime') {
// Check if template exists
if (!fs.existsSync(templatePath)) {
console.error('❌ Service worker template not found:', templatePath);
process.exit(1);
}
// Read template
const template = fs.readFileSync(templatePath, 'utf8');
// Get version information
const packageJsonPath = path.join(path.dirname(templatePath), 'package.json');
const appVersion = getAppVersion(packageJsonPath);
const gitSha = getGitSha();
const buildTimestamp = new Date().toISOString();
console.log(`🔧 Generating service worker for ${serviceName}...`);
console.log(` App Version: ${appVersion}`);
console.log(` Git SHA: ${gitSha}`);
console.log(` Build Time: ${buildTimestamp}`);
// Replace placeholders
const generatedContent = template
.replace(/\{\{APP_VERSION\}\}/g, appVersion)
.replace(/\{\{GIT_SHA\}\}/g, gitSha)
.replace(/\{\{BUILD_TIMESTAMP\}\}/g, buildTimestamp)
.replace(/\{\{SERVICE_NAME\}\}/g, serviceName);
// Add generation comment at the top
const header = `/*
* Generated Service Worker for ${serviceName}
*
* Generated at: ${buildTimestamp}
* App Version: ${appVersion}
* Git SHA: ${gitSha}
*
* DO NOT EDIT THIS FILE DIRECTLY
* Edit the template file instead and run the generator script
*/
`;
const finalContent = header + generatedContent;
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write generated service worker
fs.writeFileSync(outputPath, finalContent, 'utf8');
console.log('✅ Service worker generated successfully:', outputPath);
console.log(` Cache version: oneuptime-v${appVersion}-${gitSha}`);
}
// Command line interface
if (require.main === module) {
const args = process.argv.slice(2);
const templatePath = args[0] || 'sw.js.template';
const outputPath = args[1] || 'public/sw.js';
const serviceName = args[2] || path.basename(process.cwd());
try {
// Resolve paths relative to current working directory
const resolvedTemplatePath = path.resolve(templatePath);
const resolvedOutputPath = path.resolve(outputPath);
generateServiceWorker(resolvedTemplatePath, resolvedOutputPath, serviceName);
} catch (error) {
console.error('❌ Failed to generate service worker:', error.message);
process.exit(1);
}
}
module.exports = { generateServiceWorker, getAppVersion, getGitSha };

View file

@ -19,6 +19,10 @@ node_modules
env.js
# Note: public/sw.js is auto-generated from sw.js.template during build
# but should be committed to ensure it exists for deployments
# The file is regenerated with correct versions during Docker build
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View file

@ -3,13 +3,14 @@
"version": "0.1.0",
"private": false,
"scripts": {
"dev-build": "NODE_ENV=development node esbuild.config.js",
"generate-sw": "node scripts/generate-sw.js",
"dev-build": "npm run generate-sw && NODE_ENV=development node esbuild.config.js",
"dev": "npx nodemon",
"build": "NODE_ENV=production node esbuild.config.js",
"analyze": "analyze=true NODE_ENV=production node esbuild.config.js",
"build": "npm run generate-sw && NODE_ENV=production node esbuild.config.js",
"analyze": "npm run generate-sw && analyze=true NODE_ENV=production node esbuild.config.js",
"test": "react-app-rewired test",
"eject": "echo 'esbuild does not require eject'",
"compile": "tsc",
"compile": "npm run generate-sw && tsc",
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
"start": "node --require ts-node/register Serve.ts",
"audit": "npm audit --audit-level=low",

View file

@ -1,3 +1,25 @@
/*
* Generated Service Worker for OneUptime Dashboard
*
* Generated at: 2025-07-30T10:17:31.747Z
* App Version: 0.1.0
* Git SHA: 0a6cdd11af
*
* DO NOT EDIT THIS FILE DIRECTLY
* Edit the template file instead and run the generator script
*/
/*
* Generated Service Worker for OneUptime Dashboard
*
* Generated at: 2025-07-30T10:09:37.995Z
* App Version: 0.1.0
* Git SHA: 0a6cdd11af
*
* DO NOT EDIT THIS FILE DIRECTLY
* Edit the template file instead and run the generator script
*/
/* eslint-disable no-restricted-globals */
// OneUptime Progressive Web App Service Worker
@ -5,8 +27,10 @@
console.log('[ServiceWorker] OneUptime PWA Service Worker Loaded');
// Cache configuration
const CACHE_VERSION = 'oneuptime-v1.2.0'; // Update this when deploying new versions
// 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-v0.1.0-0a6cdd11af'; // Auto-generated version
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
@ -363,7 +387,7 @@ self.addEventListener('message', function(event) {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
} else if (event.data && event.data.type === 'GET_VERSION') {
event.ports[0].postMessage({ version: 'oneuptime-pwa-no-cache' });
event.ports[0].postMessage({ version: CACHE_VERSION });
}
});

View file

@ -0,0 +1,21 @@
#!/bin/bash
# Development Service Worker Generation Script
#
# This script can be used during local development to test
# the service worker generation with sample environment variables
echo "🔧 Generating service worker for local development..."
# Set sample environment variables for testing
export APP_VERSION="1.0.0-dev"
export GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "local-dev")
echo "Using APP_VERSION: $APP_VERSION"
echo "Using GIT_SHA: $GIT_SHA"
# Generate the service worker
node scripts/generate-sw.js
echo "✅ Service worker generated for development"
echo "🔍 Check public/sw.js to see the generated file"

View file

@ -0,0 +1,22 @@
#!/usr/bin/env node
/**
* Dashboard Service Worker Generator
*
* This script generates the Dashboard service worker from a template,
* using the universal generator from Common/Scripts.
*/
const path = require('path');
const { generateServiceWorker } = require('../../Common/Scripts/generate-service-worker');
// Generate Dashboard service worker
const templatePath = path.join(__dirname, '..', 'sw.js.template');
const outputPath = path.join(__dirname, '..', 'public', 'sw.js');
try {
generateServiceWorker(templatePath, outputPath, 'OneUptime Dashboard');
} catch (error) {
console.error('❌ Failed to generate Dashboard service worker:', error.message);
process.exit(1);
}

397
Dashboard/sw.js.template Normal file
View file

@ -0,0 +1,397 @@
/*
* Generated Service Worker for OneUptime Dashboard
*
* Generated at: 2025-07-30T10:09:37.995Z
* App Version: 0.1.0
* Git SHA: 0a6cdd11af
*
* DO NOT EDIT THIS FILE DIRECTLY
* Edit the template file instead and run the generator script
*/
/* 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-v0.1.0-0a6cdd11af'; // 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');