/** * SERVICE WORKER - INTER SANTÉ PORTAL RH * Version: 2.1 (2025.12.21) * * Fonctionnalités : * - Cache stratégique des ressources statiques * - Mode hors ligne avec fallback * - Mise à jour automatique * - Gestion intelligente des routes * - Performance optimisée */ // ============================================ // CONFIGURATION // ============================================ // Nom du cache (changer la version pour forcer une mise à jour) const CACHE_NAME = 'inter-sante-portal-v2.1'; // URL de la page hors ligne const OFFLINE_URL = '/offline.html'; // Ressources critiques à pré-cacher (doivent être accessibles) const PRECACHE_URLS = [ '/', '/Bootstrap_new/css/style_office.css', '/Bootstrap_new/css/ux_enhancements.css', '/Bootstrap_new/css/override.css', '/Bootstrap_new/js/ux-manager.js', '/Js/fonctions.js', '/manifest.json', '/Bootstrap_new/images/new/favicon.png', // Ajouter d'autres ressources critiques ici ]; // Chemins à IGNORER complètement (le SW ne les interceptera pas) const IGNORE_PATHS = [ // Routes d'authentification '/Connexion/deconnecter', '/Connexion/quitter', '/Connexion/connecter', '/Connexion/', '/', '/logout', // API et endpoints dynamiques '/api/', '/ajax/', '/webservice/', // Admin et maintenance '/admin/', '/maintenance/', // Uploads et fichiers dynamiques '/upload/', '/uploads/', '/temp/', // Fichiers de données '.php', // Tous les fichiers PHP (sauf ceux explicitement cachés) '.pdf', '.xlsx', '.docx' ]; // Chemins qui DOIVENT être mis en cache (exceptions aux règles d'ignorance) const CACHE_WHITELIST = [ '/offline.html', '/Bootstrap_new/', '/Js/', '/css/', '/images/' ]; // ============================================ // ÉVÉNEMENT D'INSTALLATION // ============================================ /** * Événement : Installation du Service Worker * Se produit lors de la première installation ou mise à jour */ self.addEventListener('install', event => { console.log('[Service Worker] Début de l\'installation - Version:', CACHE_NAME); // Attendre que le pré-cache soit terminé avant de considérer l'installation comme complète event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('[Service Worker] Ouverture du cache:', CACHE_NAME); // Tenter de mettre en cache chaque ressource const cachePromises = PRECACHE_URLS.map(url => { return fetch(url, { mode: 'no-cors', // Permet de récupérer même les ressources cross-origin credentials: 'same-origin' }) .then(response => { // Vérifier si la ressource est accessible if (response && (response.ok || response.type === 'opaque')) { console.log('[Service Worker] Pré-cache réussi:', url); return cache.put(url, response); } else { console.warn('[Service Worker] Échec pré-cache:', url, response.status); return Promise.resolve(); // Continuer même en cas d'échec } }) .catch(error => { console.warn('[Service Worker] Erreur pré-cache', url, error); return Promise.resolve(); // Ne pas bloquer l'installation }); }); return Promise.all(cachePromises); }) .then(() => { console.log('[Service Worker] Pré-cache terminé'); // Forcer l'activation immédiate du nouveau Service Worker return self.skipWaiting(); }) .catch(error => { console.error('[Service Worker] Erreur lors de l\'installation:', error); // Même en cas d'erreur, on continue pour ne pas bloquer l'app }) ); }); // ============================================ // ÉVÉNEMENT D'ACTIVATION // ============================================ /** * Événement : Activation du Service Worker * Se produit après l'installation, nettoie les anciens caches */ self.addEventListener('activate', event => { console.log('[Service Worker] Activation - Version:', CACHE_NAME); event.waitUntil( // Nettoyer les anciennes versions du cache caches.keys() .then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { // Supprimer tous les caches qui ne correspondent pas à la version actuelle if (cacheName !== CACHE_NAME) { console.log('[Service Worker] Suppression ancien cache:', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('[Service Worker] Nettoyage des anciens caches terminé'); // Prendre le contrôle immédiat de tous les clients return self.clients.claim(); }) .then(() => { console.log('[Service Worker] Activation terminée, contrôle pris'); // Envoyer un message à tous les clients pour les informer de l'activation self.clients.matchAll().then(clients => { clients.forEach(client => { client.postMessage({ type: 'SW_ACTIVATED', version: CACHE_NAME, timestamp: new Date().toISOString() }); }); }); }) ); }); // ============================================ // ÉVÉNEMENT DE RÉCUPÉRATION (FETCH) // ============================================ /** * Événement : Interception des requêtes réseau * Cœur du Service Worker - gère le cache et les stratégies de récupération */ self.addEventListener('fetch', event => { const request = event.request; const url = new URL(request.url); // ============================================ // FILTRES : Routes à ignorer complètement // ============================================ // 1. Ignorer les requêtes non-GET (POST, PUT, DELETE, etc.) if (request.method !== 'GET') { console.debug('[SW] Ignorer - Méthode non-GET:', request.method, url.pathname); return; } // 2. Ignorer les requêtes cross-origin (sauf CDN) if (url.origin !== self.location.origin) { // Pour les CDN, on laisse passer sans interception const isCDN = url.hostname.includes('cdn.jsdelivr.net') || url.hostname.includes('cdnjs.cloudflare.com') || url.hostname.includes('code.jquery.com'); if (!isCDN) { console.debug('[SW] Ignorer - Cross-origin:', url.origin); return; } } // 3. Ignorer les chemins configurés dans IGNORE_PATHS const shouldIgnore = IGNORE_PATHS.some(path => { // Vérifier les chemins exacts if (url.pathname === path) return true; // Vérifier les chemins qui commencent par... if (path.endsWith('/') && url.pathname.startsWith(path)) return true; // Vérifier les extensions de fichiers if (path.startsWith('.') && url.pathname.endsWith(path)) return true; return false; }); if (shouldIgnore) { console.debug('[SW] Ignorer - Route configurée:', url.pathname); return; } // 4. Vérifier la whitelist (exceptions) const isWhitelisted = CACHE_WHITELIST.some(path => url.pathname.includes(path)); // ============================================ // STRATÉGIES DE RÉCUPÉRATION // ============================================ // Pour les pages HTML : Network First, fallback Cache if (request.headers.get('accept') && request.headers.get('accept').includes('text/html')) { console.debug('[SW] Stratégie HTML pour:', url.pathname); event.respondWith(handleHtmlRequest(event)); return; } // Pour les ressources statiques (CSS, JS, images) : Cache First, fallback Network if (isStaticResource(request)) { console.debug('[SW] Stratégie Cache First pour:', url.pathname); event.respondWith(handleStaticRequest(event)); return; } // Par défaut : Network First console.debug('[SW] Stratégie par défaut pour:', url.pathname); event.respondWith(handleDefaultRequest(event)); }); // ============================================ // FONCTIONS AUXILIAIRES // ============================================ /** * Détermine si une ressource est statique (CSS, JS, images, fonts) * @param {Request} request - La requête à analyser * @returns {boolean} - True si c'est une ressource statique */ function isStaticResource(request) { const url = new URL(request.url); const staticExtensions = ['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.ico']; return staticExtensions.some(ext => url.pathname.endsWith(ext)); } /** * Gère les requêtes HTML (pages) * Stratégie : Network First, fallback Cache */ function handleHtmlRequest(event) { return fetch(event.request) .then(response => { // Vérifier si la réponse est valide if (!response || response.status !== 200 || response.type === 'error') { throw new Error('Network response was not ok'); } // Cloner la réponse pour la mettre en cache const responseToCache = response.clone(); // Mettre en cache en arrière-plan caches.open(CACHE_NAME) .then(cache => { cache.put(event.request, responseToCache); console.debug('[SW] HTML mis en cache:', event.request.url); }) .catch(err => { console.warn('[SW] Erreur mise en cache HTML:', err); }); return response; }) .catch(() => { // Fallback : chercher dans le cache console.debug('[SW] Mode hors ligne, fallback cache pour:', event.request.url); return caches.match(event.request) .then(cachedResponse => { if (cachedResponse) { return cachedResponse; } // Si pas dans le cache, retourner la page hors ligne return caches.match(OFFLINE_URL) .then(offlineResponse => { return offlineResponse || new Response( '

Mode hors ligne

Cette ressource n\'est pas disponible hors ligne.

', { headers: { 'Content-Type': 'text/html' } } ); }); }); }); } /** * Gère les requêtes de ressources statiques * Stratégie : Cache First, fallback Network */ function handleStaticRequest(event) { return caches.match(event.request) .then(cachedResponse => { // Si trouvé dans le cache, le retourner immédiatement if (cachedResponse) { console.debug('[SW] Ressource statique depuis cache:', event.request.url); // En arrière-plan, vérifier si une mise à jour est disponible fetchAndUpdateCache(event.request); return cachedResponse; } // Sinon, aller au réseau console.debug('[SW] Ressource statique depuis réseau:', event.request.url); return fetchAndCache(event.request); }) .catch(error => { console.error('[SW] Erreur gestion ressource statique:', error); return createFallbackResponse(event.request); }); } /** * Gère les requêtes par défaut * Stratégie : Stale-While-Revalidate */ function handleDefaultRequest(event) { return caches.match(event.request) .then(cachedResponse => { // Toujours essayer de récupérer depuis le réseau const fetchPromise = fetch(event.request) .then(networkResponse => { // Mettre à jour le cache avec la nouvelle version if (networkResponse && networkResponse.ok) { const responseToCache = networkResponse.clone(); caches.open(CACHE_NAME) .then(cache => { cache.put(event.request, responseToCache); }); } return networkResponse; }) .catch(() => { // En cas d'erreur réseau, on ignore silencieusement console.debug('[SW] Erreur réseau pour:', event.request.url); }); // Retourner la réponse en cache si disponible, sinon attendre le réseau return cachedResponse || fetchPromise; }); } /** * Récupère une ressource et la met en cache */ function fetchAndCache(request) { return fetch(request) .then(response => { // Vérifier si la réponse est valide if (!response || !response.ok) { return response; // Retourner la réponse même si elle a échoué } // Mettre en cache const responseToCache = response.clone(); caches.open(CACHE_NAME) .then(cache => { cache.put(request, responseToCache); }); return response; }) .catch(error => { console.error('[SW] Erreur fetchAndCache:', error); return createFallbackResponse(request); }); } /** * Vérifie et met à jour le cache en arrière-plan */ function fetchAndUpdateCache(request) { fetch(request) .then(response => { if (response && response.ok) { const responseToCache = response.clone(); caches.open(CACHE_NAME) .then(cache => { cache.put(request, responseToCache); console.debug('[SW] Cache mis à jour en arrière-plan:', request.url); }); } }) .catch(() => { // Ignorer silencieusement les erreurs de mise à jour en arrière-plan }); } /** * Crée une réponse de secours selon le type de ressource */ function createFallbackResponse(request) { const url = request.url; if (url.includes('.css')) { return new Response( '/* Ressource CSS temporairement indisponible */\nbody { background-color: #f3f2f1; }', { headers: { 'Content-Type': 'text/css' } } ); } if (url.includes('.js')) { return new Response( '// Ressource JavaScript temporairement indisponible\nconsole.log("Ressource en cache indisponible");', { headers: { 'Content-Type': 'application/javascript' } } ); } if (url.includes('.png') || url.includes('.jpg') || url.includes('.svg')) { // Retourner une image placeholder return fetch('/Bootstrap_new/images/new/favicon.png') .catch(() => { return new Response('', { status: 404 }); }); } return new Response( 'Ressource non disponible hors ligne', { status: 503, headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' } } ); } // ============================================ // ÉVÉNEMENTS DE MESSAGERIE // ============================================ /** * Gère les messages entre l'application et le Service Worker */ self.addEventListener('message', event => { console.log('[SW] Message reçu:', event.data); switch (event.data.type) { case 'SKIP_WAITING': // Forcer l'activation immédiate (utilisé pour les mises à jour) self.skipWaiting(); console.log('[SW] Skip waiting activé'); break; case 'GET_CACHE_STATUS': // Retourner l'état du cache event.ports[0].postMessage({ cacheName: CACHE_NAME, cacheSize: 'N/A', status: 'active', version: '2.1' }); break; case 'CLEAR_CACHE': // Nettoyer le cache spécifique caches.delete(CACHE_NAME) .then(() => { event.ports[0].postMessage({ success: true }); }) .catch(error => { event.ports[0].postMessage({ success: false, error: error.message }); }); break; case 'UPDATE_RESOURCES': // Mettre à jour des ressources spécifiques updateSpecificResources(event.data.urls); break; } }); /** * Met à jour des ressources spécifiques dans le cache */ function updateSpecificResources(urls) { caches.open(CACHE_NAME) .then(cache => { const updatePromises = urls.map(url => { return fetch(url) .then(response => { if (response.ok) { return cache.put(url, response); } }) .catch(error => { console.warn('[SW] Échec mise à jour:', url, error); }); }); return Promise.all(updatePromises); }) .then(() => { console.log('[SW] Mise à jour des ressources terminée'); }); } // ============================================ // ÉVÉNEMENTS PUSH (NOTIFICATIONS) // ============================================ /** * Gère les notifications push (optionnel) */ self.addEventListener('push', event => { if (!event.data) return; try { const data = event.data.json(); const options = { body: data.body || 'Nouvelle notification', icon: '/Bootstrap_new/images/new/favicon.png', badge: '/Bootstrap_new/images/new/favicon.png', vibrate: [200, 100, 200], data: { url: data.url || '/', timestamp: new Date().toISOString() }, actions: [ { action: 'open', title: 'Ouvrir' }, { action: 'close', title: 'Fermer' } ] }; event.waitUntil( self.registration.showNotification( data.title || 'Inter Santé - Notification', options ) ); } catch (error) { console.error('[SW] Erreur notification push:', error); } }); /** * Gère les clics sur les notifications */ self.addEventListener('notificationclick', event => { event.notification.close(); const urlToOpen = event.notification.data.url || '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(windowClients => { // Vérifier si une fenêtre est déjà ouverte for (let client of windowClients) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // Sinon ouvrir une nouvelle fenêtre if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) ); }); // ============================================ // ÉVÉNEMENT SYNC (SYNCHRONISATION) // ============================================ /** * Gère la synchronisation en arrière-plan (optionnel) */ self.addEventListener('sync', event => { console.log('[SW] Synchronisation:', event.tag); if (event.tag === 'sync-data') { event.waitUntil(syncPendingData()); } }); /** * Synchronise les données en attente */ function syncPendingData() { // À implémenter selon vos besoins console.log('[SW] Synchronisation des données en cours...'); return Promise.resolve(); } // ============================================ // LOGGING ET DEBUG // ============================================ // Niveau de log (0: none, 1: error, 2: warn, 3: info, 4: debug) const LOG_LEVEL = 3; function log(level, message, ...args) { if (level <= LOG_LEVEL) { const levels = ['', 'ERROR', 'WARN', 'INFO', 'DEBUG']; console.log(`[SW ${levels[level]}] ${message}`, ...args); } } // Initialisation console.log('[Service Worker] Initialisé - Version:', CACHE_NAME); console.log('[Service Worker] Scope:', self.registration ? self.registration.scope : self.location.origin);