fix(web): Stale-Chunk-Fehler nach Deploy automatisch abfangen
ci / quality (push) Failing after 39s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 57s

"Failed to fetch dynamically imported module" trat auf, wenn ein offener Tab
nach einem Deploy auf alte (gehashte, inzwischen gelöschte) Route-Chunks zeigt.
Jetzt:
- lazyWithReload() lädt bei fehlgeschlagenem Lazy-Import einmal automatisch neu
  (holt frische index.html); sessionStorage-Guard verhindert Reload-Schleifen.
- vite:preloadError-Event wird ebenfalls abgefangen (modulepreload-Fehler).
- errorElement (RouteError) ersetzt die Dev-Default-Fehlerseite durch eine
  saubere "Neu laden"-Ansicht als Fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-06 17:15:01 +02:00
1 parent 02bd682fa5
commit 040dd62735
1 file changed
+71 -12
+71 -12
View File
@@ -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<T extends ComponentType>(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 (
<div className="mx-auto mt-16 max-w-md text-center">
<h1 className="text-lg font-semibold">Etwas ist schiefgelaufen.</h1>
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
Wahrscheinlich wurde gerade eine neue Version veröffentlicht. Lade die Seite neu.
</p>
<Button className="mt-4" onClick={() => window.location.reload()}>
Neu laden
</Button>
</div>
);
}
const router = createBrowserRouter([
{
path: '/login',
element: withSuspense(<Login />),
errorElement: <RouteError />,
},
{
path: '/',
element: <AppShell />,
errorElement: <RouteError />,
children: [
{ index: true, element: withSuspense(<Dashboard />) },
{ path: 'dashboard', element: withSuspense(<Dashboard />) },