1// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2// SERVICE WORKER â BD-SapoPro PWA v3 3// Stratégie : Cache First pour assets statiques + Network First pour navigation 4// Précache automatique de TOUS les assets au premier chargement en ligne 5// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 6 7// Version auto-incrémentée à chaque déploiement (date du build) 8// â ï¸ Changer ce nombre après chaque npm run build + git push 9const CACHE_VERSION = 'v5-20260408-1624'; 10const CACHE_APP = 'bdsapopro-app-' + CACHE_VERSION; 11const CACHE_ASSETS = 'bdsapopro-assets-' + CACHE_VERSION; 12const CACHE_FONTS = 'bdsapopro-fonts-' + CACHE_VERSION; 13 14// URLs toujours exclues du cache (fonctions serveur, APIs externes) 15const NO_CACHE_PATTERNS = [ 16 /\.netlify\/functions\//, 17 /api\.github\.com/, 18 /api\.anthropic\.com/, 19 /js\.stripe\.com/, 20 /stripe\.com\/v1/, 21 /fonts\.googleapis\.com\/css/, 22]; 23 24// URLs à précacher immédiatement 25const PRECACHE_URLS = ['/', '/index.html', '/manifest.json']; 26 27// ââ INSTALLATION ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 28self.addEventListener('install', event => { 29 event.waitUntil( 30 caches.open(CACHE_APP) 31 .then(cache => cache.addAll(PRECACHE_URLS).catch(() => {})) 32 .then(() => self.skipWaiting()) 33 ); 34}); 35 36// ââ ACTIVATION : nettoyage des anciens caches âââââââââââââââââââââââââââââ 37self.addEventListener('activate', event => { 38 const CACHES_VALIDES = [CACHE_APP, CACHE_ASSETS, CACHE_FONTS]; 39 event.waitUntil( 40 caches.keys() 41 .then(keys => Promise.all( 42 keys 43 .filter(k => !CACHES_VALIDES.includes(k)) 44 .map(k => caches.delete(k)) 45 )) 46 .then(() => self.clients.claim()) 47 ); 48}); 49 50// ââ FETCH : stratégie intelligente par type de ressource âââââââââââââââââ 51self.addEventListener('fetch', event => { 52 const { request } = event; 53 const url = new URL(request.url); 54 55 // Ignorer les requêtes non-GET et non-HTTP 56 if (request.method !== 'GET') return; 57 if (!url.protocol.startsWith('http')) return; 58 59 // Ignorer les fonctions Netlify et APIs externes â toujours réseau 60 if (NO_CACHE_PATTERNS.some(p => p.test(request.url))) return; 61 62 // ââ Polices Google Fonts : Cache First (ne changent jamais) âââââââââââ 63 if (url.origin === 'https://fonts.gstatic.com' || url.pathname.match(/\.(woff2?|ttf|eot)$/)) { 64 event.respondWith(cacheFirst(request, CACHE_FONTS)); 65 return; 66 } 67 68 // ââ Assets Vite (JS, CSS avec hash) : Network First avec fallback cache â 69 // IMPORTANT : Cache First causait des "Failed to fetch" après chaque déploiement 70 // car les anciens chunks n'existent plus sur le réseau (nouveaux hashes Vite). 71 // Network First : on essaie toujours le réseau d'abord, cache en fallback offline. 72 if (url.pathname.startsWith('/assets/') || url.pathname.match(/\.(js|css)$/)) { 73 event.respondWith(networkFirstAssets(request)); 74 return; 75 } 76 77 // ââ Images, icônes, manifeste : Cache First ââââââââââââââââââââââââââââ 78 if (url.pathname.match(/\.(png|jpg|jpeg|svg|gif|webp|ico|json)$/)) { 79 event.respondWith(cacheFirst(request, CACHE_APP)); 80 return; 81 } 82 83 // ââ Navigation (pages HTML/SPA) : Network First â Cache â Offline page â 84 if (request.mode === 'navigate') { 85 event.respondWith( 86 fetch(request) 87 .then(response => { 88 // Sauvegarder la réponse fraîche + précacher tous les assets de la page 89 const clone = response.clone(); 90 caches.open(CACHE_APP).then(cache => cache.put(request, clone)); 91 // Précacher dynamiquement tous les assets visibles dans la page 92 precachePageAssets(); 93 return response; 94 }) 95 .catch(() => 96 caches.match(request) 97 .then(cached => cached || caches.match('/')) 98 .then(cached => cached || offlineResponse()) 99 ) 100 ); 101 return; 102 } 103 104 // ââ Tout le reste : Network First avec fallback cache ââââââââââââââââââ 105 event.respondWith( 106 fetch(request) 107 .then(response => { 108 if (response.ok) { 109 const clone = response.clone(); 110 caches.open(CACHE_APP).then(cache => cache.put(request, clone)); 111 } 112 return response; 113 }) 114 .catch(() => caches.match(request)) 115 ); 116}); 117
118// ââ Stratégie Network First pour assets JS/CSS ââââââââââââââââââââââââââââ 119// Priorité réseau â met à jour le cache â fallback cache si hors ligne 120async function networkFirstAssets(request) { 121 const cache = await caches.open(CACHE_ASSETS); 122 try { 123 const response = await fetch(request); 124 if (response.ok) { 125 cache.put(request, response.clone()); 126 } 127 return response; 128 } catch { 129 // Hors ligne : utiliser le cache si disponible 130 const cached = await cache.match(request); 131 if (cached) return cached; 132 return new Response('', { status: 503 }); 133 } 134} 135 136// ââ Stratégie Cache First ââââââââââââââââââââââââââââââââââââââââââââââââââ 137async function cacheFirst(request, cacheName) { 138 const cached = await caches.match(request); 139 if (cached) return cached; 140 try { 141 const response = await fetch(request); 142 if (response.ok) { 143 const cache = await caches.open(cacheName); 144 cache.put(request, response.clone()); 145 } 146 return response; 147 } catch { 148 return new Response('', { status: 503 }); 149 } 150} 151 152// ââ Précache dynamique de tous les assets du bundle Vite ââââââââââââââââââ 153// Appelé après chaque navigation réussie en ligne 154// Récupère la liste de tous les assets depuis /asset-manifest.json si disponible 155// Sinon, scanne le cache existant pour les URLs /assets/ 156async function precachePageAssets() { 157 try { 158 // Récupérer la liste des clients (pages ouvertes) 159 const clients = await self.clients.matchAll({ type: 'window' }); 160 if (clients.length === 0) return; 161 162 // Essayer de récupérer le manifeste des assets Vite 163 const manifestRes = await fetch('/manifest.json').catch(() => null); 164 if (!manifestRes) return; 165 166 // Scanner le DOM de la page pour trouver tous les scripts et styles 167 // (on envoie un message à la page pour qu'elle renvoie ses assets) 168 clients.forEach(client => { 169 client.postMessage({ type: 'GET_ASSETS' }); 170 }); 171 } catch {} 172} 173 174// ââ Réception des assets depuis la page âââââââââââââââââââââââââââââââââââ 175self.addEventListener('message', async event => { 176 if (event.data?.type === 'SKIP_WAITING') { 177 self.skipWaiting(); 178 return; 179 } 180 181 // La page renvoie la liste de ses assets à précacher 182 if (event.data?.type === 'CACHE_ASSETS' && Array.isArray(event.data.urls)) { 183 const cache = await caches.open(CACHE_ASSETS); 184 const urls = event.data.urls.filter(u => 185 u.includes('/assets/') && (u.endsWith('.js') || u.endsWith('.css')) 186 ); 187 for (const url of urls) { 188 const cached = await cache.match(url); 189 if (!cached) { 190 fetch(url).then(r => { if (r.ok) cache.put(url, r); }).catch(() => {}); 191 } 192 } 193 } 194}); 195 196// ââ Page hors-ligne ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 197function offlineResponse() { 198 return new Response( 199 `<!DOCTYPE html> 200<html lang="fr"> 201<head> 202 <meta charset="UTF-8"> 203 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 204 <title>BD-SapoPro â Hors ligne</title> 205 <style> 206 *{box-sizing:border-box;margin:0;padding:0} 207 body{font-family:system-ui,sans-serif;background:linear-gradient(135deg,#f0f9ff,#e0f2fe); 208 min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem} 209 .card{background:white;border-radius:1.5rem;padding:3rem 2rem;text-align:center; 210 max-width:380px;box-shadow:0 20px 60px rgba(0,0,0,.1)} 211 .icon{width:80px;height:80px;background:linear-gradient(135deg,#1d4ed8,#0ea5e9); 212 border-radius:1.2rem;display:flex;align-items:center;justify-content:center; 213 margin:0 auto 1.5rem;font-size:2.5rem} 214 h1{font-size:1.5rem;color:#1e293b;margin-bottom:.75rem} 215 p{color:#64748b;font-size:.95rem;line-height:1.6;margin-bottom:.5rem} 216 .tip{background:#f0f9ff;border:1px solid #bae6fd;border-radius:.75rem; 217 padding:1rem;margin-top:1.5rem;color:#0369a1;font-size:.85rem} 218 button{margin-top:1.5rem;background:#0ea5e9;color:white;border:none; 219 border-radius:.75rem;padding:.75rem 2rem;font-size:1rem; 220 font-weight:600;cursor:pointer;width:100%} 221 button:hover{background:#0284c7} 222 </style> 223</head> 224<body> 225 <div class="card"> 226 <div class="icon">ð§¼</div> 227 <h1>BD-SapoPro</h1> 228 <p>Vous êtes <strong>hors connexion</strong>.</p> 229 <p>L'app se chargera depuis votre cache dès que possible.</p> 230 <div class="tip"> 231 ð¡ Vos données (recettes, lots, stocksâ¦) sont stockées localement 232 et restent accessibles sans connexion après la première visite en ligne. 233 </div> 234 <button onclick="window.location.reload()">ð Réessayer</button> 235 </div> 236</body> 237</html>`, 238 { headers: { 'Content-Type': 'text/html; charset=utf-8' } } 239 ); 240}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.