dev experience
This commit is contained in:
@@ -1,11 +1,9 @@
|
|||||||
import AlphaFooter from '@/components/alpha/AlphaFooter';
|
import AlphaFooter from '@/components/alpha/AlphaFooter';
|
||||||
import { AlphaNav } from '@/components/alpha/AlphaNav';
|
import { AlphaNav } from '@/components/alpha/AlphaNav';
|
||||||
import DevToolbar from '@/components/dev/DevToolbar';
|
import DevToolbar from '@/components/dev/DevToolbar';
|
||||||
import { DebugModeToggle } from '@/components/dev/DebugModeToggle';
|
|
||||||
import { ApiErrorBoundary } from '@/components/errors/ApiErrorBoundary';
|
import { ApiErrorBoundary } from '@/components/errors/ApiErrorBoundary';
|
||||||
import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary';
|
import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary';
|
||||||
import { NotificationIntegration } from '@/components/errors/NotificationIntegration';
|
import { NotificationIntegration } from '@/components/errors/NotificationIntegration';
|
||||||
import { ErrorAnalyticsDashboard } from '@/components/errors/ErrorAnalyticsDashboard';
|
|
||||||
import NotificationProvider from '@/components/notifications/NotificationProvider';
|
import NotificationProvider from '@/components/notifications/NotificationProvider';
|
||||||
import { AuthProvider } from '@/lib/auth/AuthContext';
|
import { AuthProvider } from '@/lib/auth/AuthContext';
|
||||||
import { getAppMode } from '@/lib/mode';
|
import { getAppMode } from '@/lib/mode';
|
||||||
@@ -100,8 +98,6 @@ export default async function RootLayout({
|
|||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<>
|
<>
|
||||||
<DevToolbar />
|
<DevToolbar />
|
||||||
<DebugModeToggle />
|
|
||||||
<ErrorAnalyticsDashboard refreshInterval={5000} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</EnhancedErrorBoundary>
|
</EnhancedErrorBoundary>
|
||||||
@@ -149,8 +145,7 @@ export default async function RootLayout({
|
|||||||
{/* Development Tools */}
|
{/* Development Tools */}
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<>
|
<>
|
||||||
<DebugModeToggle />
|
<DevToolbar />
|
||||||
<ErrorAnalyticsDashboard refreshInterval={5000} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</EnhancedErrorBoundary>
|
</EnhancedErrorBoundary>
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||||
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
||||||
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
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 { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
||||||
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
|
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
|
||||||
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||||
|
|
||||||
// Import our new components
|
// Import our new components
|
||||||
import { Accordion } from './Accordion';
|
import { Accordion } from './Accordion';
|
||||||
@@ -16,7 +17,6 @@ import { UrgencySection } from './sections/UrgencySection';
|
|||||||
import { NotificationSendSection } from './sections/NotificationSendSection';
|
import { NotificationSendSection } from './sections/NotificationSendSection';
|
||||||
import { APIStatusSection } from './sections/APIStatusSection';
|
import { APIStatusSection } from './sections/APIStatusSection';
|
||||||
import { LoginSection } from './sections/LoginSection';
|
import { LoginSection } from './sections/LoginSection';
|
||||||
import { ReplaySection } from './sections/ReplaySection';
|
|
||||||
|
|
||||||
// Import types
|
// Import types
|
||||||
import type { DemoNotificationType, DemoUrgency, LoginMode } from './types';
|
import type { DemoNotificationType, DemoUrgency, LoginMode } from './types';
|
||||||
@@ -39,6 +39,9 @@ export default function DevToolbar() {
|
|||||||
const [circuitBreakers, setCircuitBreakers] = useState(() => CircuitBreakerRegistry.getInstance().getStatus());
|
const [circuitBreakers, setCircuitBreakers] = useState(() => CircuitBreakerRegistry.getInstance().getStatus());
|
||||||
const [checkingHealth, setCheckingHealth] = useState(false);
|
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
|
// Accordion state - only one open at a time
|
||||||
const [openAccordion, setOpenAccordion] = useState<string | null>('notifications');
|
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
|
// API Health Check Handler
|
||||||
const handleApiHealthCheck = async () => {
|
const handleApiHealthCheck = async () => {
|
||||||
setCheckingHealth(true);
|
setCheckingHealth(true);
|
||||||
@@ -412,15 +437,43 @@ export default function DevToolbar() {
|
|||||||
/>
|
/>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
{/* Replay Section - Accordion */}
|
{/* Error Stats Section - Accordion */}
|
||||||
<Accordion
|
<Accordion
|
||||||
title="Error Replay"
|
title="Error Stats"
|
||||||
icon={<Play className="w-4 h-4 text-gray-400" />}
|
icon={<AlertTriangle className="w-4 h-4 text-gray-400" />}
|
||||||
isOpen={openAccordion === 'replay'}
|
isOpen={openAccordion === 'errors'}
|
||||||
onToggle={() => setOpenAccordion(openAccordion === 'replay' ? null : 'replay')}
|
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>
|
</Accordion>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ErrorReporter } from '../../interfaces/ErrorReporter';
|
|||||||
import { ApiError, ApiErrorType } from './ApiError';
|
import { ApiError, ApiErrorType } from './ApiError';
|
||||||
import { RetryHandler, CircuitBreakerRegistry, DEFAULT_RETRY_CONFIG } from './RetryHandler';
|
import { RetryHandler, CircuitBreakerRegistry, DEFAULT_RETRY_CONFIG } from './RetryHandler';
|
||||||
import { ApiConnectionMonitor } from './ApiConnectionMonitor';
|
import { ApiConnectionMonitor } from './ApiConnectionMonitor';
|
||||||
|
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
||||||
|
|
||||||
export interface BaseApiClientOptions {
|
export interface BaseApiClientOptions {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
@@ -214,6 +215,28 @@ export class BaseApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
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 {
|
try {
|
||||||
const response = await fetch(endpoint, config);
|
const response = await fetch(endpoint, config);
|
||||||
@@ -235,6 +258,17 @@ export class BaseApiClient {
|
|||||||
circuitBreaker.recordFailure();
|
circuitBreaker.recordFailure();
|
||||||
this.connectionMonitor.recordFailure(error);
|
this.connectionMonitor.recordFailure(error);
|
||||||
this.handleError(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;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +277,31 @@ export class BaseApiClient {
|
|||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (!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 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) {
|
} catch (error) {
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
@@ -260,6 +316,16 @@ export class BaseApiClient {
|
|||||||
circuitBreaker.recordFailure();
|
circuitBreaker.recordFailure();
|
||||||
this.connectionMonitor.recordFailure(apiError);
|
this.connectionMonitor.recordFailure(apiError);
|
||||||
this.handleError(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;
|
throw apiError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,10 +117,6 @@ export class GlobalErrorHandler {
|
|||||||
// Store in history
|
// Store in history
|
||||||
this.addToHistory(error, enhancedContext);
|
this.addToHistory(error, enhancedContext);
|
||||||
|
|
||||||
// Show dev overlay if enabled
|
|
||||||
if (this.options.showDevOverlay) {
|
|
||||||
this.showDevOverlay(error, enhancedContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report to external if enabled
|
// Report to external if enabled
|
||||||
if (this.options.reportToExternal) {
|
if (this.options.reportToExternal) {
|
||||||
@@ -161,10 +157,6 @@ export class GlobalErrorHandler {
|
|||||||
// Store in history
|
// Store in history
|
||||||
this.addToHistory(error, enhancedContext);
|
this.addToHistory(error, enhancedContext);
|
||||||
|
|
||||||
// Show dev overlay if enabled
|
|
||||||
if (this.options.showDevOverlay) {
|
|
||||||
this.showDevOverlay(error, enhancedContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report to external if enabled
|
// Report to external if enabled
|
||||||
if (this.options.reportToExternal) {
|
if (this.options.reportToExternal) {
|
||||||
@@ -200,10 +192,7 @@ export class GlobalErrorHandler {
|
|||||||
// Store in history
|
// Store in history
|
||||||
this.addToHistory(error, enhancedContext);
|
this.addToHistory(error, enhancedContext);
|
||||||
|
|
||||||
// Show dev overlay if enabled
|
// No overlay - just enhanced console logging
|
||||||
if (this.options.showDevOverlay) {
|
|
||||||
this.showDevOverlay(error, enhancedContext);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -357,137 +346,6 @@ export class GlobalErrorHandler {
|
|||||||
this.logger.error(error.message, error, context);
|
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
|
* Report error to external services
|
||||||
@@ -581,10 +439,8 @@ export class GlobalErrorHandler {
|
|||||||
this.logErrorWithMaximumDetail(error, context);
|
this.logErrorWithMaximumDetail(error, context);
|
||||||
this.addToHistory(error, context);
|
this.addToHistory(error, context);
|
||||||
|
|
||||||
|
// Auto-capture for replay in development
|
||||||
if (this.options.showDevOverlay) {
|
if (this.options.showDevOverlay) {
|
||||||
this.showDevOverlay(error, context);
|
|
||||||
|
|
||||||
// Auto-capture for replay
|
|
||||||
const replaySystem = getGlobalReplaySystem();
|
const replaySystem = getGlobalReplaySystem();
|
||||||
replaySystem.autoCapture(error, context);
|
replaySystem.autoCapture(error, context);
|
||||||
}
|
}
|
||||||
@@ -606,12 +462,6 @@ export class GlobalErrorHandler {
|
|||||||
console.error = (console as any)._originalError;
|
console.error = (console as any)._originalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove overlay if exists
|
|
||||||
const overlay = document.getElementById('gridpilot-error-overlay');
|
|
||||||
if (overlay) {
|
|
||||||
overlay.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
if (this.options.verboseLogging) {
|
if (this.options.verboseLogging) {
|
||||||
|
|||||||
@@ -1,8 +1,33 @@
|
|||||||
import { ErrorReporter } from '../../interfaces/ErrorReporter';
|
import { ErrorReporter } from '../../interfaces/ErrorReporter';
|
||||||
|
import { getGlobalErrorHandler } from '../GlobalErrorHandler';
|
||||||
|
|
||||||
export class ConsoleErrorReporter implements ErrorReporter {
|
export class ConsoleErrorReporter implements ErrorReporter {
|
||||||
report(error: Error, context?: unknown): void {
|
report(error: Error, context?: unknown): void {
|
||||||
const timestamp = new Date().toISOString();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,15 @@ export class ConsoleLogger implements Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug(message: string, context?: unknown): void {
|
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 {
|
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 {
|
warn(message: string, context?: unknown): void {
|
||||||
@@ -22,5 +26,14 @@ export class ConsoleLogger implements Logger {
|
|||||||
error(message: string, error?: Error, context?: unknown): void {
|
error(message: string, error?: Error, context?: unknown): void {
|
||||||
const errorStr = error ? ` | Error: ${error.message}` : '';
|
const errorStr = error ? ` | Error: ${error.message}` : '';
|
||||||
console.error(this.formatMessage('error', message, context) + errorStr);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,17 @@ export function isAlpha(): boolean {
|
|||||||
*/
|
*/
|
||||||
export function getPublicRoutes(): readonly string[] {
|
export function getPublicRoutes(): readonly string[] {
|
||||||
return [
|
return [
|
||||||
|
// Core public pages
|
||||||
'/',
|
'/',
|
||||||
|
|
||||||
|
// Public content routes (leagues, drivers, teams, leaderboards, races)
|
||||||
|
'/leagues',
|
||||||
|
'/drivers',
|
||||||
|
'/teams',
|
||||||
|
'/leaderboards',
|
||||||
|
'/races',
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
'/api/signup',
|
'/api/signup',
|
||||||
'/api/auth/signup',
|
'/api/auth/signup',
|
||||||
'/api/auth/login',
|
'/api/auth/login',
|
||||||
@@ -85,8 +95,27 @@ export function getPublicRoutes(): readonly string[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a route is public (accessible in all modes)
|
* 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 {
|
export function isPublicRoute(pathname: string): boolean {
|
||||||
const publicRoutes = getPublicRoutes();
|
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 + '/')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ import { ProtestsApiClient } from '../api/protests/ProtestsApiClient';
|
|||||||
import { PenaltiesApiClient } from '../api/penalties/PenaltiesApiClient';
|
import { PenaltiesApiClient } from '../api/penalties/PenaltiesApiClient';
|
||||||
import { getWebsiteApiBaseUrl } from '../config/apiBaseUrl';
|
import { getWebsiteApiBaseUrl } from '../config/apiBaseUrl';
|
||||||
import { PenaltyService } from './penalties/PenaltyService';
|
import { PenaltyService } from './penalties/PenaltyService';
|
||||||
import { ConsoleErrorReporter } from '../infrastructure/logging/ConsoleErrorReporter';
|
import { EnhancedErrorReporter } from '../infrastructure/EnhancedErrorReporter';
|
||||||
import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger';
|
import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger';
|
||||||
import { LandingService } from './landing/LandingService';
|
import { LandingService } from './landing/LandingService';
|
||||||
|
|
||||||
@@ -55,7 +55,11 @@ import { OnboardingService } from './onboarding/OnboardingService';
|
|||||||
* Services now directly instantiate View Models instead of using Presenters.
|
* Services now directly instantiate View Models instead of using Presenters.
|
||||||
*/
|
*/
|
||||||
export class ServiceFactory {
|
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 logger = new ConsoleLogger();
|
||||||
|
|
||||||
private readonly apiClients: {
|
private readonly apiClients: {
|
||||||
|
|||||||
Reference in New Issue
Block a user