mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -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 { 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 />
|
||||||
|
|||||||
@@ -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,9 +151,20 @@ 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">
|
||||||
|
{isChunkError && (
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
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}
|
onClick={this.handleReload}
|
||||||
className="flex-1 gap-2"
|
className="flex-1 gap-2"
|
||||||
>
|
>
|
||||||
@@ -132,6 +180,7 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
|||||||
Go Home
|
Go Home
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-center text-muted-foreground">
|
<p className="text-xs text-center text-muted-foreground">
|
||||||
If this problem persists, please contact support
|
If this problem persists, please contact support
|
||||||
|
|||||||
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