diff --git a/apps/web/src/lib/router.tsx b/apps/web/src/lib/router.tsx index 7f2778e..0969d1e 100644 --- a/apps/web/src/lib/router.tsx +++ b/apps/web/src/lib/router.tsx @@ -1,24 +1,67 @@ import { createBrowserRouter, RouterProvider } from 'react-router'; -import { lazy, Suspense } from 'react'; +import { lazy, Suspense, type ComponentType } from 'react'; import { AppShell } from '../components/layout/AppShell'; import { SkeletonCard } from '../components/ui/SkeletonCard'; +import { Button } from '../components/ui/Button'; -const Dashboard = lazy(() => import('../routes/dashboard').then((m) => ({ default: m.DashboardRoute }))); -const Heute = lazy(() => - import('../routes/heute').then((m) => ({ default: m.HeuteRoute })), +// Nach einem Deploy zeigen bereits offene Tabs auf alte, inzwischen gelöschte +// Chunk-Dateien (gehashte Dateinamen). Der nächste Lazy-Import schlägt dann fehl +// („Failed to fetch dynamically imported module"). Ein einmaliger Reload holt die +// frische index.html mit den neuen Hashes. sessionStorage-Guard verhindert eine +// Reload-Schleife, falls der Fehler eine andere Ursache hat (z. B. offline). +const RELOAD_KEY = 'ebw:chunk-reload'; + +function reloadOnceForStaleChunk(): boolean { + const last = Number(sessionStorage.getItem(RELOAD_KEY) ?? '0'); + if (Date.now() - last > 10_000) { + sessionStorage.setItem(RELOAD_KEY, String(Date.now())); + window.location.reload(); + return true; + } + return false; +} + +function lazyWithReload(factory: () => Promise<{ default: T }>) { + return lazy(() => + factory().catch((err: unknown) => { + if (reloadOnceForStaleChunk()) { + // Hängt bis der Reload greift — vermeidet ein kurzes Aufblitzen des Fehlers. + return new Promise<{ default: T }>(() => {}); + } + throw err; + }), + ); +} + +// Vite feuert dieses Event bei fehlgeschlagenem modulepreload (gleiche Ursache). +if (typeof window !== 'undefined') { + window.addEventListener('vite:preloadError', (e) => { + if (reloadOnceForStaleChunk()) e.preventDefault(); + }); +} + +const Dashboard = lazyWithReload(() => + import('../routes/dashboard').then((m) => ({ default: m.DashboardRoute })), ); -const Reflexion = lazy(() => +const Heute = lazyWithReload(() => import('../routes/heute').then((m) => ({ default: m.HeuteRoute }))); +const Reflexion = lazyWithReload(() => import('../routes/reflexion').then((m) => ({ default: m.ReflexionRoute })), ); -const Bereich = lazy(() => import('../routes/bereich').then((m) => ({ default: m.BereichRoute }))); -const Projekt = lazy(() => import('../routes/projekt').then((m) => ({ default: m.ProjektRoute }))); -const Leads = lazy(() => import('../routes/leads').then((m) => ({ default: m.LeadsRoute }))); -const Lead = lazy(() => import('../routes/lead').then((m) => ({ default: m.LeadRoute }))); -const Archiv = lazy(() => import('../routes/archiv').then((m) => ({ default: m.ArchivRoute }))); -const NotFound = lazy(() => +const Bereich = lazyWithReload(() => + import('../routes/bereich').then((m) => ({ default: m.BereichRoute })), +); +const Projekt = lazyWithReload(() => + import('../routes/projekt').then((m) => ({ default: m.ProjektRoute })), +); +const Leads = lazyWithReload(() => import('../routes/leads').then((m) => ({ default: m.LeadsRoute }))); +const Lead = lazyWithReload(() => import('../routes/lead').then((m) => ({ default: m.LeadRoute }))); +const Archiv = lazyWithReload(() => + import('../routes/archiv').then((m) => ({ default: m.ArchivRoute })), +); +const NotFound = lazyWithReload(() => import('../routes/not-found').then((m) => ({ default: m.NotFoundRoute })), ); -const Login = lazy(() => import('../routes/login').then((m) => ({ default: m.LoginRoute }))); +const Login = lazyWithReload(() => import('../routes/login').then((m) => ({ default: m.LoginRoute }))); function withSuspense(node: React.ReactNode) { return ( @@ -26,14 +69,30 @@ function withSuspense(node: React.ReactNode) { ); } +function RouteError() { + return ( +
+

Etwas ist schiefgelaufen.

+

+ Wahrscheinlich wurde gerade eine neue Version veröffentlicht. Lade die Seite neu. +

+ +
+ ); +} + const router = createBrowserRouter([ { path: '/login', element: withSuspense(), + errorElement: , }, { path: '/', element: , + errorElement: , children: [ { index: true, element: withSuspense() }, { path: 'dashboard', element: withSuspense() },