From b35b0f90170caeceb67ca9fdf02782356a3c2557 Mon Sep 17 00:00:00 2001 From: KONE SOREL Date: Mon, 22 Dec 2025 07:57:04 +0000 Subject: [PATCH] =?UTF-8?q?service-worker.js=20modifi=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Js/sw-register.js | 657 ++++++++++++++++++++++++------------- Vue/gabarit.php | 8 +- offline.html | 163 +++++----- service-worker.js | 812 +++++++++++++++++++++++++++++++++++----------- 4 files changed, 1135 insertions(+), 505 deletions(-) diff --git a/Js/sw-register.js b/Js/sw-register.js index a90b3b7..687b1d2 100644 --- a/Js/sw-register.js +++ b/Js/sw-register.js @@ -1,245 +1,452 @@ -// Enregistrement du Service Worker amélioré +// sw-register.js - Version 2.0 +// Enregistrement et gestion du Service Worker + (function() { - // Vérifier si le Service Worker est supporté - if (!('serviceWorker' in navigator)) { - console.warn('[App] Service Worker non supporté'); - return; - } - - // Vérifier si nous sommes en HTTPS ou localhost - if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') { - console.warn('[App] Service Worker nécessite HTTPS en production'); - return; - } - - // Fonction principale d'enregistrement - function registerServiceWorker() { - const swUrl = '/service-worker.js?v=1.1'; + 'use strict'; - navigator.serviceWorker.register(swUrl) - .then(function(registration) { - console.log('[Service Worker] Enregistré avec succès:', registration.scope); + // ============================================ + // CONFIGURATION + // ============================================ + + const SW_URL = '/service-worker.js?v=2.1'; + const CHECK_INTERVAL = 60 * 60 * 1000; // 1 heure + const DEBUG = true; + + // ============================================ + // VÉRIFICATIONS PRÉALABLES + // ============================================ + + // 1. Vérifier si le Service Worker est supporté + if (!('serviceWorker' in navigator)) { + log('Service Worker non supporté par ce navigateur'); + return; + } + + // 2. Vérifier si on est en HTTPS ou localhost + if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') { + log('Service Worker nécessite HTTPS en production'); + return; + } + + // ============================================ + // FONCTION PRINCIPALE D'ENREGISTREMENT + // ============================================ + + function registerServiceWorker() { + log('Tentative d\'enregistrement du Service Worker...'); - // Vérifier si une mise à jour est disponible + navigator.serviceWorker.register(SW_URL) + .then(handleRegistrationSuccess) + .catch(handleRegistrationError); + } + + // ============================================ + // GESTION DE LA RÉUSSITE + // ============================================ + + function handleRegistrationSuccess(registration) { + log('Service Worker enregistré avec succès:', registration.scope); + + // Configurer les écouteurs d'événements + setupEventListeners(registration); + + // Configurer la vérification périodique des mises à jour + setupUpdateChecking(registration); + + // Initialiser la gestion hors ligne + setupOfflineManagement(); + + // Exposer l'API publique + exposePublicAPI(registration); + } + + // ============================================ + // GESTION DES ERREURS + // ============================================ + + function handleRegistrationError(error) { + console.error('Erreur d\'enregistrement du Service Worker:', error); + + // Suggestions de dépannage basées sur l'erreur + if (error.name === 'SecurityError') { + console.warn('⚠️ Vérifiez que vous êtes en HTTPS'); + } else if (error.name === 'TypeError') { + console.warn('⚠️ Vérifiez le chemin du Service Worker'); + } else if (error.message.includes('MIME type')) { + console.warn('⚠️ Vérifiez l\'en-tête Content-Type du Service Worker'); + } + } + + // ============================================ + // CONFIGURATION DES ÉCOUTEURS + // ============================================ + + function setupEventListeners(registration) { + // Écouter les mises à jour du Service Worker registration.addEventListener('updatefound', function() { - const installingWorker = registration.installing; - console.log('[Service Worker] Mise à jour trouvée:', installingWorker.state); - - installingWorker.addEventListener('statechange', function() { - console.log('[Service Worker] Nouvel état:', this.state); + const newWorker = registration.installing; + log('Nouvelle version du Service Worker trouvée:', newWorker.state); - if (this.state === 'installed' && navigator.serviceWorker.controller) { - // Nouvelle version disponible - showUpdateNotification(); - } - - if (this.state === 'activated') { - console.log('[Service Worker] Nouveau Service Worker activé'); - // Notifier tous les clients - sendMessageToSW({ type: 'NEW_VERSION_ACTIVATED' }); - } - }); + newWorker.addEventListener('statechange', function() { + log('État du nouveau Service Worker:', this.state); + + if (this.state === 'installed' && navigator.serviceWorker.controller) { + showUpdateNotification(); + } + + if (this.state === 'activated') { + log('Nouveau Service Worker activé'); + notifyClientsOfUpdate(); + } + }); }); - // Vérifier l'état du Service Worker périodiquement + // Écouter les messages du Service Worker + navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage); + } + + // ============================================ + // GESTION DES MESSAGES DU SERVICE WORKER + // ============================================ + + function handleServiceWorkerMessage(event) { + log('Message reçu du Service Worker:', event.data); + + switch (event.data.type) { + case 'SW_ACTIVATED': + log('Service Worker activé, version:', event.data.version); + break; + + case 'CACHE_UPDATED': + log('Cache mis à jour:', event.data.resources); + break; + + case 'OFFLINE_MODE': + showOfflineNotification(); + break; + } + } + + // ============================================ + // VÉRIFICATION PÉRIODIQUE DES MISES À JOUR + // ============================================ + + function setupUpdateChecking(registration) { + // Vérifier les mises à jour toutes les heures setInterval(() => { - registration.update().catch(err => { - console.debug('[SW] Pas de mise à jour disponible:', err); - }); - }, 60 * 60 * 1000); // Toutes les heures + registration.update().catch(err => { + log('Pas de mise à jour disponible:', err.message); + }); + }, CHECK_INTERVAL); - return registration; - }) - .catch(function(error) { - console.error('[Service Worker] Erreur d\'enregistrement:', error); + // Vérifier aussi quand la page redevient visible + document.addEventListener('visibilitychange', function() { + if (!document.hidden) { + registration.update().catch(() => {}); + } + }); + } + + // ============================================ + // GESTION HORS LIGNE + // ============================================ + + function setupOfflineManagement() { + window.addEventListener('online', function() { + log('Connexion rétablie'); + document.documentElement.classList.remove('offline'); + showOnlineNotification(); + syncData(); + }); - // Tentative de récupération - if (error.name === 'SecurityError') { - console.warn('[SW] Erreur de sécurité - Vérifiez HTTPS'); - } else if (error.name === 'TypeError') { - console.warn('[SW] Erreur de type - Vérifiez le chemin du SW'); + window.addEventListener('offline', function() { + log('Mode hors ligne'); + document.documentElement.classList.add('offline'); + showOfflineNotification(); + }); + + // Vérifier l'état initial + if (!navigator.onLine) { + document.documentElement.classList.add('offline'); } - }); - } - - // Gestion de l'état de connexion - function setupConnectionHandlers() { - window.addEventListener('online', function() { - console.log('[App] Connexion rétablie'); - document.documentElement.classList.remove('offline'); - showOnlineNotification(); - - // Synchroniser les données - if (navigator.serviceWorker.controller) { - sendMessageToSW({ type: 'SYNC_DATA' }); - } - }); - - window.addEventListener('offline', function() { - console.log('[App] Mode hors ligne'); - document.documentElement.classList.add('offline'); - showOfflineNotification(); - }); - - // Vérifier l'état initial - if (!navigator.onLine) { - document.documentElement.classList.add('offline'); } - } - - // Notification de mise à jour - function showUpdateNotification() { - const isAnglophone = window.appConfig?.isAnglophone || false; - // Utiliser SweetAlert2 si disponible - if (typeof Swal !== 'undefined') { - Swal.fire({ - title: isAnglophone ? 'Update Available' : 'Mise à jour disponible', - text: isAnglophone - ? 'A new version is available. Reload to update?' - : 'Une nouvelle version est disponible. Recharger pour mettre à jour ?', - icon: 'info', - showCancelButton: true, - confirmButtonText: isAnglophone ? 'Reload' : 'Recharger', - cancelButtonText: isAnglophone ? 'Later' : 'Plus tard', - allowOutsideClick: false - }).then((result) => { - if (result.isConfirmed) { - window.location.reload(); + // ============================================ + // NOTIFICATIONS + // ============================================ + + function showUpdateNotification() { + const isAnglophone = window.appConfig?.isAnglophone || false; + const message = isAnglophone + ? 'A new version is available. Reload to update?' + : 'Une nouvelle version est disponible. Recharger pour mettre à jour ?'; + + // Utiliser SweetAlert2 si disponible + if (typeof Swal !== 'undefined') { + Swal.fire({ + title: isAnglophone ? 'Update Available' : 'Mise à jour disponible', + text: message, + icon: 'info', + showCancelButton: true, + confirmButtonText: isAnglophone ? 'Reload' : 'Recharger', + cancelButtonText: isAnglophone ? 'Later' : 'Plus tard', + allowOutsideClick: false + }).then((result) => { + if (result.isConfirmed) { + window.location.reload(); + } + }); + } else { + // Fallback simple + if (confirm(message)) { + window.location.reload(); + } } - }); - } else { - // Fallback simple - if (confirm(isAnglophone - ? 'New version available. Reload?' - : 'Nouvelle version disponible. Recharger ?')) { - window.location.reload(); - } } - } - - // Notification en ligne - function showOnlineNotification() { - // Créer un toast temporaire - const toast = document.createElement('div'); - toast.className = 'position-fixed top-0 end-0 p-3'; - toast.style.zIndex = '9999'; - const isAnglophone = window.appConfig?.isAnglophone || false; - - toast.innerHTML = ` - - `; - - document.body.appendChild(toast); - - // Auto-remove après 3 secondes - setTimeout(() => { - if (toast.parentNode) { - toast.remove(); - } - }, 3000); - } - - // Notification hors ligne - function showOfflineNotification() { - // Créer un toast temporaire - const toast = document.createElement('div'); - toast.className = 'position-fixed top-0 end-0 p-3'; - toast.style.zIndex = '9999'; - - const isAnglophone = window.appConfig?.isAnglophone || false; - - toast.innerHTML = ` - - `; - - document.body.appendChild(toast); - - // Auto-remove après 5 secondes - setTimeout(() => { - if (toast.parentNode) { - toast.remove(); - } - }, 5000); - } - - // Envoyer un message au Service Worker - function sendMessageToSW(message) { - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage(message); + function showOnlineNotification() { + const isAnglophone = window.appConfig?.isAnglophone || false; + + showToast( + isAnglophone ? 'Online' : 'En ligne', + isAnglophone ? 'Connection restored. Data will be synchronized.' : 'Connexion rétablie. Les données seront synchronisées.', + 'success', + 3000 + ); + + // Notifier le Service Worker + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: 'NETWORK_RESTORED', + timestamp: Date.now() + }); + } } - } - - // Obtenir l'état du cache - function getCacheStatus() { - if (navigator.serviceWorker.controller) { - const messageChannel = new MessageChannel(); - - messageChannel.port1.onmessage = function(event) { - console.log('[SW] Cache status:', event.data); - }; - - navigator.serviceWorker.controller.postMessage( - { type: 'GET_CACHE_STATUS' }, - [messageChannel.port2] - ); - } - } - - // Fonction publique pour forcer la mise à jour - function forceUpdate() { - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' }); - } - } - - // Initialiser quand la page est prête - window.addEventListener('load', function() { - registerServiceWorker(); - setupConnectionHandlers(); - // Ajouter les écouteurs de visibilité pour les rafraîchissements - document.addEventListener('visibilitychange', function() { - if (!document.hidden && navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage({ type: 'PAGE_VISIBLE' }); - } + function showOfflineNotification() { + const isAnglophone = window.appConfig?.isAnglophone || false; + + showToast( + isAnglophone ? 'Offline' : 'Hors ligne', + isAnglophone ? 'No internet connection. Working in offline mode.' : 'Pas de connexion Internet. Mode hors ligne actif.', + 'warning', + 5000 + ); + } + + function showToast(title, message, type, duration) { + const toast = document.createElement('div'); + toast.className = `sw-toast sw-toast-${type}`; + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === 'success' ? '#27ae60' : '#e74c3c'}; + color: white; + padding: 15px 20px; + border-radius: 8px; + z-index: 9999; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + max-width: 300px; + animation: slideIn 0.3s ease; + `; + + toast.innerHTML = ` +
${title}
+
${message}
+ `; + + document.body.appendChild(toast); + + setTimeout(() => { + if (toast.parentNode) { + toast.style.animation = 'slideOut 0.3s ease'; + setTimeout(() => toast.remove(), 300); + } + }, duration); + + // Ajouter les styles d'animation si nécessaire + addToastStyles(); + } + + function addToastStyles() { + if (!document.getElementById('toast-styles')) { + const styles = document.createElement('style'); + styles.id = 'toast-styles'; + styles.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + @keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } + `; + document.head.appendChild(styles); + } + } + + // ============================================ + // SYNCHRONISATION DES DONNÉES + // ============================================ + + function syncData() { + log('Synchronisation des données...'); + + // Synchroniser avec le Service Worker + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: 'SYNC_DATA', + timestamp: Date.now() + }); + } + + // Synchroniser les données en attente + syncPendingData(); + } + + function syncPendingData() { + // À implémenter selon vos besoins + // Ex: synchroniser les formulaires en attente + } + + // ============================================ + // API PUBLIQUE + // ============================================ + + function exposePublicAPI(registration) { + window.serviceWorkerManager = { + // Forcer une mise à jour + update: function() { + return registration.update(); + }, + + // Nettoyer le cache + clearCache: function() { + return caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => caches.delete(cacheName)) + ); + }); + }, + + // Obtenir l'état + getStatus: function() { + return { + controller: !!navigator.serviceWorker.controller, + scope: registration.scope, + state: registration.installing ? registration.installing.state : 'active', + supports: { + push: 'pushManager' in registration, + sync: 'sync' in registration + } + }; + }, + + // Forcer l'activation + skipWaiting: function() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' }); + } + }, + + // Recharger pour appliquer les mises à jour + reload: function() { + window.location.reload(); + }, + + // Vérifier si supporté + isSupported: 'serviceWorker' in navigator + }; + } + + // ============================================ + // NOTIFICATION AUX CLIENTS + // ============================================ + + function notifyClientsOfUpdate() { + // Notifier toutes les pages ouvertes + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: 'NEW_VERSION_ACTIVATED', + version: '2.1', + timestamp: Date.now() + }); + } + } + + // ============================================ + // FONCTIONS UTILITAIRES + // ============================================ + + function log(message, ...args) { + if (DEBUG) { + console.log('[SW Register]', message, ...args); + } + } + + // ============================================ + // INITIALISATION + // ============================================ + + // Attendre que la page soit complètement chargée + window.addEventListener('load', function() { + // Délai pour éviter la concurrence avec d'autres scripts + setTimeout(registerServiceWorker, 100); }); - }); - - // Exposer les fonctions publiques - window.serviceWorkerManager = { - forceUpdate: forceUpdate, - getCacheStatus: getCacheStatus, - isSupported: 'serviceWorker' in navigator, - sendMessage: sendMessageToSW - }; - - console.log('[App] Service Worker Manager initialisé'); + + // Nettoyer les anciens Service Workers au chargement + window.addEventListener('load', function() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + registrations.forEach(function(registration) { + // Supprimer les SW des anciens domaines + if (!registration.scope.startsWith(window.location.origin)) { + log('Nettoyage ancien SW:', registration.scope); + registration.unregister(); + } + }); + }); + } + }); + + // ============================================ + // GESTION DES ERREURS GLOBALES + // ============================================ + + window.addEventListener('error', function(event) { + if (event.message && event.message.includes('ServiceWorker')) { + console.error('Erreur Service Worker:', event.error); + } + }); + + // ============================================ + // EXPORT POUR LES TESTS + // ============================================ + + // Pour le débogage depuis la console + window.debugSW = { + unregisterAll: function() { + return navigator.serviceWorker.getRegistrations() + .then(registrations => Promise.all( + registrations.map(r => r.unregister()) + )); + }, + clearAllCaches: function() { + return caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)) + )); + }, + forceUpdate: function() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' }); + setTimeout(() => window.location.reload(), 1000); + } + } + }; + + log('Service Worker Manager initialisé'); + })(); \ No newline at end of file diff --git a/Vue/gabarit.php b/Vue/gabarit.php index 9f8fafe..55eaeb0 100755 --- a/Vue/gabarit.php +++ b/Vue/gabarit.php @@ -35,17 +35,17 @@ foreach ($menus as $key0 => $menuParent) { - <?= htmlspecialchars($_SESSION['vue'] ?? 'INTER SANTÉ') ?> | Portail Santé + <?= htmlspecialchars($_SESSION['vue'] ?? 'INTER-SANTE') ?> | Portail Santé - + - + @@ -550,6 +550,6 @@ foreach ($menus as $key0 => $menuParent) { - + \ No newline at end of file diff --git a/offline.html b/offline.html index 377ba9e..cb57d6c 100644 --- a/offline.html +++ b/offline.html @@ -1,86 +1,85 @@ - - - - Mode hors ligne - INTER-SANTÉ - - - - -
-
📶
-

Mode hors ligne

-

Vous n'êtes pas connecté à Internet. Certaines fonctionnalités peuvent être limitées.

-

Vous pouvez continuer à utiliser les fonctionnalités disponibles hors ligne.

- -
- - -
- -
-

INTER-SANTÉ Portail RH • Version hors ligne

-
+ + + + Mode hors ligne - INTER-SANTÉ + + + +
+
📶
+

Mode hors ligne

+

Vous n'êtes pas connecté à Internet. Certaines fonctionnalités peuvent être limitées.

+

Vous pouvez continuer à utiliser les fonctionnalités disponibles hors ligne.

+ +
+ +
- + +
+

INTER-SANTE Portail RH • Version hors ligne

+
+
+ \ No newline at end of file diff --git a/service-worker.js b/service-worker.js index 66df6a3..f153a5b 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,220 +1,644 @@ -// Service Worker pour Portail RH Inter Santé -const CACHE_NAME = 'inter-sante-portal-v1.1'; +/** + * 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 ESSENTIELLES à mettre en cache (vérifier l'existence) +// Ressources critiques à pré-cacher (doivent être accessibles) const PRECACHE_URLS = [ - '/', - '/Bootstrap_new/css/style_office.css', - '/Bootstrap_new/css/ux_enhancements.css', - '/Bootstrap_new/js/ux-manager.js', - '/Js/fonctions.js', - '/manifest.json', - '/Bootstrap_new/images/new/favicon.png' + '/', + '/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 ]; -// Installation - Pré-cache des ressources essentielles +// Chemins à IGNORER complètement (le SW ne les interceptera pas) +const IGNORE_PATHS = [ + // Routes d'authentification + '/Connexion/deconnecter', + '/Connexion/quitter', + '/Connexion/connecter', + '/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 => { - event.waitUntil( - caches.open(CACHE_NAME) - .then(cache => { - console.log('[Service Worker] Pré-cache des ressources'); - - // Cache only resources that exist - const cachePromises = PRECACHE_URLS.map(url => { - return fetch(url, { mode: 'no-cors' }) - .then(response => { - if (response.ok || response.type === 'opaque') { - return cache.put(url, response); - } - console.warn(`[SW] Resource not found: ${url}`); - return Promise.resolve(); + 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.warn(`[SW] Failed to cache ${url}:`, error); - return Promise.resolve(); - }); - }); - - return Promise.all(cachePromises); - }) - .then(() => { - console.log('[Service Worker] Installation terminée'); - return self.skipWaiting(); - }) - .catch(error => { - console.error('[Service Worker] Erreur installation:', error); - }) - ); -}); - -// Activation - Nettoyage des anciens caches -self.addEventListener('activate', event => { - event.waitUntil( - caches.keys().then(cacheNames => { - return Promise.all( - cacheNames.map(cacheName => { - if (cacheName !== CACHE_NAME) { - console.log('[Service Worker] Suppression ancien cache:', cacheName); - return caches.delete(cacheName); - } - }) - ); - }).then(() => { - console.log('[Service Worker] Activation terminée'); - return self.clients.claim(); - }) - ); -}); - -// Stratégie de cache: Stale-While-Revalidate -self.addEventListener('fetch', event => { - // Ignorer les requêtes non-GET - if (event.request.method !== 'GET') { - return; - } - - // Ignorer les requêtes chrome-extension - if (event.request.url.includes('chrome-extension')) { - return; - } - - // Pour les pages HTML: Network First avec fallback cache - if (event.request.headers.get('accept').includes('text/html')) { - event.respondWith( - 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 le cache - const responseToCache = response.clone(); - caches.open(CACHE_NAME) - .then(cache => { - cache.put(event.request, responseToCache); - }); - - return response; - }) - .catch(() => { - // Fallback au cache - return caches.match(event.request) - .then(cachedResponse => { - return cachedResponse || caches.match('/'); - }); - }) - ); - return; - } - - // Pour les autres ressources: Cache First, fallback Network - event.respondWith( - caches.match(event.request) - .then(cachedResponse => { - if (cachedResponse) { - // Toujours rafraîchir le cache en arrière-plan - fetch(event.request) - .then(response => { - if (response && response.ok) { - caches.open(CACHE_NAME) - .then(cache => cache.put(event.request, response)); - } + console.error('[Service Worker] Erreur lors de l\'installation:', error); + // Même en cas d'erreur, on continue pour ne pas bloquer l'app }) - .catch(() => {}); // Ignorer les erreurs de mise à jour - - return cachedResponse; - } + ); +}); + +// ============================================ +// É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'); - // Pas dans le cache, aller au réseau - return fetch(event.request) - .then(response => { - // Vérifier si nous avons reçu une réponse valide - if (!response || !response.ok) { - return response; // Retourner la réponse même si elle a échoué + 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'); } - // Mettre en cache la réponse pour la prochaine fois + // 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); - }); + .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(error => { - console.error('[SW] Fetch failed:', error); + }) + .catch(() => { + // Fallback : chercher dans le cache + console.debug('[SW] Mode hors ligne, fallback cache pour:', event.request.url); - // Pour les CSS/JS, retourner des réponses de secours - const url = event.request.url; - if (url.includes('.css')) { - return new Response('/* Ressource temporairement indisponible */', { - headers: { 'Content-Type': 'text/css' } - }); - } - if (url.includes('.js')) { - return new Response('// Ressource temporairement indisponible', { - headers: { 'Content-Type': 'application/javascript' } - }); - } - - // Pour les images, retourner une image de secours - if (url.includes('.png') || url.includes('.jpg') || url.includes('.svg')) { - return fetch('/Bootstrap_new/images/new/favicon.png') - .catch(() => { - // Si même l'icône de secours échoue, retourner une réponse vide - return new Response('', { status: 404 }); + 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; } - return new Response('Ressource non disponible hors ligne', { - headers: { 'Content-Type': 'text/plain' } - }); - }); - }) - ); -}); + // 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); + }); +} -// Gestion des messages entre l'app et le Service Worker -self.addEventListener('message', event => { - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting(); - } - - if (event.data && event.data.type === 'GET_CACHE_STATUS') { - event.ports[0].postMessage({ - cacheName: CACHE_NAME, - status: 'active' - }); - } -}); +/** + * 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; + }); +} -// Gestion des notifications push (optionnel) -self.addEventListener('push', event => { - if (!event.data) return; - - 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 || '/' +/** + * 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; } - }; - - event.waitUntil( - self.registration.showNotification(data.title || 'Inter Santé', options) - ); }); +/** + * 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(); - - event.waitUntil( - clients.openWindow(event.notification.data.url || '/') - ); -}); \ No newline at end of file + 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); \ No newline at end of file