Fix: Improve chunk load error handling

This commit is contained in:
gpt-engineer-app[bot]
2025-11-05 21:23:09 +00:00
parent 7476fbd5da
commit fa3dfcfdee
4 changed files with 194 additions and 22 deletions

View File

@@ -20,6 +20,8 @@ import { breadcrumb } from "@/lib/errorBreadcrumbs";
import { handleError } from "@/lib/errorHandler"; import { handleError } from "@/lib/errorHandler";
import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator"; import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator";
import { APIStatusBanner } from "@/components/ui/api-status-banner"; import { APIStatusBanner } from "@/components/ui/api-status-banner";
import { useAdminRoutePreload } from "@/hooks/useAdminRoutePreload";
import { useVersionCheck } from "@/hooks/useVersionCheck";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// Core routes (eager-loaded for best UX) // Core routes (eager-loaded for best UX)
@@ -137,6 +139,12 @@ function AppContent(): React.JSX.Element {
const { isAPIReachable, isBannerDismissed } = useAPIConnectivity(); const { isAPIReachable, isBannerDismissed } = useAPIConnectivity();
const showBanner = !isAPIReachable && !isBannerDismissed; const showBanner = !isAPIReachable && !isBannerDismissed;
// Preload admin routes for moderators/admins
useAdminRoutePreload();
// Monitor for new deployments
useVersionCheck();
return ( return (
<TooltipProvider> <TooltipProvider>
<APIStatusBanner /> <APIStatusBanner />

View File

@@ -71,6 +71,32 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
window.location.reload(); window.location.reload();
}; };
handleClearCacheAndReload = async () => {
try {
// Clear all caches
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
}
// Unregister service workers
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(reg => reg.unregister()));
}
// Clear session storage chunk reload flag
sessionStorage.removeItem('chunk-load-reload');
// Force reload bypassing cache
window.location.reload();
} catch (error) {
// Fallback to regular reload if cache clearing fails
console.error('Failed to clear cache:', error);
window.location.reload();
}
};
handleGoHome = () => { handleGoHome = () => {
window.location.href = '/'; window.location.href = '/';
}; };
@@ -90,12 +116,23 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
<AlertTriangle className="w-8 h-8 text-destructive" /> <AlertTriangle className="w-8 h-8 text-destructive" />
</div> </div>
<CardTitle className="text-2xl"> <CardTitle className="text-2xl">
{isChunkError ? 'New Version Available' : 'Something Went Wrong'} {isChunkError ? 'App Update Required' : 'Something Went Wrong'}
</CardTitle> </CardTitle>
<CardDescription className="mt-2"> <CardDescription className="mt-2 space-y-2">
{isChunkError {isChunkError ? (
? "The app has been updated. Please reload the page to get the latest version." <>
: "We encountered an unexpected error. This has been logged and we'll look into it."} <p>The app has been updated with new features and improvements.</p>
<p className="text-sm font-medium">
To continue, please clear your browser cache and reload:
</p>
<ul className="text-sm list-disc list-inside space-y-1 ml-2">
<li>Click "Clear Cache & Reload" below, or</li>
<li>Press <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Ctrl+Shift+R</kbd> (Windows/Linux) or <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">+Shift+R</kbd> (Mac)</li>
</ul>
</>
) : (
"We encountered an unexpected error. This has been logged and we'll look into it."
)}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@@ -114,23 +151,35 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
</div> </div>
)} )}
<div className="flex flex-col sm:flex-row gap-2"> <div className="flex flex-col gap-2">
<Button {isChunkError && (
variant="default" <Button
onClick={this.handleReload} variant="default"
className="flex-1 gap-2" onClick={this.handleClearCacheAndReload}
> className="w-full gap-2"
<RefreshCw className="w-4 h-4" /> >
Reload Page <RefreshCw className="w-4 h-4" />
</Button> Clear Cache & Reload
<Button </Button>
variant="outline" )}
onClick={this.handleGoHome} <div className="flex flex-col sm:flex-row gap-2">
className="flex-1 gap-2" <Button
> variant={isChunkError ? "outline" : "default"}
<Home className="w-4 h-4" /> onClick={this.handleReload}
Go Home className="flex-1 gap-2"
</Button> >
<RefreshCw className="w-4 h-4" />
Reload Page
</Button>
<Button
variant="outline"
onClick={this.handleGoHome}
className="flex-1 gap-2"
>
<Home className="w-4 h-4" />
Go Home
</Button>
</div>
</div> </div>
<p className="text-xs text-center text-muted-foreground"> <p className="text-xs text-center text-muted-foreground">

View File

@@ -0,0 +1,39 @@
import { useEffect } from 'react';
import { useAuth } from './useAuth';
import { useUserRole } from './useUserRole';
/**
* Preloads admin route chunks for authenticated moderators/admins
* This reduces chunk load failures by warming up the browser cache
*/
export function useAdminRoutePreload() {
const { user } = useAuth();
const { isModerator, isAdmin } = useUserRole();
useEffect(() => {
// Only preload if user has admin access
if (!user || (!isModerator && !isAdmin)) {
return;
}
// Preload admin chunks after a short delay to avoid blocking initial page load
const preloadTimer = setTimeout(() => {
// Preload critical admin routes
const adminRoutes = [
() => import('../pages/AdminDashboard'),
() => import('../pages/AdminModeration'),
() => import('../pages/AdminReports'),
];
// Start preloading (but don't await - let it happen in background)
adminRoutes.forEach(route => {
route().catch(err => {
// Silently fail - preloading is a performance optimization
console.debug('Admin route preload failed:', err);
});
});
}, 2000); // Wait 2 seconds after auth to avoid blocking initial render
return () => clearTimeout(preloadTimer);
}, [user, isModerator, isAdmin]);
}

View File

@@ -0,0 +1,76 @@
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
// App version - automatically updated during build
const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'dev';
const VERSION_CHECK_INTERVAL = 5 * 60 * 1000; // Check every 5 minutes
/**
* Monitors for new app deployments and prompts user to refresh
*/
export function useVersionCheck() {
const [newVersionAvailable, setNewVersionAvailable] = useState(false);
useEffect(() => {
// Don't run in development
if (import.meta.env.DEV) {
return;
}
const checkVersion = async () => {
try {
// Fetch the current index.html with cache bypass
const response = await fetch('/', {
method: 'HEAD',
cache: 'no-cache',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
},
});
// Check ETag or Last-Modified to detect changes
const etag = response.headers.get('ETag');
const lastModified = response.headers.get('Last-Modified');
const currentFingerprint = `${etag}-${lastModified}`;
const storedFingerprint = sessionStorage.getItem('app-version-fingerprint');
if (storedFingerprint && storedFingerprint !== currentFingerprint) {
// New version detected
setNewVersionAvailable(true);
toast.info('New version available', {
description: 'A new version of ThrillWiki is available. Please refresh to update.',
duration: 30000, // Show for 30 seconds
action: {
label: 'Refresh Now',
onClick: () => window.location.reload(),
},
});
}
// Store current fingerprint
if (!storedFingerprint) {
sessionStorage.setItem('app-version-fingerprint', currentFingerprint);
}
} catch (error) {
// Silently fail - version check is non-critical
console.debug('Version check failed:', error);
}
};
// Initial check after 1 minute (give time for user to settle in)
const initialTimer = setTimeout(checkVersion, 60000);
// Then check periodically
const interval = setInterval(checkVersion, VERSION_CHECK_INTERVAL);
return () => {
clearTimeout(initialTimer);
clearInterval(interval);
};
}, []);
return { newVersionAvailable };
}