mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:51:12 -05:00
Fix: Improve chunk load error handling
This commit is contained in:
@@ -20,6 +20,8 @@ import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
||||
import { handleError } from "@/lib/errorHandler";
|
||||
import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator";
|
||||
import { APIStatusBanner } from "@/components/ui/api-status-banner";
|
||||
import { useAdminRoutePreload } from "@/hooks/useAdminRoutePreload";
|
||||
import { useVersionCheck } from "@/hooks/useVersionCheck";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Core routes (eager-loaded for best UX)
|
||||
@@ -136,6 +138,12 @@ function AppContent(): React.JSX.Element {
|
||||
// Check if API status banner is visible to add padding
|
||||
const { isAPIReachable, isBannerDismissed } = useAPIConnectivity();
|
||||
const showBanner = !isAPIReachable && !isBannerDismissed;
|
||||
|
||||
// Preload admin routes for moderators/admins
|
||||
useAdminRoutePreload();
|
||||
|
||||
// Monitor for new deployments
|
||||
useVersionCheck();
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -71,6 +71,32 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
||||
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 = () => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
@@ -90,12 +116,23 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
||||
<AlertTriangle className="w-8 h-8 text-destructive" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">
|
||||
{isChunkError ? 'New Version Available' : 'Something Went Wrong'}
|
||||
{isChunkError ? 'App Update Required' : 'Something Went Wrong'}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
{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."}
|
||||
<CardDescription className="mt-2 space-y-2">
|
||||
{isChunkError ? (
|
||||
<>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -114,23 +151,35 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={this.handleReload}
|
||||
className="flex-1 gap-2"
|
||||
>
|
||||
<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 className="flex flex-col gap-2">
|
||||
{isChunkError && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={this.handleClearCacheAndReload}
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Clear Cache & Reload
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant={isChunkError ? "outline" : "default"}
|
||||
onClick={this.handleReload}
|
||||
className="flex-1 gap-2"
|
||||
>
|
||||
<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>
|
||||
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
|
||||
39
src/hooks/useAdminRoutePreload.ts
Normal file
39
src/hooks/useAdminRoutePreload.ts
Normal 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]);
|
||||
}
|
||||
76
src/hooks/useVersionCheck.ts
Normal file
76
src/hooks/useVersionCheck.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user