PageSourceSearch

https://bdsapopro.netlify.app/sw.js

js bdsapopro.netlify.app collected 2026-10-03 10:30:24 UTC 10,769 bytes, 240 lines download raw bytes

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.