diff --git a/public/sw.js b/public/sw.js index ad8a6bd..80738d8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,8 +1,8 @@ -const CACHE_NAME = 'whisky-vault-v8-bunker'; // Optimierter Bunker v8 +const CACHE_NAME = 'whisky-vault-v10-bunker'; // Professional Bunker v10 -// CONFIG: Kern-Seiten und Assets für den Bunker (sofortiges Pre-Caching) +// CONFIG: Core Pages & Static Assets const CORE_PAGES = [ - '/', // Dashboard / App Shell (Enthält Scan & Tasting) + '/', ]; const STATIC_ASSETS = [ @@ -12,12 +12,25 @@ const STATIC_ASSETS = [ '/favicon.ico', ]; -// Helper: Fetch mit Timeout und Sauberen Abort -async function fetchWithTimeout(request, timeoutMs = 3000) { +// Helper: Broadcast to all clients (including those not yet controlled) +async function broadcast(message) { + try { + const clients = await self.clients.matchAll({ includeUncontrolled: true, type: 'window' }); + clients.forEach(client => client.postMessage(message)); + } catch (e) { + // Silently fail if no clients found + } +} + +// Helper: Fetch with Timeout & AbortController +async function fetchWithTimeout(url, timeoutMs = 15000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { - const response = await fetch(request, { signal: controller.signal }); + const response = await fetch(url, { + signal: controller.signal, + cache: 'no-store' // Ensure we get fresh bits for the bunker + }); clearTimeout(id); return response; } catch (e) { @@ -26,27 +39,25 @@ async function fetchWithTimeout(request, timeoutMs = 3000) { } } -// 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 +// 🏗️ INSTALL: Build the Bunker self.addEventListener('install', (event) => { + // Immediate takeover 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; + console.log('🏗️ PWA: Building Bunker v10...'); + const items = [...CORE_PAGES, ...STATIC_ASSETS]; + const total = items.length; let loaded = 0; - const promises = [...CORE_PAGES, ...STATIC_ASSETS].map(async (url) => { + // Sequential loading to avoid concurrent fetch limits on mobile + for (const url of items) { 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); + const res = await fetchWithTimeout(url); + if (res && res.ok) { + await cache.put(url, res); + } } catch (error) { console.error(`⚠️ PWA: Pre-cache failed for ${url}:`, error); } finally { @@ -56,18 +67,15 @@ self.addEventListener('install', (event) => { 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 +// 🧹 ACTIVATE: Cleanup old bunkers self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { @@ -84,7 +92,7 @@ self.addEventListener('activate', (event) => { self.clients.claim(); }); -// Communication: Listen for status checks +// 💬 MESSAGE: Handle status checks self.addEventListener('message', (event) => { if (event.data?.type === 'CHECK_BUNKER_STATUS') { event.source.postMessage({ @@ -95,36 +103,34 @@ self.addEventListener('message', (event) => { } }); +// 🚀 FETCH: Bunker Strategy 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) + // 0. BYPASS for Auth/API/Supabase if (url.pathname.includes('/auth/') || url.pathname.includes('/api/') || url.hostname.includes('supabase.co')) { return; } - // 1. NEXT.JS DATA (RSC): Bunker & Fallback + // 1. RSC DATA (_next/data): Bunker with JSON 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 fetchWithTimeout(event.request, 3000) + .catch(() => new Response(JSON.stringify({}), { + headers: { 'Content-Type': 'application/json' } + })); }) ); return; } - // 2. ASSETS & NAVIGATION: Stale-While-Revalidate (Der "Echte" Bunker Mode) + // 2. NAVIGATION & ASSETS: Bunker + SWR const isNavigation = event.request.mode === 'navigate'; const isAsset = event.request.destination === 'style' || event.request.destination === 'script' || @@ -136,8 +142,8 @@ self.addEventListener('fetch', (event) => { if (isNavigation || isAsset) { event.respondWith( caches.match(event.request).then(async (cachedResponse) => { - // Hintergrund-Update - const fetchPromise = fetchWithTimeout(event.request, 5000) + // Background update + const fetchPromise = fetchWithTimeout(event.request, 8000) .then(async (networkResponse) => { if (networkResponse && networkResponse.status === 200) { const cache = await caches.open(CACHE_NAME); @@ -145,7 +151,7 @@ self.addEventListener('fetch', (event) => { } return networkResponse; }) - .catch(() => { /* Silent in background */ }); + .catch(() => { /* Background fail silent */ }); if (isNavigation) { if (cachedResponse) return cachedResponse; diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx index 1101587..480227f 100644 --- a/src/components/OfflineIndicator.tsx +++ b/src/components/OfflineIndicator.tsx @@ -10,7 +10,6 @@ export default function OfflineIndicator() { useEffect(() => { setIsOffline(!navigator.onLine); - // Check if bunker was already ready from previous session const savedReady = localStorage.getItem('whisky_bunker_ready') === 'true'; setIsBunkerReady(savedReady); @@ -22,7 +21,7 @@ export default function OfflineIndicator() { setProgress(event.data.progress); } if (event.data?.type === 'PRECACHE_COMPLETE' || event.data?.type === 'BUNKER_STATUS') { - console.log('🛡️ PWA: Bunker is ready for offline use!'); + console.log('🛡️ PWA: Bunker is ready!'); setIsBunkerReady(true); localStorage.setItem('whisky_bunker_ready', 'true'); } @@ -34,27 +33,10 @@ export default function OfflineIndicator() { if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', handleMessage); - // Proactive check - navigator.serviceWorker.ready.then((registration) => { - if (registration.active) { - registration.active.postMessage({ type: 'CHECK_BUNKER_STATUS' }); - } - }); - - // Fallback: If after 20s we still think we are loading but SW is active, assume ready - const timer = setTimeout(() => { - const isSwActive = !!navigator.serviceWorker.controller; - if (!isBunkerReady && isSwActive) { - setIsBunkerReady(true); - localStorage.setItem('whisky_bunker_ready', 'true'); - } - }, 20000); - return () => { - clearTimeout(timer); - window.removeEventListener('online', handleOnline); - window.removeEventListener('offline', handleOffline); - navigator.serviceWorker.removeEventListener('message', handleMessage); - }; + // Initial check: if already active, ask for status + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ type: 'CHECK_BUNKER_STATUS' }); + } } return () => { @@ -64,41 +46,62 @@ export default function OfflineIndicator() { navigator.serviceWorker.removeEventListener('message', handleMessage); } }; - }, [isBunkerReady]); + }, []); + // 1. OFFLINE BAR (TOP) if (isOffline) { return ( -
- +
+ Offline-Modus: Bunker aktiv 🛡️
); } + // 2. READY STATUS (BOTTOM RIGHT) if (isBunkerReady) { return ( -
-
- - - Bunker Aktiv - -
- Die App ist vollständig im "Bunker" gespeichert und funktioniert auch ohne Internet. +
+
+
+ +
+
+
+ Bunker Aktiv + Vollständig Offline fähig +
+ + {/* Tooltip */} +
+

+ Status: Gesichert + Alle wichtigen Bestandteile der App sind lokal gespeichert. Du kannst die App jederzeit im Funkloch nutzen. +

); } - // Show nothing if online and not ready yet (or show a loading state?) + // 3. LOADING STATUS (BOTTOM RIGHT) return ( -
-
-
- - Bunker wird geladen... {progress > 0 ? `${progress}%` : ''} - +
+
+
+
+
+
+
+
+ Bunker lädt... + + {progress > 0 ? `${progress}%` : 'Initialisiere'} + +
);