dev setup

This commit is contained in:
2025-12-31 21:24:42 +01:00
parent 167e82a52b
commit 16e0bdaec1
26 changed files with 4076 additions and 646 deletions

View File

@@ -0,0 +1,155 @@
'use client';
import React, { Component, ReactNode } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { ErrorDisplay } from './ErrorDisplay';
import { DevErrorPanel } from './DevErrorPanel';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: ApiError) => void;
}
interface State {
hasError: boolean;
error: ApiError | null;
isDev: boolean;
}
/**
* Error Boundary for API-related errors
* Catches errors from API calls and displays appropriate UI
*/
export class ApiErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
isDev: process.env.NODE_ENV === 'development',
};
}
static getDerivedStateFromError(error: Error): State {
// Only handle ApiError instances
if (error instanceof ApiError) {
return {
hasError: true,
error,
isDev: process.env.NODE_ENV === 'development',
};
}
// Re-throw non-API errors
throw error;
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
if (error instanceof ApiError) {
// Report to connection monitor
connectionMonitor.recordFailure(error);
// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error);
}
// For connectivity errors in production, don't show error boundary UI
// These are handled by the notification system
if (error.isConnectivityIssue() && !this.state.isDev) {
// Reset error state so boundary doesn't block UI
setTimeout(() => this.resetError(), 100);
return;
}
}
}
componentDidMount(): void {
// Listen for connection status changes
const monitor = connectionMonitor;
monitor.on('disconnected', this.handleDisconnected);
monitor.on('degraded', this.handleDegraded);
monitor.on('connected', this.handleConnected);
}
componentWillUnmount(): void {
const monitor = connectionMonitor;
monitor.off('disconnected', this.handleDisconnected);
monitor.off('degraded', this.handleDegraded);
monitor.off('connected', this.handleConnected);
}
private handleDisconnected = (): void => {
// Connection status handled by notification system
};
private handleDegraded = (): void => {
// Connection status handled by notification system
};
private handleConnected = (): void => {
// Connection status handled by notification system
};
resetError = (): void => {
this.setState({ hasError: false, error: null });
};
render(): ReactNode {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) {
return this.props.fallback;
}
// Show different UI based on environment
if (this.state.isDev) {
return (
<DevErrorPanel
error={this.state.error}
onReset={this.resetError}
/>
);
}
return (
<ErrorDisplay
error={this.state.error}
onRetry={this.resetError}
/>
);
}
return this.props.children;
}
}
/**
* Hook-based alternative for functional components
*/
export function useApiErrorBoundary() {
const [error, setError] = React.useState<ApiError | null>(null);
const [isDev] = React.useState(process.env.NODE_ENV === 'development');
const handleError = (err: ApiError) => {
setError(err);
};
const reset = () => {
setError(null);
};
return {
error,
isDev,
handleError,
reset,
ErrorBoundary: ({ children }: { children: ReactNode }) => (
<ApiErrorBoundary onError={handleError}>
{children}
</ApiErrorBoundary>
),
};
}

View File

@@ -0,0 +1,329 @@
'use client';
import { useState, useEffect } from 'react';
import { ApiConnectionMonitor, ConnectionStatus } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
import {
Activity,
Wifi,
WifiOff,
AlertTriangle,
CheckCircle2,
RefreshCw,
Terminal,
Shield,
Clock,
TrendingUp
} from 'lucide-react';
interface ApiStatusToolbarProps {
position?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left';
autoHide?: boolean;
}
/**
* Development toolbar showing real-time API connection status
* Integrates with existing DevToolbar or works standalone
*/
export function ApiStatusToolbar({ position = 'bottom-right', autoHide = false }: ApiStatusToolbarProps) {
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
const [health, setHealth] = useState(ApiConnectionMonitor.getInstance().getHealth());
const [expanded, setExpanded] = useState(false);
const [show, setShow] = useState(true);
useEffect(() => {
const monitor = ApiConnectionMonitor.getInstance();
const registry = CircuitBreakerRegistry.getInstance();
const updateState = () => {
setStatus(monitor.getStatus());
setHealth(monitor.getHealth());
};
// Initial state
updateState();
// Listen for events
monitor.on('connected', updateState);
monitor.on('disconnected', updateState);
monitor.on('degraded', updateState);
monitor.on('success', updateState);
monitor.on('failure', updateState);
// Auto-hide logic
if (autoHide) {
const hideTimer = setTimeout(() => setShow(false), 5000);
const showOnInteraction = () => setShow(true);
document.addEventListener('mousemove', showOnInteraction);
document.addEventListener('click', showOnInteraction);
return () => {
clearTimeout(hideTimer);
document.removeEventListener('mousemove', showOnInteraction);
document.removeEventListener('click', showOnInteraction);
monitor.off('connected', updateState);
monitor.off('disconnected', updateState);
monitor.off('degraded', updateState);
monitor.off('success', updateState);
monitor.off('failure', updateState);
};
}
return () => {
monitor.off('connected', updateState);
monitor.off('disconnected', updateState);
monitor.off('degraded', updateState);
monitor.off('success', updateState);
monitor.off('failure', updateState);
};
}, [autoHide]);
const handleHealthCheck = async () => {
const monitor = ApiConnectionMonitor.getInstance();
await monitor.performHealthCheck();
};
const handleReset = () => {
ApiConnectionMonitor.getInstance().reset();
CircuitBreakerRegistry.getInstance().resetAll();
};
const getReliabilityColor = (reliability: number) => {
if (reliability >= 95) return 'text-green-400';
if (reliability >= 80) return 'text-yellow-400';
return 'text-red-400';
};
const getStatusIcon = () => {
switch (status) {
case 'connected':
return <CheckCircle2 className="w-4 h-4 text-green-400" />;
case 'degraded':
return <AlertTriangle className="w-4 h-4 text-yellow-400" />;
case 'disconnected':
return <WifiOff className="w-4 h-4 text-red-400" />;
case 'checking':
return <RefreshCw className="w-4 h-4 animate-spin text-blue-400" />;
default:
return <Wifi className="w-4 h-4 text-gray-400" />;
}
};
const getStatusColor = () => {
switch (status) {
case 'connected': return 'bg-green-500/20 border-green-500/40';
case 'degraded': return 'bg-yellow-500/20 border-yellow-500/40';
case 'disconnected': return 'bg-red-500/20 border-red-500/40';
default: return 'bg-gray-500/20 border-gray-500/40';
}
};
const reliability = ((health.successfulRequests / Math.max(health.totalRequests, 1)) * 100).toFixed(1);
if (!show) {
return (
<button
onClick={() => setShow(true)}
className={`fixed p-2 bg-iron-gray border border-charcoal-outline rounded-lg shadow-lg hover:bg-charcoal-outline transition-all ${
position === 'bottom-right' ? 'bottom-4 right-4' :
position === 'top-right' ? 'top-4 right-4' :
position === 'bottom-left' ? 'bottom-4 left-4' :
'top-4 left-4'
}`}
>
<Activity className="w-5 h-5 text-primary-blue" />
</button>
);
}
return (
<div className={`fixed z-50 transition-all ${
position === 'bottom-right' ? 'bottom-4 right-4' :
position === 'top-right' ? 'top-4 right-4' :
position === 'bottom-left' ? 'bottom-4 left-4' :
'top-4 left-4'
}`}>
{/* Compact Status Indicator */}
{!expanded ? (
<button
onClick={() => setExpanded(true)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg border shadow-lg backdrop-blur-md transition-all hover:scale-105 ${getStatusColor()}`}
>
{getStatusIcon()}
<span className="text-sm font-semibold text-white">{status.toUpperCase()}</span>
<span className={`text-xs ${getReliabilityColor(parseFloat(reliability))}`}>
{reliability}%
</span>
</button>
) : (
/* Expanded Panel */
<div className={`w-80 rounded-lg border shadow-2xl backdrop-blur-md overflow-hidden ${getStatusColor()}`}>
{/* Header */}
<div className="bg-iron-gray/80 border-b border-charcoal-outline px-3 py-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-primary-blue" />
<span className="text-xs font-bold text-white">API STATUS</span>
</div>
<div className="flex gap-1">
<button
onClick={handleHealthCheck}
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
title="Run Health Check"
>
<RefreshCw className="w-3 h-3 text-gray-400 hover:text-white" />
</button>
<button
onClick={handleReset}
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
title="Reset Stats"
>
<span className="text-xs text-gray-400 hover:text-white">R</span>
</button>
<button
onClick={() => setExpanded(false)}
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
>
<span className="text-xs text-gray-400 hover:text-white">×</span>
</button>
</div>
</div>
{/* Body */}
<div className="px-3 py-2 space-y-2 bg-deep-graphite/90">
{/* Status Row */}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Status</span>
<span className={`text-xs font-bold uppercase ${status === 'connected' ? 'text-green-400' : status === 'degraded' ? 'text-yellow-400' : 'text-red-400'}`}>
{status}
</span>
</div>
{/* Reliability */}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Reliability</span>
<span className={`text-xs font-bold ${getReliabilityColor(parseFloat(reliability))}`}>
{reliability}%
</span>
</div>
{/* Request Stats */}
<div className="grid grid-cols-3 gap-2 text-center">
<div className="bg-iron-gray/50 rounded p-1">
<div className="text-[10px] text-gray-400">Total</div>
<div className="text-sm font-bold text-white">{health.totalRequests}</div>
</div>
<div className="bg-iron-gray/50 rounded p-1">
<div className="text-[10px] text-gray-400">Success</div>
<div className="text-sm font-bold text-green-400">{health.successfulRequests}</div>
</div>
<div className="bg-iron-gray/50 rounded p-1">
<div className="text-[10px] text-gray-400">Failed</div>
<div className="text-sm font-bold text-red-400">{health.failedRequests}</div>
</div>
</div>
{/* Performance */}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Avg Response</span>
<span className="text-xs font-mono text-blue-400">
{health.averageResponseTime.toFixed(0)}ms
</span>
</div>
{/* Consecutive Failures */}
{health.consecutiveFailures > 0 && (
<div className="flex items-center justify-between bg-red-500/10 rounded px-2 py-1">
<span className="text-xs text-red-400">Consecutive Failures</span>
<span className="text-xs font-bold text-red-400">{health.consecutiveFailures}</span>
</div>
)}
{/* Circuit Breakers */}
<div className="border-t border-charcoal-outline pt-2">
<div className="flex items-center gap-1 mb-1">
<Shield className="w-3 h-3 text-gray-400" />
<span className="text-[10px] text-gray-400 font-bold">CIRCUIT BREAKERS</span>
</div>
<CircuitBreakerStatus />
</div>
{/* Last Check */}
<div className="border-t border-charcoal-outline pt-2 flex items-center justify-between">
<span className="text-[10px] text-gray-500">Last Check</span>
<span className="text-[10px] text-gray-400 font-mono">
{health.lastCheck ? new Date(health.lastCheck).toLocaleTimeString() : 'Never'}
</span>
</div>
{/* Actions */}
<div className="grid grid-cols-2 gap-2 pt-1">
<button
onClick={handleHealthCheck}
className="px-2 py-1 bg-primary-blue hover:bg-primary-blue/80 text-white text-xs rounded transition-colors"
>
Check Health
</button>
<button
onClick={() => {
const monitor = ApiConnectionMonitor.getInstance();
const report = monitor.getDebugReport();
alert(report);
}}
className="px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 text-xs rounded transition-colors border border-charcoal-outline"
>
Debug Report
</button>
</div>
</div>
</div>
)}
</div>
);
}
/**
* Circuit Breaker Status Component
*/
function CircuitBreakerStatus() {
const [status, setStatus] = useState(CircuitBreakerRegistry.getInstance().getStatus());
useEffect(() => {
const registry = CircuitBreakerRegistry.getInstance();
// Poll for updates every 2 seconds
const interval = setInterval(() => {
setStatus(registry.getStatus());
}, 2000);
return () => clearInterval(interval);
}, []);
const entries = Object.entries(status);
if (entries.length === 0) {
return (
<div className="text-[10px] text-gray-500 italic">No active circuit breakers</div>
);
}
return (
<div className="space-y-1 max-h-20 overflow-auto">
{entries.map(([endpoint, breaker]) => (
<div key={endpoint} className="flex items-center justify-between text-[10px]">
<span className="text-gray-400 truncate flex-1">{endpoint.split('/').pop() || endpoint}</span>
<span className={`px-1 rounded ${
breaker.state === 'CLOSED' ? 'bg-green-500/20 text-green-400' :
breaker.state === 'OPEN' ? 'bg-red-500/20 text-red-400' :
'bg-yellow-500/20 text-yellow-400'
}`}>
{breaker.state}
</span>
{breaker.failures > 0 && (
<span className="text-red-400 ml-1">({breaker.failures})</span>
)}
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,347 @@
'use client';
import { ApiError } from '@/lib/api/base/ApiError';
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
import { useState, useEffect } from 'react';
import { X, RefreshCw, Copy, Terminal, Activity, AlertTriangle } from 'lucide-react';
interface DevErrorPanelProps {
error: ApiError;
onReset: () => void;
}
/**
* Developer-focused error panel with detailed debugging information
*/
export function DevErrorPanel({ error, onReset }: DevErrorPanelProps) {
const [connectionStatus, setConnectionStatus] = useState(connectionMonitor.getHealth());
const [circuitBreakers, setCircuitBreakers] = useState(CircuitBreakerRegistry.getInstance().getStatus());
const [copied, setCopied] = useState(false);
useEffect(() => {
// Update status on mount
const health = connectionMonitor.getHealth();
setConnectionStatus(health);
setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus());
// Listen for status changes
const handleStatusChange = () => {
setConnectionStatus(connectionMonitor.getHealth());
setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus());
};
connectionMonitor.on('success', handleStatusChange);
connectionMonitor.on('failure', handleStatusChange);
connectionMonitor.on('connected', handleStatusChange);
connectionMonitor.on('disconnected', handleStatusChange);
connectionMonitor.on('degraded', handleStatusChange);
return () => {
connectionMonitor.off('success', handleStatusChange);
connectionMonitor.off('failure', handleStatusChange);
connectionMonitor.off('connected', handleStatusChange);
connectionMonitor.off('disconnected', handleStatusChange);
connectionMonitor.off('degraded', handleStatusChange);
};
}, []);
const copyToClipboard = async () => {
const debugInfo = {
error: {
type: error.type,
message: error.message,
context: error.context,
stack: error.stack,
},
connection: connectionStatus,
circuitBreakers,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
};
try {
await navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
// Silent failure for clipboard operations
}
};
const triggerHealthCheck = async () => {
await connectionMonitor.performHealthCheck();
setConnectionStatus(connectionMonitor.getHealth());
};
const resetCircuitBreakers = () => {
CircuitBreakerRegistry.getInstance().resetAll();
setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus());
};
const getSeverityColor = (type: string) => {
switch (error.getSeverity()) {
case 'error': return 'bg-red-500/20 text-red-400 border-red-500/40';
case 'warn': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/40';
case 'info': return 'bg-blue-500/20 text-blue-400 border-blue-500/40';
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/40';
}
};
const reliability = connectionMonitor.getReliability();
return (
<div className="fixed inset-0 z-50 overflow-auto bg-deep-graphite p-4 font-mono text-sm">
<div className="max-w-6xl mx-auto space-y-4">
{/* Header */}
<div className="bg-iron-gray border border-charcoal-outline rounded-lg p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Terminal className="w-5 h-5 text-primary-blue" />
<h2 className="text-lg font-bold text-white">API Error Debug Panel</h2>
<span className={`px-2 py-1 rounded border text-xs ${getSeverityColor(error.type)}`}>
{error.type}
</span>
</div>
<div className="flex gap-2">
<button
onClick={copyToClipboard}
className="px-3 py-1 bg-iron-gray hover:bg-charcoal-outline border border-charcoal-outline rounded text-gray-300 flex items-center gap-2"
title="Copy debug info"
>
<Copy className="w-4 h-4" />
{copied ? 'Copied!' : 'Copy'}
</button>
<button
onClick={onReset}
className="px-3 py-1 bg-primary-blue hover:bg-primary-blue/80 text-white rounded flex items-center gap-2"
>
<X className="w-4 h-4" />
Close
</button>
</div>
</div>
{/* Error Details */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="space-y-4">
<div className="bg-iron-gray border border-charcoal-outline rounded-lg overflow-hidden">
<div className="bg-charcoal-outline px-4 py-2 font-semibold text-white flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
Error Details
</div>
<div className="p-4 space-y-2 text-xs">
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Type:</span>
<span className="col-span-2 text-red-400 font-bold">{error.type}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Message:</span>
<span className="col-span-2 text-gray-300">{error.message}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Endpoint:</span>
<span className="col-span-2 text-blue-400">{error.context.endpoint || 'N/A'}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Method:</span>
<span className="col-span-2 text-yellow-400">{error.context.method || 'N/A'}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Status:</span>
<span className="col-span-2">{error.context.statusCode || 'N/A'}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Retry Count:</span>
<span className="col-span-2">{error.context.retryCount || 0}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Timestamp:</span>
<span className="col-span-2 text-gray-500">{error.context.timestamp}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Retryable:</span>
<span className={`col-span-2 ${error.isRetryable() ? 'text-green-400' : 'text-red-400'}`}>
{error.isRetryable() ? 'Yes' : 'No'}
</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Connectivity:</span>
<span className={`col-span-2 ${error.isConnectivityIssue() ? 'text-red-400' : 'text-green-400'}`}>
{error.isConnectivityIssue() ? 'Yes' : 'No'}
</span>
</div>
{error.context.troubleshooting && (
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Troubleshoot:</span>
<span className="col-span-2 text-yellow-400">{error.context.troubleshooting}</span>
</div>
)}
</div>
</div>
{/* Connection Status */}
<div className="bg-iron-gray border border-charcoal-outline rounded-lg overflow-hidden">
<div className="bg-charcoal-outline px-4 py-2 font-semibold text-white flex items-center gap-2">
<Activity className="w-4 h-4" />
Connection Health
</div>
<div className="p-4 space-y-2 text-xs">
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Status:</span>
<span className={`col-span-2 font-bold ${
connectionStatus.status === 'connected' ? 'text-green-400' :
connectionStatus.status === 'degraded' ? 'text-yellow-400' :
'text-red-400'
}`}>
{connectionStatus.status.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Reliability:</span>
<span className="col-span-2">{reliability.toFixed(2)}%</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Total Requests:</span>
<span className="col-span-2">{connectionStatus.totalRequests}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Successful:</span>
<span className="col-span-2 text-green-400">{connectionStatus.successfulRequests}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Failed:</span>
<span className="col-span-2 text-red-400">{connectionStatus.failedRequests}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Consecutive Failures:</span>
<span className="col-span-2">{connectionStatus.consecutiveFailures}</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Avg Response:</span>
<span className="col-span-2">{connectionStatus.averageResponseTime.toFixed(2)}ms</span>
</div>
<div className="grid grid-cols-3 gap-2">
<span className="text-gray-500">Last Check:</span>
<span className="col-span-2 text-gray-500">
{connectionStatus.lastCheck?.toLocaleTimeString() || 'Never'}
</span>
</div>
</div>
</div>
</div>
{/* Right Column */}
<div className="space-y-4">
{/* Circuit Breakers */}
<div className="bg-iron-gray border border-charcoal-outline rounded-lg overflow-hidden">
<div className="bg-charcoal-outline px-4 py-2 font-semibold text-white flex items-center gap-2">
<span className="text-lg"></span>
Circuit Breakers
</div>
<div className="p-4">
{Object.keys(circuitBreakers).length === 0 ? (
<div className="text-gray-500 text-center py-4">No circuit breakers active</div>
) : (
<div className="space-y-2 text-xs max-h-48 overflow-auto">
{Object.entries(circuitBreakers).map(([endpoint, status]) => (
<div key={endpoint} className="flex items-center justify-between p-2 bg-deep-graphite rounded border border-charcoal-outline">
<span className="text-blue-400 truncate flex-1">{endpoint}</span>
<span className={`px-2 py-1 rounded ${
status.state === 'CLOSED' ? 'bg-green-500/20 text-green-400' :
status.state === 'OPEN' ? 'bg-red-500/20 text-red-400' :
'bg-yellow-500/20 text-yellow-400'
}`}>
{status.state}
</span>
<span className="text-gray-500 ml-2">{status.failures} failures</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Actions */}
<div className="bg-iron-gray border border-charcoal-outline rounded-lg overflow-hidden">
<div className="bg-charcoal-outline px-4 py-2 font-semibold text-white">
Actions
</div>
<div className="p-4 space-y-2">
<button
onClick={triggerHealthCheck}
className="w-full px-3 py-2 bg-primary-blue hover:bg-primary-blue/80 text-white rounded flex items-center justify-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Run Health Check
</button>
<button
onClick={resetCircuitBreakers}
className="w-full px-3 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded flex items-center justify-center gap-2"
>
<span className="text-lg">🔄</span>
Reset Circuit Breakers
</button>
<button
onClick={() => {
connectionMonitor.reset();
setConnectionStatus(connectionMonitor.getHealth());
}}
className="w-full px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded flex items-center justify-center gap-2"
>
<span className="text-lg">🗑</span>
Reset Connection Stats
</button>
</div>
</div>
{/* Quick Fixes */}
<div className="bg-iron-gray border border-charcoal-outline rounded-lg overflow-hidden">
<div className="bg-charcoal-outline px-4 py-2 font-semibold text-white">
Quick Fixes
</div>
<div className="p-4 space-y-2 text-xs">
<div className="text-gray-400">Common solutions:</div>
<ul className="list-disc list-inside space-y-1 text-gray-300">
<li>Check API server is running</li>
<li>Verify CORS configuration</li>
<li>Check environment variables</li>
<li>Review network connectivity</li>
<li>Check API rate limits</li>
</ul>
</div>
</div>
{/* Raw Error */}
<div className="bg-iron-gray border border-charcoal-outline rounded-lg overflow-hidden">
<div className="bg-charcoal-outline px-4 py-2 font-semibold text-white">
Raw Error
</div>
<div className="p-4">
<pre className="text-xs text-gray-400 overflow-auto max-h-32 bg-deep-graphite p-2 rounded">
{JSON.stringify({
type: error.type,
message: error.message,
context: error.context,
}, null, 2)}
</pre>
</div>
</div>
</div>
</div>
{/* Console Output */}
<div className="bg-iron-gray border border-charcoal-outline rounded-lg overflow-hidden">
<div className="bg-charcoal-outline px-4 py-2 font-semibold text-white flex items-center gap-2">
<Terminal className="w-4 h-4" />
Console Output
</div>
<div className="p-4 bg-deep-graphite font-mono text-xs">
<div className="text-gray-500 mb-2">{'>'} {error.getDeveloperMessage()}</div>
<div className="text-gray-600">Check browser console for full stack trace and additional debug info.</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,146 @@
'use client';
import { ApiError } from '@/lib/api/base/ApiError';
import { AlertTriangle, Wifi, RefreshCw, ArrowLeft } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
interface ErrorDisplayProps {
error: ApiError;
onRetry?: () => void;
}
/**
* User-friendly error display for production environments
*/
export function ErrorDisplay({ error, onRetry }: ErrorDisplayProps) {
const router = useRouter();
const [isRetrying, setIsRetrying] = useState(false);
const userMessage = error.getUserMessage();
const isConnectivity = error.isConnectivityIssue();
const handleRetry = async () => {
if (onRetry) {
setIsRetrying(true);
try {
onRetry();
} finally {
setIsRetrying(false);
}
}
};
const handleGoBack = () => {
router.back();
};
const handleGoHome = () => {
router.push('/');
};
return (
<div className="min-h-screen bg-deep-graphite flex items-center justify-center p-4">
<div className="max-w-md w-full bg-iron-gray border border-charcoal-outline rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="bg-red-500/10 border-b border-red-500/20 p-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-500/20 rounded-lg">
{isConnectivity ? (
<Wifi className="w-6 h-6 text-red-400" />
) : (
<AlertTriangle className="w-6 h-6 text-red-400" />
)}
</div>
<div>
<h1 className="text-xl font-bold text-white">
{isConnectivity ? 'Connection Issue' : 'Something Went Wrong'}
</h1>
<p className="text-sm text-gray-400">Error {error.context.statusCode || 'N/A'}</p>
</div>
</div>
</div>
{/* Body */}
<div className="p-6 space-y-4">
<p className="text-gray-300 leading-relaxed">{userMessage}</p>
{/* Details for debugging (collapsed by default) */}
<details className="text-xs text-gray-500 font-mono bg-deep-graphite p-3 rounded border border-charcoal-outline">
<summary className="cursor-pointer hover:text-gray-300">Technical Details</summary>
<div className="mt-2 space-y-1">
<div>Type: {error.type}</div>
<div>Endpoint: {error.context.endpoint || 'N/A'}</div>
{error.context.statusCode && <div>Status: {error.context.statusCode}</div>}
{error.context.retryCount !== undefined && (
<div>Retries: {error.context.retryCount}</div>
)}
</div>
</details>
{/* Action Buttons */}
<div className="flex flex-col gap-2 pt-2">
{error.isRetryable() && (
<button
onClick={handleRetry}
disabled={isRetrying}
className="flex items-center justify-center gap-2 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition-colors disabled:opacity-50"
>
{isRetrying ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Retrying...
</>
) : (
<>
<RefreshCw className="w-4 h-4" />
Try Again
</>
)}
</button>
)}
<div className="flex gap-2">
<button
onClick={handleGoBack}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded-lg font-medium transition-colors border border-charcoal-outline"
>
<ArrowLeft className="w-4 h-4" />
Go Back
</button>
<button
onClick={handleGoHome}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded-lg font-medium transition-colors border border-charcoal-outline"
>
Home
</button>
</div>
</div>
</div>
{/* Footer */}
<div className="bg-iron-gray/50 border-t border-charcoal-outline p-4 text-xs text-gray-500 text-center">
If this persists, please contact support at{' '}
<a
href="mailto:support@gridpilot.com"
className="text-primary-blue hover:underline"
>
support@gridpilot.com
</a>
</div>
</div>
</div>
);
}
/**
* Full-screen error display with more context
*/
export function FullScreenError({ error, onRetry }: ErrorDisplayProps) {
return (
<div className="fixed inset-0 z-50 bg-deep-graphite flex items-center justify-center p-4">
<ErrorDisplay error={error} onRetry={onRetry} />
</div>
);
}

View File

@@ -0,0 +1,143 @@
'use client';
import { useEffect, useState } from 'react';
import { useNotifications } from '@/components/notifications/NotificationProvider';
import { ApiError } from '@/lib/api/base/ApiError';
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
/**
* Integration component that listens for API errors and shows notifications
* Should be placed at the root level of the app
*/
export function NotificationIntegration() {
const { addNotification } = useNotifications();
const [lastConnectionStatus, setLastConnectionStatus] = useState<string | null>(null);
useEffect(() => {
// Listen for custom notification events from error reporter
const handleNotificationEvent = (event: CustomEvent) => {
const { type, title, message, variant, autoDismiss } = event.detail;
addNotification({
type: type || 'error',
title: title || 'Error',
message,
variant: variant || 'toast',
requiresResponse: variant === 'modal',
});
};
// Listen for connection status changes
const handleConnectionChange = () => {
const status = connectionMonitor.getStatus();
if (status === 'disconnected' && lastConnectionStatus !== 'disconnected') {
addNotification({
type: 'connection_lost',
title: 'Connection Lost',
message: 'Unable to connect to the server. Please check your internet connection.',
variant: 'modal',
requiresResponse: true,
});
} else if (status === 'degraded' && lastConnectionStatus !== 'degraded') {
addNotification({
type: 'connection_degraded',
title: 'Connection Issues',
message: 'API connection is experiencing issues. Some features may be limited.',
variant: 'toast',
requiresResponse: false,
});
} else if (status === 'connected' && lastConnectionStatus === 'disconnected') {
addNotification({
type: 'connection_restored',
title: 'Connection Restored',
message: 'API connection has been restored.',
variant: 'toast',
requiresResponse: false,
});
}
setLastConnectionStatus(status);
};
// Listen for gridpilot notification events
if (typeof window !== 'undefined') {
window.addEventListener('gridpilot-notification', handleNotificationEvent as EventListener);
}
// Monitor connection status changes
connectionMonitor.on('disconnected', handleConnectionChange);
connectionMonitor.on('degraded', handleConnectionChange);
connectionMonitor.on('connected', handleConnectionChange);
return () => {
if (typeof window !== 'undefined') {
window.removeEventListener('gridpilot-notification', handleNotificationEvent as EventListener);
}
connectionMonitor.off('disconnected', handleConnectionChange);
connectionMonitor.off('degraded', handleConnectionChange);
connectionMonitor.off('connected', handleConnectionChange);
};
}, [addNotification, lastConnectionStatus]);
return null; // This component doesn't render anything
}
/**
* Hook to manually trigger API error notifications
*/
export function useApiErrorNotifications() {
const { addNotification } = useNotifications();
const showApiError = (error: ApiError) => {
const isConnectivity = error.isConnectivityIssue();
addNotification({
type: error.type.toLowerCase(),
title: error.type.replace('_', ' ').toUpperCase(),
message: error.getUserMessage(),
variant: isConnectivity ? 'modal' : 'toast',
requiresResponse: isConnectivity,
actionUrl: isConnectivity ? undefined : '/support',
});
};
const showConnectionStatus = () => {
const status = connectionMonitor.getStatus();
const health = connectionMonitor.getHealth();
if (status === 'disconnected') {
addNotification({
type: 'connection_lost',
title: 'API Unavailable',
message: 'The API server is not responding. Please try again later.',
variant: 'modal',
requiresResponse: true,
});
} else if (status === 'degraded') {
const reliability = ((health.successfulRequests / Math.max(health.totalRequests, 1)) * 100).toFixed(1);
addNotification({
type: 'connection_degraded',
title: 'Degraded Performance',
message: `API reliability is at ${reliability}%. Some features may not work correctly.`,
variant: 'toast',
requiresResponse: false,
});
} else {
addNotification({
type: 'connection_ok',
title: 'Connection OK',
message: 'API connection is healthy.',
variant: 'toast',
requiresResponse: false,
});
}
};
return {
showApiError,
showConnectionStatus,
getHealth: () => connectionMonitor.getHealth(),
getStatus: () => connectionMonitor.getStatus(),
};
}