First Init
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
This commit is contained in:
+247
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* WorkRoot IT Solutions - Service Worker
|
||||
* Implements:
|
||||
* - Cache-first strategy for static assets
|
||||
* - Stale-while-revalidate for HTML pages
|
||||
* - Offline fallback page
|
||||
* - Background sync for form submissions
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = 'v1';
|
||||
const STATIC_CACHE = `workroot-static-${CACHE_VERSION}`;
|
||||
const PAGE_CACHE = `workroot-pages-${CACHE_VERSION}`;
|
||||
const SYNC_QUEUE = 'workroot-sync-queue';
|
||||
|
||||
// Static assets to pre-cache on install
|
||||
const PRECACHE_ASSETS = [
|
||||
'/',
|
||||
'/offline',
|
||||
'/manifest.json',
|
||||
'/favicon.svg',
|
||||
'/apple-touch-icon.png',
|
||||
];
|
||||
|
||||
// Page routes to cache with stale-while-revalidate
|
||||
const PAGE_ROUTES = [
|
||||
'/',
|
||||
'/about',
|
||||
'/services',
|
||||
'/portfolio',
|
||||
'/contact',
|
||||
'/blog',
|
||||
'/privacy',
|
||||
'/terms',
|
||||
];
|
||||
|
||||
// ─── Install Event ────────────────────────────────────────────────────────────
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE).then((cache) => {
|
||||
return cache.addAll(PRECACHE_ASSETS);
|
||||
}).then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Activate Event ───────────────────────────────────────────────────────────
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
// Delete old caches
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name.startsWith('workroot-') && name !== STATIC_CACHE && name !== PAGE_CACHE)
|
||||
.map((name) => caches.delete(name))
|
||||
);
|
||||
}),
|
||||
// Take control immediately
|
||||
self.clients.claim(),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Fetch Event ──────────────────────────────────────────────────────────────
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Only handle same-origin requests
|
||||
if (url.origin !== self.location.origin) return;
|
||||
|
||||
// Skip non-GET requests (POST etc. handled via background sync)
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
// Skip API routes — always go to network
|
||||
if (url.pathname.startsWith('/api/')) return;
|
||||
|
||||
// Static assets: cache-first
|
||||
if (isStaticAsset(url.pathname)) {
|
||||
event.respondWith(cacheFirst(request, STATIC_CACHE));
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML pages: stale-while-revalidate with offline fallback
|
||||
if (request.headers.get('Accept')?.includes('text/html')) {
|
||||
event.respondWith(staleWhileRevalidate(request));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Background Sync Event ───────────────────────────────────────────────────
|
||||
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'form-sync') {
|
||||
event.waitUntil(processSyncQueue());
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Push Event (for future notifications) ──────────────────────────────────
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
if (!event.data) return;
|
||||
|
||||
const data = event.data.json();
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title || 'WorkRoot', {
|
||||
body: data.body || '',
|
||||
icon: '/apple-touch-icon.png',
|
||||
badge: '/favicon.svg',
|
||||
data: data.url ? { url: data.url } : undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
if (event.notification.data?.url) {
|
||||
event.waitUntil(clients.openWindow(event.notification.data.url));
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Strategy Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function isStaticAsset(pathname) {
|
||||
return (
|
||||
pathname.startsWith('/_assets/') ||
|
||||
/\.(js|css|woff2?|ttf|otf|eot|png|jpg|jpeg|gif|webp|avif|svg|ico)$/i.test(pathname)
|
||||
);
|
||||
}
|
||||
|
||||
async function cacheFirst(request, cacheName) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
return new Response('Asset unavailable offline', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
async function staleWhileRevalidate(request) {
|
||||
const cache = await caches.open(PAGE_CACHE);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
const fetchPromise = fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (cached) {
|
||||
// Serve cached immediately, update in background
|
||||
fetchPromise.catch(() => {});
|
||||
return cached;
|
||||
}
|
||||
|
||||
// No cache — await network
|
||||
const response = await fetchPromise;
|
||||
if (response) return response;
|
||||
|
||||
// Network failed and no cache — serve offline fallback
|
||||
const offlineFallback = await cache.match('/offline') || await caches.match('/offline');
|
||||
if (offlineFallback) return offlineFallback;
|
||||
|
||||
return new Response(
|
||||
'<html><body><h1>You are offline</h1><p>Please check your connection.</p></body></html>',
|
||||
{ status: 503, headers: { 'Content-Type': 'text/html' } }
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Background Sync Queue ────────────────────────────────────────────────────
|
||||
|
||||
async function processSyncQueue() {
|
||||
const db = await openDB();
|
||||
const queue = await getAllFromDB(db, SYNC_QUEUE);
|
||||
|
||||
for (const item of queue) {
|
||||
try {
|
||||
const response = await fetch(item.url, {
|
||||
method: item.method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item.body),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await deleteFromDB(db, SYNC_QUEUE, item.id);
|
||||
// Notify any open clients of success
|
||||
const clients = await self.clients.matchAll();
|
||||
clients.forEach((client) => {
|
||||
client.postMessage({ type: 'SYNC_SUCCESS', id: item.id, formType: item.formType });
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Leave in queue for next sync attempt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── IndexedDB Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('workroot-pwa', 1);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(SYNC_QUEUE)) {
|
||||
const store = db.createObjectStore(SYNC_QUEUE, { keyPath: 'id', autoIncrement: true });
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => resolve(event.target.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllFromDB(db, storeName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFromDB(db, storeName, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user