const CACHE_NAME = 'whisky-vault-v8-bunker'; // Optimierter Bunker v8 // CONFIG: Kern-Seiten und Assets für den Bunker (sofortiges Pre-Caching) const CORE_PAGES = [ '/', // Dashboard / App Shell (Enthält Scan & Tasting) ]; const STATIC_ASSETS = [ '/manifest.webmanifest', '/icon-192.png', '/icon-512.png', '/favicon.ico', ]; // Helper: Fetch mit Timeout und Sauberen Abort async function fetchWithTimeout(request, timeoutMs = 3000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(request, { signal: controller.signal }); clearTimeout(id); return response; } catch (e) { clearTimeout(id); throw e; } } // Helper: Broadcast an alle Clients async function broadcast(message) { const clients = await self.clients.matchAll(); clients.forEach(client => client.postMessage(message)); } // Install: Lade alles Wichtige einzeln in den Bunker self.addEventListener('install', (event) => { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME).then(async (cache) => { console.log('🏗️ PWA: Building bunker v8...'); const total = CORE_PAGES.length + STATIC_ASSETS.length; let loaded = 0; const promises = [...CORE_PAGES, ...STATIC_ASSETS].map(async (url) => { try { // Im Dev-Mode kann / lange dauern (Kompilierung). 10s Timeout. const res = await fetchWithTimeout(url, 10000); if (!res.ok) throw new Error(`Status ${res.status}`); await cache.put(url, res); } catch (error) { console.error(`⚠️ PWA: Pre-cache failed for ${url}:`, error); } finally { loaded++; broadcast({ type: 'PRECACHE_PROGRESS', progress: Math.round((loaded / total) * 100) }); } }); await Promise.all(promises); console.log('✅ PWA: Bunker build finished'); // Signal an Clients: Bunker ist bereit broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME }); }) ); }); // Activate: Alte Bunker räumen self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log('🧹 PWA: Clearing old bunker', cacheName); return caches.delete(cacheName); } }) ); }) ); self.clients.claim(); }); // Communication: Listen for status checks self.addEventListener('message', (event) => { if (event.data?.type === 'CHECK_BUNKER_STATUS') { event.source.postMessage({ type: 'BUNKER_STATUS', isReady: true, version: CACHE_NAME }); } }); self.addEventListener('fetch', (event) => { // Nur GET-Requests cachen if (event.request.method !== 'GET') return; const url = new URL(event.request.url); // 0. BYPASS: Auth & API (wird von App/Dexie direkt gehandelt) if (url.pathname.includes('/auth/') || url.pathname.includes('/api/') || url.hostname.includes('supabase.co')) { return; } // 1. NEXT.JS DATA (RSC): Bunker & Fallback if (url.pathname.startsWith('/_next/data/')) { event.respondWith( caches.match(event.request).then((cached) => { if (cached) return cached; return fetchWithTimeout(event.request, 2000) .catch(() => { return new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } }); }); }) ); return; } // 2. ASSETS & NAVIGATION: Stale-While-Revalidate (Der "Echte" Bunker Mode) const isNavigation = event.request.mode === 'navigate'; const isAsset = event.request.destination === 'style' || event.request.destination === 'script' || event.request.destination === 'worker' || event.request.destination === 'font' || event.request.destination === 'image' || url.pathname.startsWith('/_next/static'); if (isNavigation || isAsset) { event.respondWith( caches.match(event.request).then(async (cachedResponse) => { // Hintergrund-Update const fetchPromise = fetchWithTimeout(event.request, 5000) .then(async (networkResponse) => { if (networkResponse && networkResponse.status === 200) { const cache = await caches.open(CACHE_NAME); cache.put(event.request, networkResponse.clone()); } return networkResponse; }) .catch(() => { /* Silent in background */ }); if (isNavigation) { if (cachedResponse) return cachedResponse; const shell = await caches.match('/'); if (shell) return shell; } return cachedResponse || fetchPromise || fetch(event.request); }) ); } });