diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index 44497eda4..e8822bcbe 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -1,11 +1,9 @@ import AlphaFooter from '@/components/alpha/AlphaFooter'; import { AlphaNav } from '@/components/alpha/AlphaNav'; import DevToolbar from '@/components/dev/DevToolbar'; -import { DebugModeToggle } from '@/components/dev/DebugModeToggle'; import { ApiErrorBoundary } from '@/components/errors/ApiErrorBoundary'; import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary'; import { NotificationIntegration } from '@/components/errors/NotificationIntegration'; -import { ErrorAnalyticsDashboard } from '@/components/errors/ErrorAnalyticsDashboard'; import NotificationProvider from '@/components/notifications/NotificationProvider'; import { AuthProvider } from '@/lib/auth/AuthContext'; import { getAppMode } from '@/lib/mode'; @@ -100,8 +98,6 @@ export default async function RootLayout({ {process.env.NODE_ENV === 'development' && ( <> - - > )} @@ -149,8 +145,7 @@ export default async function RootLayout({ {/* Development Tools */} {process.env.NODE_ENV === 'development' && ( <> - - + > )} diff --git a/apps/website/components/dev/DevToolbar.tsx b/apps/website/components/dev/DevToolbar.tsx index 7fb0f1c33..192a5bb8e 100644 --- a/apps/website/components/dev/DevToolbar.tsx +++ b/apps/website/components/dev/DevToolbar.tsx @@ -3,11 +3,12 @@ import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useNotifications } from '@/components/notifications/NotificationProvider'; import type { NotificationVariant } from '@/components/notifications/notificationTypes'; -import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn, Play } from 'lucide-react'; +import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn, AlertTriangle } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor'; import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler'; +import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler'; // Import our new components import { Accordion } from './Accordion'; @@ -16,7 +17,6 @@ import { UrgencySection } from './sections/UrgencySection'; import { NotificationSendSection } from './sections/NotificationSendSection'; import { APIStatusSection } from './sections/APIStatusSection'; import { LoginSection } from './sections/LoginSection'; -import { ReplaySection } from './sections/ReplaySection'; // Import types import type { DemoNotificationType, DemoUrgency, LoginMode } from './types'; @@ -39,6 +39,9 @@ export default function DevToolbar() { const [circuitBreakers, setCircuitBreakers] = useState(() => CircuitBreakerRegistry.getInstance().getStatus()); const [checkingHealth, setCheckingHealth] = useState(false); + // Error Stats State + const [errorStats, setErrorStats] = useState({ total: 0, byType: {} as Record }); + // Accordion state - only one open at a time const [openAccordion, setOpenAccordion] = useState('notifications'); @@ -136,6 +139,28 @@ export default function DevToolbar() { }; }, []); + // Error Stats Effect + useEffect(() => { + const updateErrorStats = () => { + try { + const handler = getGlobalErrorHandler(); + const stats = handler.getStats(); + setErrorStats(stats); + } catch { + // Handler might not be initialized yet + setErrorStats({ total: 0, byType: {} }); + } + }; + + // Initial update + updateErrorStats(); + + // Poll for updates every 3 seconds + const interval = setInterval(updateErrorStats, 3000); + + return () => clearInterval(interval); + }, []); + // API Health Check Handler const handleApiHealthCheck = async () => { setCheckingHealth(true); @@ -412,15 +437,43 @@ export default function DevToolbar() { /> - {/* Replay Section - Accordion */} + {/* Error Stats Section - Accordion */} } - isOpen={openAccordion === 'replay'} - onToggle={() => setOpenAccordion(openAccordion === 'replay' ? null : 'replay')} + title="Error Stats" + icon={} + isOpen={openAccordion === 'errors'} + onToggle={() => setOpenAccordion(openAccordion === 'errors' ? null : 'errors')} > - + + + Total Errors + {errorStats.total} + + {Object.keys(errorStats.byType).length > 0 ? ( + + {Object.entries(errorStats.byType).map(([type, count]) => ( + + {type} + {count} + + ))} + + ) : ( + No errors yet + )} + { + const handler = getGlobalErrorHandler(); + handler.clearHistory(); + setErrorStats({ total: 0, byType: {} }); + }} + className="w-full p-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline text-xs" + > + Clear Error History + + + )} diff --git a/apps/website/lib/api/base/BaseApiClient.ts b/apps/website/lib/api/base/BaseApiClient.ts index 7225ced64..212a46d52 100644 --- a/apps/website/lib/api/base/BaseApiClient.ts +++ b/apps/website/lib/api/base/BaseApiClient.ts @@ -10,6 +10,7 @@ import { ErrorReporter } from '../../interfaces/ErrorReporter'; import { ApiError, ApiErrorType } from './ApiError'; import { RetryHandler, CircuitBreakerRegistry, DEFAULT_RETRY_CONFIG } from './RetryHandler'; import { ApiConnectionMonitor } from './ApiConnectionMonitor'; +import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger'; export interface BaseApiClientOptions { timeout?: number; @@ -214,6 +215,28 @@ export class BaseApiClient { } const startTime = Date.now(); + let requestId: string | undefined; + + // Log request start (only in development for maximum transparency) + if (process.env.NODE_ENV === 'development') { + try { + const apiLogger = getGlobalApiLogger(); + const headerObj: Record = {}; + if (typeof headers === 'object') { + Object.entries(headers).forEach(([key, value]) => { + headerObj[key] = value; + }); + } + requestId = apiLogger.logRequest( + endpoint, + method, + headerObj, + data + ); + } catch (e) { + // Silent fail - logger might not be initialized + } + } try { const response = await fetch(endpoint, config); @@ -235,6 +258,17 @@ export class BaseApiClient { circuitBreaker.recordFailure(); this.connectionMonitor.recordFailure(error); this.handleError(error); + + // Log error + if (process.env.NODE_ENV === 'development' && requestId) { + try { + const apiLogger = getGlobalApiLogger(); + apiLogger.logError(requestId, error, responseTime); + } catch (e) { + // Silent fail + } + } + throw error; } @@ -243,9 +277,31 @@ export class BaseApiClient { const text = await response.text(); if (!text) { + // Log empty response + if (process.env.NODE_ENV === 'development' && requestId) { + try { + const apiLogger = getGlobalApiLogger(); + apiLogger.logResponse(requestId, response, null, responseTime); + } catch (e) { + // Silent fail + } + } return null as T; } - return JSON.parse(text) as T; + + const parsedData = JSON.parse(text) as T; + + // Log successful response + if (process.env.NODE_ENV === 'development' && requestId) { + try { + const apiLogger = getGlobalApiLogger(); + apiLogger.logResponse(requestId, response, parsedData, responseTime); + } catch (e) { + // Silent fail + } + } + + return parsedData; } catch (error) { const responseTime = Date.now() - startTime; @@ -260,6 +316,16 @@ export class BaseApiClient { circuitBreaker.recordFailure(); this.connectionMonitor.recordFailure(apiError); this.handleError(apiError); + + // Log network error + if (process.env.NODE_ENV === 'development' && requestId) { + try { + const apiLogger = getGlobalApiLogger(); + apiLogger.logError(requestId, apiError, responseTime); + } catch (e) { + // Silent fail + } + } throw apiError; } diff --git a/apps/website/lib/infrastructure/GlobalErrorHandler.ts b/apps/website/lib/infrastructure/GlobalErrorHandler.ts index eb1adda00..4429e2366 100644 --- a/apps/website/lib/infrastructure/GlobalErrorHandler.ts +++ b/apps/website/lib/infrastructure/GlobalErrorHandler.ts @@ -117,10 +117,6 @@ export class GlobalErrorHandler { // Store in history this.addToHistory(error, enhancedContext); - // Show dev overlay if enabled - if (this.options.showDevOverlay) { - this.showDevOverlay(error, enhancedContext); - } // Report to external if enabled if (this.options.reportToExternal) { @@ -161,10 +157,6 @@ export class GlobalErrorHandler { // Store in history this.addToHistory(error, enhancedContext); - // Show dev overlay if enabled - if (this.options.showDevOverlay) { - this.showDevOverlay(error, enhancedContext); - } // Report to external if enabled if (this.options.reportToExternal) { @@ -200,10 +192,7 @@ export class GlobalErrorHandler { // Store in history this.addToHistory(error, enhancedContext); - // Show dev overlay if enabled - if (this.options.showDevOverlay) { - this.showDevOverlay(error, enhancedContext); - } + // No overlay - just enhanced console logging } }; } @@ -357,137 +346,6 @@ export class GlobalErrorHandler { this.logger.error(error.message, error, context); } - /** - * Show development overlay with error details - */ - private showDevOverlay(error: Error | ApiError, context: Record): void { - // Check if overlay already exists - const existingOverlay = document.getElementById('gridpilot-error-overlay'); - if (existingOverlay) { - // Update existing overlay - this.updateDevOverlay(existingOverlay, error, context); - return; - } - - // Create new overlay - const overlay = document.createElement('div'); - overlay.id = 'gridpilot-error-overlay'; - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.95); - color: #fff; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; - z-index: 999999; - overflow: auto; - padding: 20px; - border: 4px solid #ff4444; - box-shadow: 0 0 50px rgba(255, 68, 68, 0.5); - `; - - this.updateDevOverlay(overlay, error, context); - document.body.appendChild(overlay); - - // Add keyboard shortcut to dismiss - const dismissHandler = (e: KeyboardEvent) => { - if (e.key === 'Escape' || e.key === 'Enter') { - overlay.remove(); - document.removeEventListener('keydown', dismissHandler); - } - }; - document.addEventListener('keydown', dismissHandler); - } - - /** - * Update existing dev overlay - */ - private updateDevOverlay(overlay: HTMLElement, error: Error | ApiError, context: Record): void { - const isApiError = error instanceof ApiError; - const timestamp = new Date().toLocaleTimeString(); - - overlay.innerHTML = ` - - - - - 🚨 UNCAUGHT ERROR - DEVELOPMENT MODE - - ${timestamp} | Press ESC or ENTER to dismiss - - - CLOSE - - - - - - Error Information - - Type: ${isApiError ? error.type : error.name} - Message: ${error.message} - ${isApiError ? `Severity: ${error.getSeverity()}` : ''} - ${isApiError ? `Retryable: ${error.isRetryable()}` : ''} - ${isApiError ? `Connectivity: ${error.isConnectivityIssue()}` : ''} - - - - - Environment - - Mode: ${process.env.NODE_ENV} - App Mode: ${process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch'} - URL: ${window.location.href} - User Agent: ${navigator.userAgent} - - - - - ${isApiError && error.context ? ` - - API Context - ${JSON.stringify(error.context, null, 2)} - - ` : ''} - - - Stack Trace - ${context.enhancedStack || error.stack || 'No stack trace available'} - - - - Additional Context - ${JSON.stringify(context, null, 2)} - - - - Quick Actions - - - 📋 Copy Error Details - - - 🔄 Reload Page - - - 📝 Log to Console - - - - - - 💡 Tip: This overlay only appears in development mode. In production, errors are logged silently and handled gracefully. - - - `; - } /** * Report error to external services @@ -581,10 +439,8 @@ export class GlobalErrorHandler { this.logErrorWithMaximumDetail(error, context); this.addToHistory(error, context); + // Auto-capture for replay in development if (this.options.showDevOverlay) { - this.showDevOverlay(error, context); - - // Auto-capture for replay const replaySystem = getGlobalReplaySystem(); replaySystem.autoCapture(error, context); } @@ -606,12 +462,6 @@ export class GlobalErrorHandler { console.error = (console as any)._originalError; } - // Remove overlay if exists - const overlay = document.getElementById('gridpilot-error-overlay'); - if (overlay) { - overlay.remove(); - } - this.isInitialized = false; if (this.options.verboseLogging) { diff --git a/apps/website/lib/infrastructure/logging/ConsoleErrorReporter.ts b/apps/website/lib/infrastructure/logging/ConsoleErrorReporter.ts index 7a82e611f..5c8052fd0 100644 --- a/apps/website/lib/infrastructure/logging/ConsoleErrorReporter.ts +++ b/apps/website/lib/infrastructure/logging/ConsoleErrorReporter.ts @@ -1,8 +1,33 @@ import { ErrorReporter } from '../../interfaces/ErrorReporter'; +import { getGlobalErrorHandler } from '../GlobalErrorHandler'; export class ConsoleErrorReporter implements ErrorReporter { report(error: Error, context?: unknown): void { const timestamp = new Date().toISOString(); - console.error(`[${timestamp}] Error reported:`, error.message, { error, context }); + + // Use enhanced global handler if available + try { + const globalHandler = getGlobalErrorHandler(); + const enhancedContext: Record = { + source: 'legacy_reporter', + timestamp, + }; + if (typeof context === 'object' && context !== null) { + Object.assign(enhancedContext, context); + } + globalHandler.report(error, enhancedContext); + } catch { + // Fallback to basic logging if global handler not available + console.error(`[${timestamp}] Error reported:`, error.message, { error, context }); + + // Also log to console with enhanced format + if (process.env.NODE_ENV === 'development') { + console.groupCollapsed(`%c[LEGACY ERROR] ${error.name}: ${error.message}`, 'color: #ff6600; font-weight: bold;'); + console.log('Error:', error); + console.log('Context:', context); + console.log('Stack:', error.stack); + console.groupEnd(); + } + } } } \ No newline at end of file diff --git a/apps/website/lib/infrastructure/logging/ConsoleLogger.ts b/apps/website/lib/infrastructure/logging/ConsoleLogger.ts index c601a5fab..cd65b3d14 100644 --- a/apps/website/lib/infrastructure/logging/ConsoleLogger.ts +++ b/apps/website/lib/infrastructure/logging/ConsoleLogger.ts @@ -8,11 +8,15 @@ export class ConsoleLogger implements Logger { } debug(message: string, context?: unknown): void { - console.debug(this.formatMessage('debug', message, context)); + if (process.env.NODE_ENV === 'development') { + console.debug(this.formatMessage('debug', message, context)); + } } info(message: string, context?: unknown): void { - console.info(this.formatMessage('info', message, context)); + if (process.env.NODE_ENV === 'development') { + console.info(this.formatMessage('info', message, context)); + } } warn(message: string, context?: unknown): void { @@ -22,5 +26,14 @@ export class ConsoleLogger implements Logger { error(message: string, error?: Error, context?: unknown): void { const errorStr = error ? ` | Error: ${error.message}` : ''; console.error(this.formatMessage('error', message, context) + errorStr); + + // In development, also show enhanced error info + if (process.env.NODE_ENV === 'development' && error) { + console.groupCollapsed(`%c[ERROR DETAIL] ${message}`, 'color: #ff4444; font-weight: bold;'); + console.log('Error Object:', error); + console.log('Stack Trace:', error.stack); + console.log('Context:', context); + console.groupEnd(); + } } } \ No newline at end of file diff --git a/apps/website/lib/mode.ts b/apps/website/lib/mode.ts index 8d64e0e01..8ce0afe4c 100644 --- a/apps/website/lib/mode.ts +++ b/apps/website/lib/mode.ts @@ -64,7 +64,17 @@ export function isAlpha(): boolean { */ export function getPublicRoutes(): readonly string[] { return [ + // Core public pages '/', + + // Public content routes (leagues, drivers, teams, leaderboards, races) + '/leagues', + '/drivers', + '/teams', + '/leaderboards', + '/races', + + // Auth routes '/api/signup', '/api/auth/signup', '/api/auth/login', @@ -85,8 +95,27 @@ export function getPublicRoutes(): readonly string[] { /** * Check if a route is public (accessible in all modes) + * Supports both exact matches and prefix matches for nested routes */ export function isPublicRoute(pathname: string): boolean { const publicRoutes = getPublicRoutes(); - return publicRoutes.includes(pathname); + + // Check exact match first + if (publicRoutes.includes(pathname)) { + return true; + } + + // Check prefix matches for nested routes + // e.g., '/leagues' should match '/leagues/123', '/leagues/create', etc. + const publicPrefixes = [ + '/leagues', + '/drivers', + '/teams', + '/leaderboards', + '/races', + ]; + + return publicPrefixes.some(prefix => + pathname === prefix || pathname.startsWith(prefix + '/') + ); } \ No newline at end of file diff --git a/apps/website/lib/services/ServiceFactory.ts b/apps/website/lib/services/ServiceFactory.ts index 16f581f78..b17a5a99c 100644 --- a/apps/website/lib/services/ServiceFactory.ts +++ b/apps/website/lib/services/ServiceFactory.ts @@ -14,7 +14,7 @@ import { ProtestsApiClient } from '../api/protests/ProtestsApiClient'; import { PenaltiesApiClient } from '../api/penalties/PenaltiesApiClient'; import { getWebsiteApiBaseUrl } from '../config/apiBaseUrl'; import { PenaltyService } from './penalties/PenaltyService'; -import { ConsoleErrorReporter } from '../infrastructure/logging/ConsoleErrorReporter'; +import { EnhancedErrorReporter } from '../infrastructure/EnhancedErrorReporter'; import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger'; import { LandingService } from './landing/LandingService'; @@ -55,7 +55,11 @@ import { OnboardingService } from './onboarding/OnboardingService'; * Services now directly instantiate View Models instead of using Presenters. */ export class ServiceFactory { - private readonly errorReporter = new ConsoleErrorReporter(); + private readonly errorReporter = new EnhancedErrorReporter(new ConsoleLogger(), { + showUserNotifications: true, + logToConsole: true, + reportToExternal: process.env.NODE_ENV === 'production', + }); private readonly logger = new ConsoleLogger(); private readonly apiClients: {
${JSON.stringify(error.context, null, 2)}
${context.enhancedStack || error.stack || 'No stack trace available'}
${JSON.stringify(context, null, 2)}