- Add 89 auto-generated i18n keys covering remaining hardcoded text - Fix truncated EN value for auto.b114ed1c (server settings description) - Add data-i18n attributes to menu items, help TOC entries, and form labels - Bump CACHE_VERSION to v4 for new static assets - Remove stale orphaned file from repo - 285 tests passing, 0 failures - All 509 HTML i18n refs have corresponding keys - EN/FR perfectly aligned at 1357 keys each
191 lines
5.5 KiB
JavaScript
191 lines
5.5 KiB
JavaScript
/* ObsiGate Service Worker - PWA v2 with IndexedDB offline support */
|
|
|
|
const CACHE_VERSION = 'obsigate-v4-pwa';
|
|
const STATIC_CACHE = CACHE_VERSION + '-static';
|
|
const DYNAMIC_CACHE = CACHE_VERSION + '-dynamic';
|
|
const MAX_DYNAMIC_CACHE_SIZE = 100;
|
|
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/static/index.html',
|
|
'/static/app.js',
|
|
'/static/style.css',
|
|
'/static/manifest.json'
|
|
];
|
|
|
|
/* ── Install ── */
|
|
self.addEventListener('install', (event) => {
|
|
console.log('[SW] Installing v' + CACHE_VERSION);
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE)
|
|
.then((cache) => {
|
|
console.log('[SW] Caching static assets');
|
|
return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' })));
|
|
})
|
|
.catch((err) => console.error('[SW] Cache error:', err))
|
|
.then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
|
|
/* ── Activate ── */
|
|
self.addEventListener('activate', (event) => {
|
|
console.log('[SW] Activating...');
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter((name) => name.startsWith('obsigate-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
|
|
.map((name) => {
|
|
console.log('[SW] Deleting old cache:', name);
|
|
return caches.delete(name);
|
|
})
|
|
);
|
|
})
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
/* ── Fetch ── */
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
|
|
if (request.method !== 'GET') return;
|
|
if (url.pathname === '/api/events') return;
|
|
if (url.pathname.startsWith('/api/auth/')) return;
|
|
|
|
// API requests: Network first, tell client to use IndexedDB if offline
|
|
if (url.pathname.startsWith('/api/')) {
|
|
event.respondWith(networkFirstAPI(request));
|
|
return;
|
|
}
|
|
|
|
// Static assets: Cache first
|
|
event.respondWith(cacheFirstStatic(request, url));
|
|
});
|
|
|
|
/* ── Network First for API ── */
|
|
async function networkFirstAPI(request) {
|
|
try {
|
|
const response = await fetch(request);
|
|
|
|
// Cache successful GETs in dynamic cache
|
|
if (response && response.status === 200) {
|
|
const cache = await caches.open(DYNAMIC_CACHE);
|
|
cache.put(request, response.clone());
|
|
limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
console.log('[SW] Offline — checking cache for:', request.url);
|
|
|
|
// Try dynamic cache first
|
|
const cached = await caches.match(request);
|
|
if (cached) {
|
|
// Add offline header so the page can detect
|
|
const headers = new Headers(cached.headers);
|
|
headers.set('X-ObsiGate-Offline', 'true');
|
|
return new Response(cached.body, { status: cached.status, statusText: cached.statusText, headers });
|
|
}
|
|
|
|
// Fallback: tell the page to use IndexedDB
|
|
return new Response(JSON.stringify({
|
|
offline: true,
|
|
endpoint: new URL(request.url).pathname,
|
|
message: 'Vous etes hors-ligne. Les donnees locales sont utilisees.'
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json', 'X-ObsiGate-Offline': 'true' }
|
|
});
|
|
}
|
|
}
|
|
|
|
/* ── Cache First for Static ── */
|
|
async function cacheFirstStatic(request, url) {
|
|
const cached = await caches.match(request);
|
|
if (cached) return cached;
|
|
|
|
try {
|
|
const response = await fetch(request);
|
|
if (response && response.status === 200) {
|
|
const cache = await caches.open(STATIC_CACHE);
|
|
cache.put(request, response.clone());
|
|
}
|
|
return response;
|
|
} catch {
|
|
// Offline fallback for HTML navigation
|
|
if (request.mode === 'navigate' || url.pathname === '/' || url.pathname.endsWith('/')) {
|
|
const offlinePage = await caches.match('/static/index.html');
|
|
if (offlinePage) return offlinePage;
|
|
}
|
|
return new Response('Offline', { status: 503 });
|
|
}
|
|
}
|
|
|
|
async function limitCacheSize(cacheName, maxSize) {
|
|
const cache = await caches.open(cacheName);
|
|
const keys = await cache.keys();
|
|
if (keys.length > maxSize) {
|
|
for (let i = 0; i < keys.length - maxSize; i++) {
|
|
await cache.delete(keys[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── Messages ── */
|
|
self.addEventListener('message', (event) => {
|
|
const data = event.data;
|
|
if (!data || !data.type) return;
|
|
|
|
switch (data.type) {
|
|
case 'SKIP_WAITING':
|
|
self.skipWaiting();
|
|
break;
|
|
case 'CLEAR_CACHE':
|
|
event.waitUntil(
|
|
caches.keys().then(names => Promise.all(names.map(n => caches.delete(n))))
|
|
);
|
|
break;
|
|
case 'CLAIM_CLIENTS':
|
|
event.waitUntil(self.clients.claim());
|
|
break;
|
|
}
|
|
});
|
|
|
|
/* ── Sync ── */
|
|
self.addEventListener('sync', (event) => {
|
|
console.log('[SW] Background sync:', event.tag);
|
|
if (event.tag === 'obsigate-sync') {
|
|
event.waitUntil(
|
|
self.clients.matchAll().then(clients => {
|
|
clients.forEach(client => {
|
|
client.postMessage({ type: 'SYNC_TRIGGERED' });
|
|
});
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
/* ── Push ── */
|
|
self.addEventListener('push', (event) => {
|
|
const options = {
|
|
body: event.data ? event.data.text() : 'Nouvelle mise a jour',
|
|
icon: '/static/icons/icon-192x192.svg',
|
|
badge: '/static/icons/icon-72x72.svg',
|
|
vibrate: [200, 100, 200],
|
|
actions: [
|
|
{ action: 'open', title: 'Ouvrir ObsiGate' },
|
|
{ action: 'close', title: 'Fermer' }
|
|
]
|
|
};
|
|
event.waitUntil(self.registration.showNotification('ObsiGate', options));
|
|
});
|
|
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
if (event.action === 'open') {
|
|
event.waitUntil(clients.openWindow('/'));
|
|
}
|
|
});
|