From 87856c87f015af0bc3ef39b775ffbf29ae74dc6e Mon Sep 17 00:00:00 2001 From: robin Date: Sat, 20 Dec 2025 23:32:07 +0100 Subject: [PATCH] sw.js fixed --- .fixoffline | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++ public/sw.js | 117 +++++++++++++++++++++++------------------ 2 files changed, 213 insertions(+), 50 deletions(-) create mode 100644 .fixoffline diff --git a/.fixoffline b/.fixoffline new file mode 100644 index 0000000..b2fbb2f --- /dev/null +++ b/.fixoffline @@ -0,0 +1,146 @@ +Hier ist die instruktive Anleitung für deine IDE (Cursor, Antigravity, etc.). Du kannst diesen Text direkt als Prompt oder Kontext-Datei (`OFFLINE_STRATEGY.md`) in deine IDE kopieren. + +Es enthält die **Pre-Caching Strategie** (für den Start im Keller) und den **Navigation Fallback** (gegen den White Screen). + +--- + +# 🛡️ PWA "Ironclad" Offline Strategy Implementation + +**Context:** The application fails to load when started completely offline ("Keller-Szenario") or crashes during navigation when the network is flaky. +**Goal:** Implement aggressive Pre-Caching for core routes and a robust Navigation Fallback in `public/sw.js`. +**Framework:** Next.js (App Router) + Workbox. + +## 🚨 Critical Requirements + +1. **Preserve Existing Logic:** Do not remove the `skipWaiting()` or `clients.claim()` logic if already present. +2. **App Router Compatibility:** Ensure `_next/data` requests do not cause crashes. +3. **Dexie Integration:** Rely on the existing `useLiveQuery` hooks for data. The Service Worker only needs to deliver the **App Shell**. + +--- + +## 📝 Implementation Instructions for `public/sw.js` + +Please refactor or append the Service Worker logic to include the following **three mechanics**: + +### 1. Define Core Pages (The "Keller" List) + +Define a list of static routes that *must* work immediately upon installation, even without a network connection. + +```javascript +// CONFIG: Core pages to pre-cache immediately on install +const CORE_PAGES = [ + '/', // Dashboard / Home + '/tasting/new', // Critical: Add Tasting Screen + '/scan', // Critical: Scan Screen + '/offline' // Optional: Dedicated offline fallback +]; + +``` + +### 2. The "Warm Cache" Install Event + +Add an `install` event listener that aggressively fetches these pages into the cache *before* the user needs them. + +```javascript +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open('pages').then((cache) => { + console.log('⚡ PWA: Pre-caching core pages for offline usage...'); + // { cache: 'reload' } forces a network fetch, ignoring browser http cache + return cache.addAll(CORE_PAGES); + }) + ); + self.skipWaiting(); +}); + +``` + +### 3. The Navigation Safety Net (The "Anti-Crash" Logic) + +Implement a `NavigationRoute` that attempts the network first (with a short timeout), falls back to the cache, and—**critically**—falls back to the Root (`/`) if the specific deep-link is missing from the cache. + +*Rationale:* In Next.js (SPA), loading `/` from cache is sufficient to bootstrap the React app. The client-side router will then handle the URL, and Dexie will provide the data. + +```javascript +import { registerRoute, NavigationRoute } from 'workbox-routing'; +import { NetworkFirst } from 'workbox-strategies'; +import { CacheableResponsePlugin } from 'workbox-cacheable-response'; + +// Strategy: Network (3s timeout) -> Cache -> Fallback to Root +const navigationHandler = async (params) => { + try { + // 1. Try Network First (Fail fast if poor connection) + const strategy = new NetworkFirst({ + cacheName: 'pages', + networkTimeoutSeconds: 3, + plugins: [ + new CacheableResponsePlugin({ statuses: [0, 200] }), + ], + }); + return await strategy.handle(params); + + } catch (error) { + // 2. Network failed. Try getting the specific page from cache. + const cache = await caches.open('pages'); + // params.request.url contains the full URL user is trying to reach + let response = await cache.match(params.request); + + if (response) return response; + + // 3. CRITICAL FALLBACK: Load the App Shell (Root) + // Even if the user requested /tasting/new, serving / allows Next.js to start. + // Once React hydrates, it sees the URL is /tasting/new and renders the correct page + // using data from Dexie. + const fallbackResponse = await cache.match('/'); + if (fallbackResponse) { + console.log('⚠️ PWA: Serving Root App Shell for offline navigation.'); + return fallbackResponse; + } + + // 4. Absolute last resort + return new Response('Offline mode unavailable', { status: 504 }); + } +}; + +// Apply this handler to all Navigation requests +registerRoute(new NavigationRoute(navigationHandler)); + +``` + +### 4. Handling Next.js Data (RSC) + +Prevent crashes when `_next/data/...` fetches fail. + +```javascript +// Swallow errors for RSC data to prevent "Application Error" screens +registerRoute( + ({ url }) => url.pathname.startsWith('/_next/data/'), + new NetworkFirst({ + cacheName: 'next-data', + networkTimeoutSeconds: 2, + plugins: [ + { + // If offline and not in cache, return empty JSON to keep React alive + handlerDidError: async () => new Response(JSON.stringify({}), { + headers: { 'Content-Type': 'application/json' } + }) + } + ] + }) +); + +``` + +--- + +## ✅ Verification Checklist + +After applying these changes: + +1. **Clear Storage:** Unregister old Service Workers and clear Storage in DevTools. +2. **Install:** Load the app once online. Verify `pages` cache contains `/tasting/new`. +3. **Test:** +* Go Offline (Airplane Mode). +* Reload the page. +* Navigate to `/tasting/new`. +* **Expectation:** App loads, UI renders, data comes from Dexie. No "No Internet" dino. \ No newline at end of file diff --git a/public/sw.js b/public/sw.js index e11b16e..97fe8db 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,9 +1,12 @@ -const CACHE_NAME = 'whisky-vault-v5'; // Increment version for refined strategy +const CACHE_NAME = 'whisky-vault-v6-bunker'; // Neue Version erzwingen! // CONFIG: Core pages and assets to pre-cache immediately on install +// WICHTIG: Hier muss die URL für "Neues Tasting" rein, damit sie offline da ist! const CORE_PAGES = [ - '/', // Dashboard / Home (Includes Scan and Tasting features) - '/sessions', // Tasting Sessions overview + '/', // Dashboard / App Shell + '/sessions', // Tasting Sessions + '/scan', // Scan Page (falls URL existiert) + // '/tasting/new' // <--- FÜGE HIER DEINE TASTING URL HINZU, falls sie existiert! ]; const STATIC_ASSETS = [ @@ -13,48 +16,41 @@ const STATIC_ASSETS = [ '/favicon.ico', ]; +// Install: Lade alles Wichtige sofort in den Keller self.addEventListener('install', (event) => { + self.skipWaiting(); // Sofortiger Wechsel zum neuen SW event.waitUntil( caches.open(CACHE_NAME).then((cache) => { console.log('⚡ PWA: Pre-caching core pages and assets...'); - // Combine items to cache return cache.addAll([...CORE_PAGES, ...STATIC_ASSETS]); }) ); - self.skipWaiting(); }); +// Activate: Alten Müll rauswerfen 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 cache', cacheName); return caches.delete(cacheName); } }) ); }) ); - self.clients.claim(); + self.clients.claim(); // Sofort Kontrolle übernehmen }); -// Helper for Network-First with Timeout +// Helper: Network mit Timeout (für den Notfall) function fetchWithTimeout(request, timeoutMs = 3000) { return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - reject(new Error('Network timeout')); - }, timeoutMs); - + const timeoutId = setTimeout(() => reject(new Error('Network timeout')), timeoutMs); fetch(request).then( - (response) => { - clearTimeout(timeoutId); - resolve(response); - }, - (err) => { - clearTimeout(timeoutId); - reject(err); - } + (res) => { clearTimeout(timeoutId); resolve(res); }, + (err) => { clearTimeout(timeoutId); reject(err); } ); }); } @@ -62,35 +58,41 @@ function fetchWithTimeout(request, timeoutMs = 3000) { self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); - // CRITICAL: Always bypass cache for auth, api, and supabase requests - const isAuthRequest = url.pathname.includes('/auth/') || - url.pathname.includes('/v1/auth/') || - url.pathname.includes('/v1/token'); - const isApiRequest = url.pathname.includes('/api/'); - const isSupabaseRequest = url.hostname.includes('supabase.co'); - - if (isAuthRequest || isApiRequest || isSupabaseRequest) { + // 0. BYPASS: Auth & API immer durchlassen (werden von Dexie/App handled) + if (url.pathname.includes('/auth/') || + url.pathname.includes('/api/') || + url.hostname.includes('supabase.co')) { return; } - // 1. NEXT.JS DATA (RSC): Stale-While-Revalidate with empty JSON fallback + // 1. NEXT.JS DATA (RSC): Swallow Errors + // Verhindert "Application Error" wenn JSON fehlt if (url.pathname.startsWith('/_next/data/')) { event.respondWith( - fetchWithTimeout(event.request, 2000) - .catch(async () => { - const cachedResponse = await caches.match(event.request); - if (cachedResponse) return cachedResponse; + caches.match(event.request).then((cached) => { + // Wenn im Cache (selten bei RSC), nimm es + if (cached) return cached; - // Fallback to empty JSON to prevent "Application Error" screens - return new Response(JSON.stringify({}), { - headers: { 'Content-Type': 'application/json' } + // Sonst Netzwerk mit Timeout + return fetchWithTimeout(event.request, 2000) + .then(res => { + // Optional: RSC cachen für später? + // Vorsicht: RSC Daten veralten schnell. Hier cachen wir NICHT. + return res; + }) + .catch(() => { + // Fallback: Leeres JSON, damit React nicht crasht + return new Response(JSON.stringify({}), { + headers: { 'Content-Type': 'application/json' } + }); }); - }) + }) ); return; } - // 2. ASSETS & APP SHELL: Stale-While-Revalidate + // 2. ASSETS (JS/CSS/Images): CACHE FIRST (Der "Bunker" Fix) + // Wenn wir die Datei haben, nutzen wir sie. Keine Diskussion mit dem Netzwerk. const isAsset = event.request.destination === 'style' || event.request.destination === 'script' || event.request.destination === 'worker' || @@ -101,7 +103,13 @@ self.addEventListener('fetch', (event) => { if (isAsset) { event.respondWith( caches.match(event.request).then((cachedResponse) => { - const fetchPromise = fetch(event.request).then((networkResponse) => { + if (cachedResponse) { + // TREFFER: Sofort zurückgeben. Kein Fetch! + // Das verhindert den "Network Changed" Crash. + return cachedResponse; + } + // MISS: Nur dann zum Netzwerk gehen + return fetch(event.request).then((networkResponse) => { if (networkResponse && networkResponse.status === 200) { const responseClone = networkResponse.clone(); caches.open(CACHE_NAME).then((cache) => { @@ -110,27 +118,36 @@ self.addEventListener('fetch', (event) => { } return networkResponse; }); - return cachedResponse || fetchPromise; }) ); return; } - // 3. NAVIGATION: Ironclad Navigation Fallback + // 3. NAVIGATION (HTML): App Shell / Cache First + // Wenn offline, gib die Startseite zurück und lass Next.js routen. if (event.request.mode === 'navigate') { event.respondWith( - fetchWithTimeout(event.request, 3000) - .catch(async () => { - console.log('[SW] Navigation network failure, attempting cache fallback'); + (async () => { + try { + // A. Suche exakte Seite im Cache const cachedResponse = await caches.match(event.request); if (cachedResponse) return cachedResponse; - // CRITICAL FALLBACK: Load the Root App Shell ('/') - // This allows Next.js to bootstrap and handle the routing client-side - console.warn('⚠️ PWA: Route not in cache, fallback to Root App Shell'); - return caches.match('/'); - }) + // B. Suche Startseite (App Shell) im Cache + // Das ist der wichtigste Fallback für SPAs! + const shellResponse = await caches.match('/'); + if (shellResponse) return shellResponse; + + // C. Wenn nichts im Cache ist (ganz neuer User): Netzwerk + return await fetchWithTimeout(event.request, 3000); + } catch (error) { + console.log('[SW] Offline fallback failed:', error); + // D. Letzter Ausweg (Text anzeigen) + return new Response('

Offline

Bitte App neu laden.

', { + headers: { 'Content-Type': 'text/html' } + }); + } + })() ); - return; } -}); +}); \ No newline at end of file