646 lines
21 KiB
JavaScript
646 lines
21 KiB
JavaScript
/**
|
|
* 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(
|
|
'<h1>Mode hors ligne</h1><p>Cette ressource n\'est pas disponible hors ligne.</p>',
|
|
{ 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); |