feat: finalisation éditeur Forge + mode hors-ligne PWA + consolidation roadmap
CI / lint (push) Failing after 7s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Failing after -7s

- Forge: Tab indent, Ctrl+K, auto-focus, AI status, messages erreur FR
- Forge: slash commands clean, cache-bust iframe, preview serveur
- Forge: iframe 82vh, hauteur auto, table inline, sélecteur langage
- PWA hors-ligne: IndexedDB (offline-db.js + offline.js)
- Mermaid: thème dark complet, highlight.js preview
- Backend: màj CSP, rename chain, bookmarks/history/shares
- Roadmap: consolidation dans docs/ROADMAP.md (75 items)
- Infra: docker-compose.podman.yml, suppression test-vault
- .gitignore: exclusion odysseus/
This commit is contained in:
2026-06-14 20:27:37 -04:00
parent 0cdebc5b40
commit feeb46b873
31 changed files with 1769 additions and 981 deletions
+97 -144
View File
@@ -1,11 +1,10 @@
/* ObsiGate Service Worker - PWA Support */
/* ObsiGate Service Worker - PWA v2 with IndexedDB offline support */
const CACHE_VERSION = 'obsigate-v1.9.0';
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
const MAX_DYNAMIC_CACHE_SIZE = 50;
const CACHE_VERSION = 'obsigate-v2-pwa';
const STATIC_CACHE = CACHE_VERSION + '-static';
const DYNAMIC_CACHE = CACHE_VERSION + '-dynamic';
const MAX_DYNAMIC_CACHE_SIZE = 100;
// Assets to cache on install
const STATIC_ASSETS = [
'/',
'/static/index.html',
@@ -14,25 +13,23 @@ const STATIC_ASSETS = [
'/static/manifest.json'
];
// Install event - cache static assets
/* ── Install ── */
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
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] Failed to cache static assets:', err);
})
.catch((err) => console.error('[SW] Cache error:', err))
.then(() => self.skipWaiting())
);
});
// Activate event - clean up old caches
/* ── Activate ── */
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
console.log('[SW] Activating...');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
@@ -49,189 +46,145 @@ self.addEventListener('activate', (event) => {
);
});
// Fetch event - serve from cache, fallback to network
/* ── Fetch ── */
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
if (request.method !== 'GET') return;
if (url.pathname === '/api/events') return;
if (url.pathname.startsWith('/api/auth/')) return;
// Skip SSE connections
if (url.pathname === '/api/events') {
return;
}
// Skip authentication endpoints
if (url.pathname.startsWith('/api/auth/')) {
return;
}
// API requests - Network first, cache fallback
// API requests: Network first, tell client to use IndexedDB if offline
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstStrategy(request));
event.respondWith(networkFirstAPI(request));
return;
}
// Static assets - Cache first, network fallback
event.respondWith(cacheFirstStrategy(request));
// Static assets: Cache first
event.respondWith(cacheFirstStatic(request, url));
});
// Cache first strategy (for static assets)
async function cacheFirstStrategy(request) {
/* ── Network First for API ── */
async function networkFirstAPI(request) {
try {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const response = await fetch(request);
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse && networkResponse.status === 200) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('[SW] Cache first strategy failed:', error);
// Return offline page if available
const offlinePage = await caches.match('/static/index.html');
if (offlinePage) {
return offlinePage;
}
return new Response('Offline - Unable to fetch resource', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/plain'
})
});
}
}
// Network first strategy (for API calls)
async function networkFirstStrategy(request) {
try {
const networkResponse = await fetch(request);
// Cache successful GET responses
if (networkResponse && networkResponse.status === 200 && request.method === 'GET') {
// Cache successful GETs in dynamic cache
if (response && response.status === 200) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
// Limit dynamic cache size
cache.put(request, response.clone());
limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
}
return networkResponse;
return response;
} catch (error) {
console.log('[SW] Network failed, trying cache:', request.url);
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
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 });
}
// Return offline response
// Fallback: tell the page to use IndexedDB
return new Response(JSON.stringify({
error: 'Offline',
message: 'Unable to fetch data. Please check your connection.'
offline: true,
endpoint: new URL(request.url).pathname,
message: 'Vous etes hors-ligne. Les donnees locales sont utilisees.'
}), {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'application/json'
})
status: 200,
headers: { 'Content-Type': 'application/json', 'X-ObsiGate-Offline': 'true' }
});
}
}
// Limit cache size
/* ── 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) {
// Delete oldest entries
const deleteCount = keys.length - maxSize;
for (let i = 0; i < deleteCount; i++) {
for (let i = 0; i < keys.length - maxSize; i++) {
await cache.delete(keys[i]);
}
}
}
// Message event - handle messages from clients
/* ── Messages ── */
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
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;
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
});
/* ── Sync ── */
self.addEventListener('sync', (event) => {
console.log('[SW] Background sync:', event.tag);
if (event.tag === 'obsigate-sync') {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((name) => caches.delete(name))
);
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ type: 'SYNC_TRIGGERED' });
});
})
);
}
});
// Sync event - background sync for offline actions
self.addEventListener('sync', (event) => {
console.log('[SW] Background sync:', event.tag);
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
async function syncData() {
// Placeholder for background sync logic
console.log('[SW] Syncing data...');
}
// Push notification event
/* ── Push ── */
self.addEventListener('push', (event) => {
const options = {
body: event.data ? event.data.text() : 'New update available',
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],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Ouvrir ObsiGate'
},
{
action: 'close',
title: 'Fermer'
}
{ action: 'open', title: 'Ouvrir ObsiGate' },
{ action: 'close', title: 'Fermer' }
]
};
event.waitUntil(
self.registration.showNotification('ObsiGate', options)
);
event.waitUntil(self.registration.showNotification('ObsiGate', options));
});
// Notification click event
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/')
);
if (event.action === 'open') {
event.waitUntil(clients.openWindow('/'));
}
});