From 414fba39c7fe0222efc49a2cafe180285a10d9d5 Mon Sep 17 00:00:00 2001 From: Dicken Date: Thu, 4 Jun 2026 09:36:30 +0200 Subject: [PATCH] Fix PWA-Reload: __BUILD_TIME__ und version.txt aus selber Quelle; Fix QuickLinkIcon custom icons --- Dockerfile | 9 ++++--- frontend/src/App.jsx | 23 ++++++++++------- frontend/src/main.jsx | 56 ++++++++++++++++------------------------- frontend/vite.config.js | 16 +++++++----- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/Dockerfile b/Dockerfile index 104bd8d..531fb7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,10 @@ WORKDIR /build COPY frontend/package.json ./ RUN npm install COPY frontend/ ./ +# version.txt VOR dem Build generieren – Unix-Timestamp (Sekunden) +# Dieser Wert wird von vite.config.js als __BUILD_TIME__ ins Bundle gebacken +# UND vom Backend als /api/build-time ausgeliefert → exakt derselbe Wert +RUN date -u +"%s" > ./version.txt RUN npm run build # ── Stage 2: Express liefert API + React in einem Container ────────────────── @@ -13,9 +17,8 @@ WORKDIR /app COPY backend/package.json ./ RUN npm install --production COPY backend/ ./ -# 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 +# version.txt aus dem Build-Stage kopieren (selber Wert wie im JS-Bundle) +COPY --from=builder /build/version.txt ./version.txt COPY --from=builder /build/dist ./public EXPOSE 4000 CMD ["node", "src/index.js"] diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 85e8c56..8b0df8b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -455,17 +455,22 @@ function QuickLinksCarousel({ links }) { function QuickLinkIcon({ l }) { const [imgOk, setImgOk] = useState(true); - // Favicon-URL bauen: gespeicherten Wert nehmen oder aus URL ableiten + // Favicon nur wenn kein eigenes Icon gesetzt (Standard-🔗) oder bereits eine URL gespeichert ist + const useDefaultFavicon = !l.icon || l.icon === '🔗'; const faviconUrl = (() => { if (l.icon && l.icon.startsWith('http')) return l.icon; - try { - const host = new URL(l.url).hostname; - return `https://www.google.com/s2/favicons?sz=64&domain=${host}`; - } catch { return null; } + if (useDefaultFavicon) { + try { + const host = new URL(l.url).hostname; + return `https://www.google.com/s2/favicons?sz=64&domain=${host}`; + } catch { return null; } + } + return null; })(); - const showEmoji = !faviconUrl || !imgOk; - const fallbackEmoji = l.icon && !l.icon.startsWith('http') ? l.icon : '🔗'; + const showImg = !!faviconUrl && imgOk; + const showEmoji = !showImg; + const emoji = (l.icon && !l.icon.startsWith('http')) ? l.icon : '🔗'; return ( - {!showEmoji + {showImg ? setImgOk(false)}/> - : {fallbackEmoji} + : {emoji} } {l.title} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 4517b46..3f0c3be 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -3,17 +3,13 @@ import ReactDOM from 'react-dom/client' import App from './App.jsx' // ── 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 +// __BUILD_TIME__ wird von Vite zur Build-Zeit aus version.txt gebacken. +// /api/build-time liefert denselben Wert vom Server. +// Wenn sie abweichen → neues Build wurde deployed → Reload. +// Kein localStorage, keine Race-Condition. +const MY_BUILD_TIME = __BUILD_TIME__; window.__newBuildAvailable = false; -const STORED_KEY = 'dd_build_time'; - -// Läuft gerade eine Eingabe? (Fokus auf Eingabefeld) function isUserTyping() { const el = document.activeElement; if (!el) return false; @@ -21,7 +17,6 @@ function isUserTyping() { 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) { @@ -40,21 +35,20 @@ async function getBuildTime() { } 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); - if (!stored) { - localStorage.setItem(STORED_KEY, bt); - } else if (stored !== bt) { - localStorage.setItem(STORED_KEY, bt); +// Prüft ob Server-Build != Bundle-Build → Flag setzen +// reloadNow: sofort laden wenn kein Input aktiv (für App-Fokus) +async function checkBuild({ reloadNow = false } = {}) { + const serverBt = await getBuildTime(); + if (!serverBt) return; + if (serverBt !== MY_BUILD_TIME) { window.__newBuildAvailable = true; + if (reloadNow && !isUserTyping() && !hasUnsavedInput()) { + window.location.reload(); + } } } -// Sicherer Reload: nur wenn neuer Build da UND keine Eingabe aktiv/ungespeichert -// Wird von App.jsx beim Dashboard-Klick aufgerufen. Gibt true zurück wenn neu geladen wird. +// Sicherer Reload bei Seitenwechsel (aufgerufen aus App.jsx setActive) window.__safeReloadIfNewBuild = function () { if (!window.__newBuildAvailable) return false; if (isUserTyping() || hasUnsavedInput()) return false; @@ -62,32 +56,26 @@ window.__safeReloadIfNewBuild = function () { return true; }; -// Beim Start einmal prüfen (App-Öffnen Fall 1: Kaltstart) +// Beim Start prüfen (kein sofortiger Reload – Seite lädt gerade erst) checkBuild(); -// Im Hintergrund alle 60s nur das Flag aktualisieren +// Im Hintergrund alle 60s setInterval(() => checkBuild(), 60_000); -// App-Öffnen Fall 2: App kommt aus dem Hintergrund zurück in den Vordergrund -document.addEventListener('visibilitychange', async () => { +// App kommt in den Vordergrund → prüfen und bei Bedarf sofort laden +// (PWA vom Homescreen öffnen / aus dem Hintergrund holen) +document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { - await checkBuild(); - // Nur laden wenn der User nicht gerade tippt und nichts ungespeichert ist - if (!isUserTyping() && !hasUnsavedInput()) { - window.__safeReloadIfNewBuild(); - } + checkBuild({ reloadNow: true }); } }); // ── Service Worker ──────────────────────────────────────────────────────────── if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', (e) => { - if (e.data?.type === 'SW_ACTIVATED') { - window.__newBuildAvailable = true; // nur merken - } + if (e.data?.type === 'SW_ACTIVATED') window.__newBuildAvailable = true; }); 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'); reg.addEventListener('updatefound', () => { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index d81b529..b954fc2 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,12 +1,16 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import { readFileSync } from 'fs' -const now = new Date(); -const buildTime = now.toLocaleString('de-DE', { - timeZone: 'Europe/Berlin', - day: '2-digit', month: '2-digit', year: 'numeric', - hour: '2-digit', minute: '2-digit' -}); +// version.txt wird im Dockerfile VOR dem Build generiert (date +%s) +// Damit haben __BUILD_TIME__ im Bundle und /api/build-time exakt denselben Wert +let buildTime; +try { + buildTime = readFileSync('./version.txt', 'utf8').trim(); +} catch { + // Fallback für lokale Entwicklung ohne Dockerfile + buildTime = String(Date.now()); +} export default defineConfig({ plugins: [react()],