Fix PWA-Update: window global statt dynamic import, synchroner Reload bei Seitenwechsel

This commit is contained in:
2026-06-02 19:38:40 +02:00
parent b698818791
commit b0b4c27f00
3 changed files with 26 additions and 41 deletions

View File

@@ -1,52 +1,44 @@
// Service Worker stellt sicher dass nach einem Deploy immer der neue Stand geladen wird // Service Worker index.html immer frisch vom Server, Assets gecacht
// index.html und API: immer vom Netzwerk (no-store) const CACHE_NAME = 'dickendock-v3';
// Assets (JS/CSS mit Hash): Network-first mit Cache-Fallback für Offline
const CACHE_NAME = 'dickendock-v2';
self.addEventListener('install', () => { self.addEventListener('install', () => {
self.skipWaiting(); // sofort aktivieren ohne auf Tab-Schließen zu warten self.skipWaiting();
}); });
self.addEventListener('activate', e => { self.addEventListener('activate', e => {
e.waitUntil( e.waitUntil(
caches.keys() caches.keys()
.then(keys => Promise.all(keys.map(k => caches.delete(k)))) .then(keys => Promise.all(keys.map(k => caches.delete(k))))
.then(() => self.clients.claim()) // alle offenen Tabs sofort übernehmen .then(() => self.clients.claim())
); );
}); });
self.addEventListener('fetch', e => { self.addEventListener('fetch', e => {
const url = new URL(e.request.url); const url = new URL(e.request.url);
// API-Requests: nie cachen // API: immer Netzwerk
if (url.pathname.startsWith('/api/')) { if (url.pathname.startsWith('/api/')) {
e.respondWith(fetch(e.request)); e.respondWith(fetch(e.request));
return; return;
} }
// HTML-Navigation (index.html, /, /*, alle Routen): immer frisch vom Server // HTML-Navigation: immer frisch vom Server, nie aus Cache
// Das ist der Kern: verhindert dass eine alte index.html aus dem HTTP-Cache kommt
if (e.request.mode === 'navigate') { if (e.request.mode === 'navigate') {
e.respondWith( e.respondWith(
fetch(e.request, { fetch(e.request, { cache: 'no-store' })
cache: 'no-store', // Browser-HTTP-Cache umgehen .catch(() => caches.match('/index.html'))
headers: { 'Cache-Control': 'no-cache' }
}).catch(() => caches.match('/index.html')) // nur bei echtem Offline-Fall: Cache-Fallback
); );
return; return;
} }
// Statische Assets (Vite baut mit Content-Hash im Dateinamen): // Statische Assets (JS/CSS mit Hash): Network-first, Cache als Fallback
// Network-first, bei Fehler Cache-Fallback (für Offline-Nutzung)
e.respondWith( e.respondWith(
fetch(e.request) fetch(e.request)
.then(response => { .then(res => {
if (response.ok) { if (res.ok) {
const clone = response.clone(); caches.open(CACHE_NAME).then(c => c.put(e.request, res.clone()));
caches.open(CACHE_NAME).then(c => c.put(e.request, clone));
} }
return response; return res;
}) })
.catch(() => caches.match(e.request)) .catch(() => caches.match(e.request))
); );

View File

@@ -2383,7 +2383,10 @@ export default function App() {
const [user,setUser]=useState(null); const [user,setUser]=useState(null);
const [active,setActiveRaw]=useState('dashboard'); const [active,setActiveRaw]=useState('dashboard');
const setActive = (page) => { const setActive = (page) => {
import('./main.jsx').then(m => m.reloadIfNewBuild?.()).catch(()=>{}); if (window.__newBuildAvailable) {
window.location.reload();
return; // kein Rendern mehr, Reload läuft
}
setActiveRaw(page); setActiveRaw(page);
}; };
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'}); const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});

View File

@@ -2,10 +2,11 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.jsx' import App from './App.jsx'
// ── Auto-Reload bei neuem Build ─────────────────────────────────────────────── // ── Build-Zeit Check ──────────────────────────────────────────────────────────
// Reload passiert beim nächsten Seitenwechsel (setActive) — nicht mitten in Eingaben // Globale Variable synchron lesbar, kein dynamischer Import nötig
window.__newBuildAvailable = false;
const STORED_KEY = 'dd_build_time'; const STORED_KEY = 'dd_build_time';
let newBuildAvailable = false;
async function checkBuildTime() { async function checkBuildTime() {
try { try {
@@ -16,43 +17,32 @@ async function checkBuildTime() {
localStorage.setItem(STORED_KEY, buildTime); localStorage.setItem(STORED_KEY, buildTime);
} else if (stored !== buildTime) { } else if (stored !== buildTime) {
localStorage.setItem(STORED_KEY, buildTime); localStorage.setItem(STORED_KEY, buildTime);
newBuildAvailable = true; window.__newBuildAvailable = true;
} }
} catch {} } catch {}
} }
// Beim Start sofort prüfen (PWA-Launch: index.html könnte gecacht sein) // Sofort beim Start prüfen, dann alle 60s
checkBuildTime(); checkBuildTime();
// Danach alle 60s im Hintergrund weiter prüfen
setInterval(checkBuildTime, 60_000); setInterval(checkBuildTime, 60_000);
// Wird von App.jsx bei jedem Seitenwechsel aufgerufen
export function reloadIfNewBuild() {
if (newBuildAvailable) window.location.reload();
}
// ── Service Worker ──────────────────────────────────────────────────────────── // ── Service Worker ────────────────────────────────────────────────────────────
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => { navigator.serviceWorker.register('/sw.js').then(reg => {
// Falls schon ein wartender SW vorhanden → sofort aktivieren if (reg.waiting) reg.waiting.postMessage('SKIP_WAITING');
if (reg.waiting) {
reg.waiting.postMessage('SKIP_WAITING');
}
reg.addEventListener('updatefound', () => { reg.addEventListener('updatefound', () => {
const nw = reg.installing; const nw = reg.installing;
nw?.addEventListener('statechange', () => { nw?.addEventListener('statechange', () => {
if (nw.state === 'installed') { if (nw.state === 'installed') {
// Neuer SW fertig installiert → sofort aktivieren
nw.postMessage('SKIP_WAITING'); nw.postMessage('SKIP_WAITING');
} }
}); });
}); });
// Wenn der SW-Controller gewechselt hat (neuer SW aktiv): // Neuer SW aktiv → beim nächsten Seitenwechsel reloaden
// newBuildAvailable setzen — Reload passiert beim nächsten setActive
navigator.serviceWorker.addEventListener('controllerchange', () => { navigator.serviceWorker.addEventListener('controllerchange', () => {
newBuildAvailable = true; window.__newBuildAvailable = true;
}); });
}); });
} }