diff --git a/docs/todo.md b/docs/todo.md index ef170cd5..2f2a2f46 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,17 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔄 „Neue Version verfügbar"-Banner (offener-Tab-Problem)** + - Cache-Header waren schon optimal (index.html `no-store`, Assets + `immutable`) – aber eine laufende SPA holt sich nach einem Deploy + nicht von selbst neuen Code. Neuer `useVersionCheck`-Hook holt die + (immer frische) index.html periodisch + beim Zurückkehren zum Tab, + vergleicht die Vite-Asset-Hash-Signatur mit dem Startstand. Bei + Änderung erscheint ein dezentes Banner „Neue Version verfügbar → + Jetzt neu laden" (`UpdateBanner`, oben im Layout). + - Kein Backend-/Build-Change nötig; im Dev-Modus automatisch No-op + (keine `/assets/`-Dateien → leere Signatur). + - [x] **👁 Kundenportal: Toggle „Deaktivierte Verträge anzeigen"** - Der Admin-/Mitarbeiter-Blick (CustomerDetail) hatte den Toggle schon; jetzt auch in der Kundenportal-Vertragsübersicht (`ContractList`, diff --git a/frontend/src/components/UpdateBanner.tsx b/frontend/src/components/UpdateBanner.tsx new file mode 100644 index 00000000..d5bfe643 --- /dev/null +++ b/frontend/src/components/UpdateBanner.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { RefreshCw, X } from 'lucide-react'; +import { useVersionCheck } from '../hooks/useVersionCheck'; + +/** + * Zeigt ein dezentes Banner, sobald nach einem Deploy eine neue Frontend- + * Version verfügbar ist (siehe useVersionCheck). "Jetzt neu laden" lädt die + * Seite neu und holt damit den frischen Code; "später" blendet für die + * laufende Session aus. + */ +export default function UpdateBanner() { + const updateAvailable = useVersionCheck(); + const [dismissed, setDismissed] = useState(false); + + if (!updateAvailable || dismissed) return null; + + return ( +
+
+
+ + Eine neue Version ist verfügbar. Bitte neu laden, um sie zu nutzen. +
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx index be87fcb1..148172fa 100644 --- a/frontend/src/components/layout/Layout.tsx +++ b/frontend/src/components/layout/Layout.tsx @@ -5,6 +5,7 @@ import { gdprApi } from '../../services/api'; import Sidebar from './Sidebar'; import ScrollToTopButton from '../ScrollToTopButton'; import BirthdayModal from '../BirthdayModal'; +import UpdateBanner from '../UpdateBanner'; import { AlertTriangle, ArrowRight, Building, Shield } from 'lucide-react'; function ConsentBanner() { @@ -80,6 +81,7 @@ export default function Layout() {
+
diff --git a/frontend/src/hooks/useVersionCheck.ts b/frontend/src/hooks/useVersionCheck.ts new file mode 100644 index 00000000..4a575b61 --- /dev/null +++ b/frontend/src/hooks/useVersionCheck.ts @@ -0,0 +1,68 @@ +import { useEffect, useRef, useState } from 'react'; + +/** + * Erkennt, ob nach einem Deploy eine neue Frontend-Version vorliegt, während + * der Nutzer die App offen hat (der einzige Fall, den Cache-Header NICHT + * abfangen können – eine laufende SPA holt sich nicht von selbst neuen Code). + * + * Prinzip: Die `index.html` wird server-seitig mit `no-store` ausgeliefert, + * ist also immer frisch. Sie referenziert die Vite-Hash-Assets + * (`/assets/index-.js`), deren Namen sich bei jedem Build ändern. Wir + * holen die index.html periodisch (und beim Zurückkehren zum Tab), extrahieren + * die Menge der referenzierten Assets als "Signatur" und vergleichen sie mit + * der beim App-Start geladenen. Ändert sie sich → neue Version deployt. + */ +function extractAssetSignature(html: string): string { + const matches = html.match(/\/assets\/[A-Za-z0-9._-]+/g) || []; + return [...new Set(matches)].sort().join('|'); +} + +async function fetchSignature(): Promise { + try { + const res = await fetch('/index.html', { cache: 'no-store' }); + if (!res.ok) return null; + const sig = extractAssetSignature(await res.text()); + return sig || null; + } catch { + // Netz-Aussetzer o.ä. – kein Alarm, nächster Intervall versucht es erneut. + return null; + } +} + +export function useVersionCheck(intervalMs = 5 * 60 * 1000): boolean { + const [updateAvailable, setUpdateAvailable] = useState(false); + const baseline = useRef(null); + + useEffect(() => { + // Kein Dev-Guard nötig: der Vite-Dev-Server liefert keine + // `/assets/`-Dateien aus → extractAssetSignature ist leer → + // fetchSignature gibt null zurück → check() bricht ab, kein Alarm. + // Im Prod-Build greift die Hash-Signatur wie gewünscht. + let cancelled = false; + + const check = async () => { + const sig = await fetchSignature(); + if (cancelled || !sig) return; + if (baseline.current === null) { + baseline.current = sig; // erster Lauf = Referenzstand + } else if (sig !== baseline.current) { + setUpdateAvailable(true); + } + }; + + check(); // sofort Baseline setzen + const id = window.setInterval(check, intervalMs); + const onVisible = () => { + if (document.visibilityState === 'visible') check(); + }; + document.addEventListener('visibilitychange', onVisible); + + return () => { + cancelled = true; + window.clearInterval(id); + document.removeEventListener('visibilitychange', onVisible); + }; + }, [intervalMs]); + + return updateAvailable; +}