PWA-Reload sauber: nur bei App-Oeffnen oder Dashboard-Klick, niemals bei Eingaben; version.txt im Build, SW v4, Carousel 4 Icons, Kalender ohne Komma
This commit is contained in:
@@ -13,7 +13,9 @@ WORKDIR /app
|
|||||||
COPY backend/package.json ./
|
COPY backend/package.json ./
|
||||||
RUN npm install --production
|
RUN npm install --production
|
||||||
COPY backend/ ./
|
COPY backend/ ./
|
||||||
COPY version.txt ./version.txt
|
# version.txt bei jedem Build mit aktuellem Zeitstempel überschreiben
|
||||||
|
# → /api/build-time liefert neuen Wert → Frontend erkennt neuen Build → Auto-Reload
|
||||||
|
RUN date -u +"build-%Y%m%d-%H%M%S" > ./version.txt
|
||||||
COPY --from=builder /build/dist ./public
|
COPY --from=builder /build/dist ./public
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
CMD ["node", "src/index.js"]
|
CMD ["node", "src/index.js"]
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ app.get('/manifest.json', (_req, res) => {
|
|||||||
app.get('/sw.js', (_req, res) => {
|
app.get('/sw.js', (_req, res) => {
|
||||||
res.setHeader('Content-Type', 'application/javascript');
|
res.setHeader('Content-Type', 'application/javascript');
|
||||||
res.setHeader('Service-Worker-Allowed', '/');
|
res.setHeader('Service-Worker-Allowed', '/');
|
||||||
res.setHeader('Cache-Control', 'no-cache');
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
res.sendFile(path.join(PUBLIC, 'sw.js'));
|
res.sendFile(path.join(PUBLIC, 'sw.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,12 +90,6 @@ app.use(express.static(PUBLIC, {
|
|||||||
if (filePath.match(/\.(js|css)$/) && !filePath.includes('sw.js')) {
|
if (filePath.match(/\.(js|css)$/) && !filePath.includes('sw.js')) {
|
||||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
}
|
}
|
||||||
// index.html niemals cachen – auch nicht über express.static
|
|
||||||
if (filePath.endsWith('index.html')) {
|
|
||||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
||||||
res.setHeader('Pragma', 'no-cache');
|
|
||||||
res.setHeader('Expires', '0');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -101,7 +97,6 @@ app.use(express.static(PUBLIC, {
|
|||||||
app.get('*', (_req, res) => {
|
app.get('*', (_req, res) => {
|
||||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
res.setHeader('Pragma', 'no-cache');
|
res.setHeader('Pragma', 'no-cache');
|
||||||
res.setHeader('Expires', '0');
|
|
||||||
res.sendFile(path.join(PUBLIC, 'index.html'));
|
res.sendFile(path.join(PUBLIC, 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,44 @@
|
|||||||
// Service Worker – index.html immer frisch vom Server, Assets gecacht
|
// Service Worker v4 – garantiert frische index.html, Auto-Reload bei neuem Build
|
||||||
const CACHE_NAME = 'dickendock-v3';
|
const VERSION = 'v4';
|
||||||
|
const CACHE_NAME = 'dickendock-' + VERSION;
|
||||||
|
|
||||||
self.addEventListener('install', () => {
|
self.addEventListener('install', () => {
|
||||||
self.skipWaiting();
|
self.skipWaiting(); // neuen SW sofort übernehmen
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', e => {
|
self.addEventListener('activate', e => {
|
||||||
e.waitUntil(
|
e.waitUntil((async () => {
|
||||||
caches.keys()
|
// alte Caches löschen
|
||||||
.then(keys => Promise.all(keys.map(k => caches.delete(k))))
|
const keys = await caches.keys();
|
||||||
.then(() => self.clients.claim())
|
await Promise.all(keys.map(k => caches.delete(k)));
|
||||||
);
|
await self.clients.claim();
|
||||||
|
// allen offenen Clients sagen: neue Version aktiv → reload
|
||||||
|
const clients = await self.clients.matchAll({ type: 'window' });
|
||||||
|
clients.forEach(c => c.postMessage({ type: 'SW_ACTIVATED', version: VERSION }));
|
||||||
|
})());
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', e => {
|
self.addEventListener('fetch', e => {
|
||||||
const url = new URL(e.request.url);
|
const url = new URL(e.request.url);
|
||||||
|
|
||||||
// API: immer Netzwerk
|
// API immer direkt ans Netz
|
||||||
if (url.pathname.startsWith('/api/')) {
|
if (url.pathname.startsWith('/api/')) {
|
||||||
e.respondWith(fetch(e.request));
|
e.respondWith(fetch(e.request));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML-Navigation: immer frisch vom Server, nie aus Cache
|
// HTML-Navigation: IMMER frisch vom Netzwerk, niemals aus Cache
|
||||||
|
// (Vite-Bundles haben Hash im Namen → neue index.html lädt neue Bundles)
|
||||||
if (e.request.mode === 'navigate') {
|
if (e.request.mode === 'navigate') {
|
||||||
e.respondWith(
|
e.respondWith(
|
||||||
fetch(e.request, { cache: 'no-store' })
|
fetch(e.request, { cache: 'reload' }) // 'reload' = HTTP-Cache komplett umgehen
|
||||||
.catch(() => caches.match('/index.html'))
|
.catch(() => caches.match('/index.html'))
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statische Assets (JS/CSS mit Hash): Network-first, Cache als Fallback
|
// Restliche Assets: einfach durchreichen
|
||||||
e.respondWith(
|
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
|
||||||
fetch(e.request)
|
|
||||||
.then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
caches.open(CACHE_NAME).then(c => c.put(e.request, res.clone()));
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
.catch(() => caches.match(e.request))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('message', e => {
|
self.addEventListener('message', e => {
|
||||||
|
|||||||
@@ -2379,7 +2379,9 @@ 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) => {
|
||||||
if (window.__newBuildAvailable) { window.location.reload(); return; }
|
// Reload nur beim Klick auf Dashboard und nur wenn ein neuer Build vorliegt
|
||||||
|
// und keine Eingabe aktiv ist (schützt ungespeicherte Eingaben)
|
||||||
|
if (page === 'dashboard' && window.__newBuildAvailable && window.__safeReloadIfNewBuild?.()) return;
|
||||||
setActiveRaw(page);
|
setActiveRaw(page);
|
||||||
};
|
};
|
||||||
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
|
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
|
||||||
|
|||||||
@@ -2,46 +2,93 @@ 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'
|
||||||
|
|
||||||
// ── Build-Zeit Check & Auto-Reload ───────────────────────────────────────────
|
// ── Auto-Reload bei neuem Build ───────────────────────────────────────────────
|
||||||
|
// Regeln:
|
||||||
|
// - Neuer Build wird erkannt → nur Flag setzen (window.__newBuildAvailable)
|
||||||
|
// - Reload passiert NUR:
|
||||||
|
// a) beim Öffnen der App (App kommt aus Hintergrund in den Vordergrund)
|
||||||
|
// b) beim Klick auf "Dashboard" (in App.jsx geprüft)
|
||||||
|
// - NIEMALS während einer Eingabe oder bei ungespeicherten Feldern
|
||||||
window.__newBuildAvailable = false;
|
window.__newBuildAvailable = false;
|
||||||
|
|
||||||
const STORED_KEY = 'dd_build_time';
|
const STORED_KEY = 'dd_build_time';
|
||||||
|
|
||||||
async function checkBuildTime({ reloadIfChanged = false } = {}) {
|
// Läuft gerade eine Eingabe? (Fokus auf Eingabefeld)
|
||||||
|
function isUserTyping() {
|
||||||
|
const el = document.activeElement;
|
||||||
|
if (!el) return false;
|
||||||
|
const tag = el.tagName;
|
||||||
|
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stehen irgendwo ungespeicherte Eingaben in Textfeldern?
|
||||||
|
function hasUnsavedInput() {
|
||||||
|
const fields = document.querySelectorAll('input, textarea');
|
||||||
|
for (const f of fields) {
|
||||||
|
const isText = f.tagName === 'TEXTAREA' ||
|
||||||
|
['text','search','url','email','number','password','tel'].includes(f.type);
|
||||||
|
if (isText && f.value && f.value.trim() !== '') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuildTime() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/build-time', { cache: 'no-store' });
|
const r = await fetch('/api/build-time', { cache: 'no-store' });
|
||||||
const { buildTime } = await r.json();
|
const { buildTime } = await r.json();
|
||||||
|
return buildTime;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüft die Build-Zeit und setzt nur das Flag (lädt nie von selbst)
|
||||||
|
async function checkBuild() {
|
||||||
|
const bt = await getBuildTime();
|
||||||
|
if (!bt) return;
|
||||||
const stored = localStorage.getItem(STORED_KEY);
|
const stored = localStorage.getItem(STORED_KEY);
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
localStorage.setItem(STORED_KEY, buildTime);
|
localStorage.setItem(STORED_KEY, bt);
|
||||||
} else if (stored !== buildTime) {
|
} else if (stored !== bt) {
|
||||||
localStorage.setItem(STORED_KEY, buildTime);
|
localStorage.setItem(STORED_KEY, bt);
|
||||||
window.__newBuildAvailable = true;
|
window.__newBuildAvailable = true;
|
||||||
// Wenn explizit gewünscht (App-Fokus): sofort neu laden
|
|
||||||
if (reloadIfChanged) window.location.reload();
|
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Beim Start prüfen
|
// Sicherer Reload: nur wenn neuer Build da UND keine Eingabe aktiv/ungespeichert
|
||||||
checkBuildTime();
|
// Wird von App.jsx beim Dashboard-Klick aufgerufen. Gibt true zurück wenn neu geladen wird.
|
||||||
// Alle 60s im Hintergrund
|
window.__safeReloadIfNewBuild = function () {
|
||||||
setInterval(() => checkBuildTime(), 60_000);
|
if (!window.__newBuildAvailable) return false;
|
||||||
|
if (isUserTyping() || hasUnsavedInput()) return false;
|
||||||
|
window.location.reload();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// Wenn die App/der Tab wieder in den Vordergrund kommt → prüfen und ggf. sofort neu laden
|
// Beim Start einmal prüfen (App-Öffnen Fall 1: Kaltstart)
|
||||||
// Das deckt den Fall ab: PWA vom Homescreen öffnen
|
checkBuild();
|
||||||
document.addEventListener('visibilitychange', () => {
|
// Im Hintergrund alle 60s nur das Flag aktualisieren
|
||||||
|
setInterval(() => checkBuild(), 60_000);
|
||||||
|
|
||||||
|
// App-Öffnen Fall 2: App kommt aus dem Hintergrund zurück in den Vordergrund
|
||||||
|
document.addEventListener('visibilitychange', async () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
checkBuildTime({ reloadIfChanged: true });
|
await checkBuild();
|
||||||
|
// Nur laden wenn der User nicht gerade tippt und nichts ungespeichert ist
|
||||||
|
if (!isUserTyping() && !hasUnsavedInput()) {
|
||||||
|
window.__safeReloadIfNewBuild();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
window.addEventListener('focus', () => {
|
|
||||||
checkBuildTime({ reloadIfChanged: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Service Worker ────────────────────────────────────────────────────────────
|
// ── Service Worker ────────────────────────────────────────────────────────────
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.addEventListener('message', (e) => {
|
||||||
|
if (e.data?.type === 'SW_ACTIVATED') {
|
||||||
|
window.__newBuildAvailable = true; // nur merken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||||||
|
// Browser regelmäßig nach neuem SW suchen lassen
|
||||||
|
setInterval(() => reg.update().catch(()=>{}), 60_000);
|
||||||
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;
|
||||||
@@ -49,9 +96,6 @@ if ('serviceWorker' in navigator) {
|
|||||||
if (nw.state === 'installed') nw.postMessage('SKIP_WAITING');
|
if (nw.state === 'installed') nw.postMessage('SKIP_WAITING');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
||||||
window.__newBuildAvailable = true;
|
|
||||||
});
|
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user