dev experience

This commit is contained in:
2026-01-01 17:43:38 +01:00
parent df7e5db5ba
commit 9958053462
8 changed files with 208 additions and 173 deletions

View File

@@ -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' && (
<>
<DevToolbar />
<DebugModeToggle />
<ErrorAnalyticsDashboard refreshInterval={5000} />
</>
)}
</EnhancedErrorBoundary>
@@ -149,8 +145,7 @@ export default async function RootLayout({
{/* Development Tools */}
{process.env.NODE_ENV === 'development' && (
<>
<DebugModeToggle />
<ErrorAnalyticsDashboard refreshInterval={5000} />
<DevToolbar />
</>
)}
</EnhancedErrorBoundary>

View File

@@ -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<string, number> });
// Accordion state - only one open at a time
const [openAccordion, setOpenAccordion] = useState<string | null>('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() {
/>
</Accordion>
{/* Replay Section - Accordion */}
{/* Error Stats Section - Accordion */}
<Accordion
title="Error Replay"
icon={<Play className="w-4 h-4 text-gray-400" />}
isOpen={openAccordion === 'replay'}
onToggle={() => setOpenAccordion(openAccordion === 'replay' ? null : 'replay')}
title="Error Stats"
icon={<AlertTriangle className="w-4 h-4 text-gray-400" />}
isOpen={openAccordion === 'errors'}
onToggle={() => setOpenAccordion(openAccordion === 'errors' ? null : 'errors')}
>
<ReplaySection />
<div className="space-y-2 text-xs">
<div className="flex justify-between items-center p-2 bg-iron-gray/30 rounded">
<span className="text-gray-400">Total Errors</span>
<span className="font-mono font-bold text-red-400">{errorStats.total}</span>
</div>
{Object.keys(errorStats.byType).length > 0 ? (
<div className="space-y-1">
{Object.entries(errorStats.byType).map(([type, count]) => (
<div key={type} className="flex justify-between items-center p-1.5 bg-deep-graphite rounded">
<span className="text-gray-300">{type}</span>
<span className="font-mono text-yellow-400">{count}</span>
</div>
))}
</div>
) : (
<div className="text-center text-gray-500 py-2">No errors yet</div>
)}
<button
onClick={() => {
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
</button>
</div>
</Accordion>
</div>
)}

View File

@@ -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<string, string> = {};
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;
}

View File

@@ -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<string, unknown>): 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<string, unknown>): void {
const isApiError = error instanceof ApiError;
const timestamp = new Date().toLocaleTimeString();
overlay.innerHTML = `
<div style="max-width: 1200px; margin: 0 auto;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
<div>
<h1 style="color: #ff4444; margin: 0 0 10px 0; font-size: 24px;">
🚨 UNCAUGHT ERROR - DEVELOPMENT MODE
</h1>
<div style="color: #888;">${timestamp} | Press ESC or ENTER to dismiss</div>
</div>
<button onclick="this.parentElement.parentElement.remove()"
style="background: #ff4444; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; font-weight: bold;">
CLOSE
</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333;">
<h3 style="color: #ffaa00; margin-top: 0;">Error Information</h3>
<div style="line-height: 1.6;">
<div><strong>Type:</strong> <span style="color: #ff4444;">${isApiError ? error.type : error.name}</span></div>
<div><strong>Message:</strong> ${error.message}</div>
${isApiError ? `<div><strong>Severity:</strong> ${error.getSeverity()}</div>` : ''}
${isApiError ? `<div><strong>Retryable:</strong> ${error.isRetryable()}</div>` : ''}
${isApiError ? `<div><strong>Connectivity:</strong> ${error.isConnectivityIssue()}</div>` : ''}
</div>
</div>
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333;">
<h3 style="color: #00aaff; margin-top: 0;">Environment</h3>
<div style="line-height: 1.6;">
<div><strong>Mode:</strong> ${process.env.NODE_ENV}</div>
<div><strong>App Mode:</strong> ${process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch'}</div>
<div><strong>URL:</strong> ${window.location.href}</div>
<div><strong>User Agent:</strong> ${navigator.userAgent}</div>
</div>
</div>
</div>
${isApiError && error.context ? `
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 20px;">
<h3 style="color: #00ff88; margin-top: 0;">API Context</h3>
<pre style="background: #000; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 0;">${JSON.stringify(error.context, null, 2)}</pre>
</div>
` : ''}
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 20px;">
<h3 style="color: #ff4444; margin-top: 0;">Stack Trace</h3>
<pre style="background: #000; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 0; white-space: pre-wrap;">${context.enhancedStack || error.stack || 'No stack trace available'}</pre>
</div>
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 20px;">
<h3 style="color: #ffaa00; margin-top: 0;">Additional Context</h3>
<pre style="background: #000; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 0;">${JSON.stringify(context, null, 2)}</pre>
</div>
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333;">
<h3 style="color: #00aaff; margin-top: 0;">Quick Actions</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button onclick="navigator.clipboard.writeText(\`${error.message}\n\nStack:\n${error.stack}\n\nContext:\n${JSON.stringify(context, null, 2)}\`)"
style="background: #0066cc; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px;">
📋 Copy Error Details
</button>
<button onclick="window.location.reload()"
style="background: #cc6600; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px;">
🔄 Reload Page
</button>
<button onclick="console.clear(); console.log('Error details:', ${JSON.stringify({ error: error.message, stack: error.stack, context }).replace(/"/g, '"')})"
style="background: #6600cc; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px;">
📝 Log to Console
</button>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: #222; border-radius: 4px; border-left: 4px solid #ffaa00;">
<strong>💡 Tip:</strong> This overlay only appears in development mode. In production, errors are logged silently and handled gracefully.
</div>
</div>
`;
}
/**
* 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) {

View File

@@ -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<string, unknown> = {
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();
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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 + '/')
);
}

View File

@@ -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: {