diff --git a/src/App.tsx b/src/App.tsx index 79d2887f..79fa2f25 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( diff --git a/src/components/error/RouteErrorBoundary.tsx b/src/components/error/RouteErrorBoundary.tsx index 06471e7e..f247f0ed 100644 --- a/src/components/error/RouteErrorBoundary.tsx +++ b/src/components/error/RouteErrorBoundary.tsx @@ -71,6 +71,32 @@ export class RouteErrorBoundary extends Component { + 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 - {isChunkError ? 'New Version Available' : 'Something Went Wrong'} + {isChunkError ? 'App Update Required' : 'Something Went Wrong'} - - {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."} + + {isChunkError ? ( + <> +

The app has been updated with new features and improvements.

+

+ To continue, please clear your browser cache and reload: +

+
    +
  • Click "Clear Cache & Reload" below, or
  • +
  • Press Ctrl+Shift+R (Windows/Linux) or ⌘+Shift+R (Mac)
  • +
+ + ) : ( + "We encountered an unexpected error. This has been logged and we'll look into it." + )}
@@ -114,23 +151,35 @@ export class RouteErrorBoundary extends Component )} -
- - +
+ {isChunkError && ( + + )} +
+ + +

diff --git a/src/hooks/useAdminRoutePreload.ts b/src/hooks/useAdminRoutePreload.ts new file mode 100644 index 00000000..79d2b569 --- /dev/null +++ b/src/hooks/useAdminRoutePreload.ts @@ -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]); +} diff --git a/src/hooks/useVersionCheck.ts b/src/hooks/useVersionCheck.ts new file mode 100644 index 00000000..8a6469b3 --- /dev/null +++ b/src/hooks/useVersionCheck.ts @@ -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 }; +}