| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/client-portal-laravel/releases/20260626120707/public/ |
Upload File : |
/**
* Service Worker for ScopeForged Client Portal
* Handles push notifications and offline caching
*/
const CACHE_NAME = 'scopeforged-v1';
const OFFLINE_URL = '/offline.html';
// Assets to cache for offline use
const STATIC_ASSETS = [
'/',
'/offline.html',
'/images/logos/favicon.svg',
'/images/logos/web-app-manifest-192x192.png',
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => cacheName !== CACHE_NAME)
.map((cacheName) => caches.delete(cacheName))
);
}).then(() => self.clients.claim())
);
});
// Fetch event - network first, fall back to cache
self.addEventListener('fetch', (event) => {
// Skip cross-origin requests
if (!event.request.url.startsWith(self.location.origin)) {
return;
}
// Skip non-GET requests
if (event.request.method !== 'GET') {
return;
}
// Skip API requests - always go to network
if (event.request.url.includes('/api/') ||
event.request.url.includes('/sanctum/') ||
event.request.url.includes('/livewire/')) {
return;
}
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone the response for caching
const responseClone = response.clone();
// Cache successful responses
if (response.status === 200) {
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
}
return response;
})
.catch(() => {
// Try to return cached response
return caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for navigation requests
if (event.request.mode === 'navigate') {
return caches.match(OFFLINE_URL);
}
return new Response('Network error', {
status: 503,
statusText: 'Service Unavailable'
});
});
})
);
});
// Push notification event
self.addEventListener('push', (event) => {
if (!event.data) {
console.log('Push event but no data');
return;
}
let data;
try {
data = event.data.json();
} catch (e) {
data = {
title: 'ScopeForged',
body: event.data.text(),
icon: '/images/logos/web-app-manifest-192x192.png'
};
}
const options = {
body: data.body || data.message || 'You have a new notification',
icon: data.icon || '/images/logos/web-app-manifest-192x192.png',
badge: '/images/logos/favicon-96x96.png',
vibrate: [100, 50, 100],
data: {
url: data.url || data.action_url || '/',
type: data.type || 'general',
id: data.id || null
},
actions: data.actions || [],
tag: data.tag || 'default',
renotify: data.renotify || false,
requireInteraction: data.requireInteraction || false,
silent: data.silent || false
};
// Add default actions based on notification type
if (data.type === 'audit_deadline_reminder' || data.type === 'stale_audit_item') {
options.actions = [
{ action: 'view', title: 'View Details' },
{ action: 'dismiss', title: 'Dismiss' }
];
options.requireInteraction = true;
} else if (data.type === 'audit_assignment') {
options.actions = [
{ action: 'view', title: 'View Finding' },
{ action: 'dismiss', title: 'Later' }
];
}
event.waitUntil(
self.registration.showNotification(data.title || 'ScopeForged', options)
);
});
// Notification click event
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const notificationData = event.notification.data || {};
let url = notificationData.url || '/';
// Handle action buttons
if (event.action === 'dismiss') {
return; // Just close the notification
}
if (event.action === 'view' && notificationData.url) {
url = notificationData.url;
}
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
// Try to focus an existing window
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.focus();
client.navigate(url);
return;
}
}
// Open a new window if none exists
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
// Handle notification close
self.addEventListener('notificationclose', (event) => {
// Track notification dismissal for analytics if needed
const notificationData = event.notification.data || {};
// Could send analytics here
console.log('Notification closed:', notificationData.type);
});
// Background sync for offline actions
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-status-updates') {
event.waitUntil(syncStatusUpdates());
}
});
// Sync queued status updates when back online
async function syncStatusUpdates() {
try {
const db = await openDB();
const updates = await getAllFromStore(db, 'pending-updates');
for (const update of updates) {
try {
const response = await fetch(update.url, {
method: update.method,
headers: update.headers,
body: JSON.stringify(update.body)
});
if (response.ok) {
await deleteFromStore(db, 'pending-updates', update.id);
}
} catch (e) {
console.error('Failed to sync update:', e);
}
}
} catch (e) {
console.error('Sync failed:', e);
}
}
// Simple IndexedDB helpers
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('scopeforged-offline', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('pending-updates')) {
db.createObjectStore('pending-updates', { keyPath: 'id', autoIncrement: true });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function getAllFromStore(db, storeName) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function deleteFromStore(db, storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}