fix: optimize bunker loading and add progress feedback

This commit is contained in:
2025-12-20 23:57:55 +01:00
parent b0a79541b6
commit 000f2582a3
2 changed files with 54 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
const CACHE_NAME = 'whisky-vault-v7-bunker'; // Neue Version für stabilen Bunker + SWR const CACHE_NAME = 'whisky-vault-v8-bunker'; // Optimierter Bunker v8
// CONFIG: Kern-Seiten und Assets für den Bunker (sofortiges Pre-Caching) // CONFIG: Kern-Seiten und Assets für den Bunker (sofortiges Pre-Caching)
const CORE_PAGES = [ const CORE_PAGES = [
@@ -6,13 +6,13 @@ const CORE_PAGES = [
]; ];
const STATIC_ASSETS = [ const STATIC_ASSETS = [
'/manifest.webmanifest', // Korrigierter Pfad für Next.js '/manifest.webmanifest',
'/icon-192.png', '/icon-192.png',
'/icon-512.png', '/icon-512.png',
'/favicon.ico', '/favicon.ico',
]; ];
// Helper: Fetch mit Timeout und AbortController (sauberer Abbruch) // Helper: Fetch mit Timeout und Sauberen Abort
async function fetchWithTimeout(request, timeoutMs = 3000) { async function fetchWithTimeout(request, timeoutMs = 3000) {
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs); const id = setTimeout(() => controller.abort(), timeoutMs);
@@ -26,36 +26,47 @@ 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: Lade alles Wichtige einzeln in den Bunker
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
self.skipWaiting(); self.skipWaiting();
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME).then(async (cache) => { caches.open(CACHE_NAME).then(async (cache) => {
console.log('🏗️ PWA: Building bunker v7...'); 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) => { const promises = [...CORE_PAGES, ...STATIC_ASSETS].map(async (url) => {
try { try {
const res = await fetch(url); // Im Dev-Mode kann / lange dauern (Kompilierung). 10s Timeout.
const res = await fetchWithTimeout(url, 10000);
if (!res.ok) throw new Error(`Status ${res.status}`); if (!res.ok) throw new Error(`Status ${res.status}`);
return cache.put(url, res); await cache.put(url, res);
} catch (error) { } catch (error) {
console.error(`⚠️ PWA: Pre-cache failed for ${url}:`, 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); await Promise.all(promises);
console.log('✅ PWA: Bunker build finished'); console.log('✅ PWA: Bunker build finished');
// Signal to clients that pre-caching is complete // Signal an Clients: Bunker ist bereit
broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME }); broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME });
}) })
); );
}); });
// Helper: Broadcast to all clients
async function broadcast(message) {
const clients = await self.clients.matchAll();
clients.forEach(client => client.postMessage(message));
}
// Activate: Alte Bunker räumen // Activate: Alte Bunker räumen
self.addEventListener('activate', (event) => { self.addEventListener('activate', (event) => {
event.waitUntil( event.waitUntil(
@@ -114,7 +125,6 @@ self.addEventListener('fetch', (event) => {
} }
// 2. ASSETS & NAVIGATION: Stale-While-Revalidate (Der "Echte" Bunker Mode) // 2. ASSETS & NAVIGATION: Stale-While-Revalidate (Der "Echte" Bunker Mode)
// Wir liefern SOFORT aus dem Cache, fragen aber im Hintergrund das Netzwerk.
const isNavigation = event.request.mode === 'navigate'; const isNavigation = event.request.mode === 'navigate';
const isAsset = event.request.destination === 'style' || const isAsset = event.request.destination === 'style' ||
event.request.destination === 'script' || event.request.destination === 'script' ||
@@ -126,7 +136,7 @@ self.addEventListener('fetch', (event) => {
if (isNavigation || isAsset) { if (isNavigation || isAsset) {
event.respondWith( event.respondWith(
caches.match(event.request).then(async (cachedResponse) => { caches.match(event.request).then(async (cachedResponse) => {
// Hintergrund-Update vorbereiten // Hintergrund-Update
const fetchPromise = fetchWithTimeout(event.request, 5000) const fetchPromise = fetchWithTimeout(event.request, 5000)
.then(async (networkResponse) => { .then(async (networkResponse) => {
if (networkResponse && networkResponse.status === 200) { if (networkResponse && networkResponse.status === 200) {
@@ -135,21 +145,14 @@ self.addEventListener('fetch', (event) => {
} }
return networkResponse; return networkResponse;
}) })
.catch(() => { /* Fail silently in background */ }); .catch(() => { /* Silent in background */ });
// Navigation Fallback Logik
if (isNavigation) { if (isNavigation) {
if (cachedResponse) return cachedResponse; if (cachedResponse) return cachedResponse;
// Root Fallback für Deep Links (App Shell)
const shell = await caches.match('/'); const shell = await caches.match('/');
if (shell) { if (shell) return shell;
console.log('[SW] Route not cached, using Root App Shell fallback');
return shell;
}
} }
// Assets: Cache oder Netzwerk
return cachedResponse || fetchPromise || fetch(event.request); return cachedResponse || fetchPromise || fetch(event.request);
}) })
); );

View File

@@ -6,6 +6,7 @@ import { WifiOff, ShieldCheck } from 'lucide-react';
export default function OfflineIndicator() { export default function OfflineIndicator() {
const [isOffline, setIsOffline] = useState(false); const [isOffline, setIsOffline] = useState(false);
const [isBunkerReady, setIsBunkerReady] = useState(false); const [isBunkerReady, setIsBunkerReady] = useState(false);
const [progress, setProgress] = useState(0);
useEffect(() => { useEffect(() => {
setIsOffline(!navigator.onLine); setIsOffline(!navigator.onLine);
@@ -17,6 +18,9 @@ export default function OfflineIndicator() {
const handleOffline = () => setIsOffline(true); const handleOffline = () => setIsOffline(true);
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'PRECACHE_PROGRESS') {
setProgress(event.data.progress);
}
if (event.data?.type === 'PRECACHE_COMPLETE' || event.data?.type === 'BUNKER_STATUS') { 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 for offline use!');
setIsBunkerReady(true); setIsBunkerReady(true);
@@ -30,10 +34,27 @@ export default function OfflineIndicator() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', handleMessage); navigator.serviceWorker.addEventListener('message', handleMessage);
// Proactively check status if SW is already active // Proactive check
if (navigator.serviceWorker.controller) { navigator.serviceWorker.ready.then((registration) => {
navigator.serviceWorker.controller.postMessage({ type: 'CHECK_BUNKER_STATUS' }); 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);
};
} }
return () => { return () => {
@@ -43,7 +64,7 @@ export default function OfflineIndicator() {
navigator.serviceWorker.removeEventListener('message', handleMessage); navigator.serviceWorker.removeEventListener('message', handleMessage);
} }
}; };
}, []); }, [isBunkerReady]);
if (isOffline) { if (isOffline) {
return ( return (
@@ -76,7 +97,7 @@ export default function OfflineIndicator() {
<div className="bg-zinc-900/80 backdrop-blur-md border border-amber-500/30 px-3 py-1.5 rounded-full flex items-center gap-2 shadow-lg shadow-amber-500/10"> <div className="bg-zinc-900/80 backdrop-blur-md border border-amber-500/30 px-3 py-1.5 rounded-full flex items-center gap-2 shadow-lg shadow-amber-500/10">
<div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" /> <div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" />
<span className="text-[9px] font-black uppercase tracking-widest text-zinc-300"> <span className="text-[9px] font-black uppercase tracking-widest text-zinc-300">
Bunker wird geladen... Bunker wird geladen... {progress > 0 ? `${progress}%` : ''}
</span> </span>
</div> </div>
</div> </div>