383 lines
17 KiB
TypeScript
383 lines
17 KiB
TypeScript
'use client';
|
||
|
||
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
||
import { ApiError } from '@/lib/api/base/ApiError';
|
||
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
|
||
import { Badge } from '@/ui/Badge';
|
||
import { Button } from '@/ui/Button';
|
||
import { Card } from '@/ui/Card';
|
||
import { Heading } from '@/ui/Heading';
|
||
import { Icon } from '@/ui/Icon';
|
||
import { Grid } from '@/ui/primitives/Grid';
|
||
import { Stack } from '@/ui/primitives/Stack';
|
||
import { Text } from '@/ui/Text';
|
||
import { Activity, AlertTriangle, Copy, RefreshCw, Terminal, X } from 'lucide-react';
|
||
import { useEffect, useState } from '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 getSeverityVariant = (): 'danger' | 'warning' | 'info' | 'default' => {
|
||
switch (error.getSeverity()) {
|
||
case 'error': return 'danger';
|
||
case 'warn': return 'warning';
|
||
case 'info': return 'info';
|
||
default: return 'default';
|
||
}
|
||
};
|
||
|
||
const reliability = connectionMonitor.getReliability();
|
||
|
||
return (
|
||
<Stack
|
||
position="fixed"
|
||
inset="0"
|
||
zIndex={50}
|
||
overflow="auto"
|
||
bg="bg-deep-graphite"
|
||
p={4}
|
||
>
|
||
<Stack maxWidth="6xl" mx="auto" fullWidth>
|
||
<Stack gap={4}>
|
||
{/* Header */}
|
||
<Stack bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="lg" p={4} direction="row" align="center" justify="between">
|
||
<Stack direction="row" align="center" gap={3}>
|
||
<Icon icon={Terminal} size={5} color="rgb(59, 130, 246)" />
|
||
<Heading level={2}>API Error Debug Panel</Heading>
|
||
<Badge variant={getSeverityVariant()}>
|
||
{error.type}
|
||
</Badge>
|
||
</Stack>
|
||
<Stack direction="row" gap={2}>
|
||
<Button
|
||
variant="secondary"
|
||
onClick={copyToClipboard}
|
||
icon={<Icon icon={Copy} size={4} />}
|
||
>
|
||
{copied ? 'Copied!' : 'Copy'}
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
onClick={onReset}
|
||
icon={<Icon icon={X} size={4} />}
|
||
>
|
||
Close
|
||
</Button>
|
||
</Stack>
|
||
</Stack>
|
||
|
||
{/* Error Details */}
|
||
<Grid cols={1} lgCols={2} gap={4}>
|
||
<Stack gap={4}>
|
||
<Card p={0} rounded="lg" overflow="hidden" variant="outline" borderColor="border-charcoal-outline" className="bg-panel-gray/40">
|
||
<Stack bg="bg-charcoal-outline" px={4} py={2} direction="row" align="center" gap={2}>
|
||
<Icon icon={AlertTriangle} size={4} color="text-white" />
|
||
<Text weight="semibold" color="text-white">Error Details</Text>
|
||
</Stack>
|
||
<Stack p={4}>
|
||
<Stack gap={2} style={{ fontSize: '0.75rem' }}>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Type:</Text>
|
||
<Text className="col-span-2" color="text-red-400" weight="bold">{error.type}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Message:</Text>
|
||
<Text className="col-span-2" color="text-gray-300">{error.message}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Endpoint:</Text>
|
||
<Text className="col-span-2" color="text-primary-blue">{error.context.endpoint || 'N/A'}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Method:</Text>
|
||
<Text className="col-span-2" color="text-warning-amber">{error.context.method || 'N/A'}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Status:</Text>
|
||
<Text className="col-span-2">{error.context.statusCode || 'N/A'}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Retry Count:</Text>
|
||
<Text className="col-span-2">{error.context.retryCount || 0}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Timestamp:</Text>
|
||
<Text className="col-span-2" color="text-gray-500">{error.context.timestamp}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Retryable:</Text>
|
||
<Text className="col-span-2" color={error.isRetryable() ? 'text-performance-green' : 'text-red-400'}>
|
||
{error.isRetryable() ? 'Yes' : 'No'}
|
||
</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Connectivity:</Text>
|
||
<Text className="col-span-2" color={error.isConnectivityIssue() ? 'text-red-400' : 'text-performance-green'}>
|
||
{error.isConnectivityIssue() ? 'Yes' : 'No'}
|
||
</Text>
|
||
</Grid>
|
||
{error.context.troubleshooting && (
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Troubleshoot:</Text>
|
||
<Text className="col-span-2" color="text-warning-amber">{error.context.troubleshooting}</Text>
|
||
</Grid>
|
||
)}
|
||
</Stack>
|
||
</Stack>
|
||
</Card>
|
||
|
||
{/* Connection Status */}
|
||
<Card p={0} rounded="lg" overflow="hidden" variant="outline" borderColor="border-charcoal-outline" className="bg-panel-gray/40">
|
||
<Stack bg="bg-charcoal-outline" px={4} py={2} direction="row" align="center" gap={2}>
|
||
<Icon icon={Activity} size={4} color="text-white" />
|
||
<Text weight="semibold" color="text-white">Connection Health</Text>
|
||
</Stack>
|
||
<Stack p={4}>
|
||
<Stack gap={2} style={{ fontSize: '0.75rem' }}>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Status:</Text>
|
||
<Text className="col-span-2" weight="bold" color={
|
||
connectionStatus.status === 'connected' ? 'text-performance-green' :
|
||
connectionStatus.status === 'degraded' ? 'text-warning-amber' :
|
||
'text-red-400'
|
||
}>
|
||
{connectionStatus.status.toUpperCase()}
|
||
</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Reliability:</Text>
|
||
<Text className="col-span-2">{reliability.toFixed(2)}%</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Total Requests:</Text>
|
||
<Text className="col-span-2">{connectionStatus.totalRequests}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Successful:</Text>
|
||
<Text className="col-span-2" color="text-performance-green">{connectionStatus.successfulRequests}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Failed:</Text>
|
||
<Text className="col-span-2" color="text-red-400">{connectionStatus.failedRequests}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Consecutive Failures:</Text>
|
||
<Text className="col-span-2">{connectionStatus.consecutiveFailures}</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Avg Response:</Text>
|
||
<Text className="col-span-2">{connectionStatus.averageResponseTime.toFixed(2)}ms</Text>
|
||
</Grid>
|
||
<Grid cols={3} gap={2}>
|
||
<Text color="text-gray-500">Last Check:</Text>
|
||
<Text className="col-span-2" color="text-gray-500">
|
||
{connectionStatus.lastCheck?.toLocaleTimeString() || 'Never'}
|
||
</Text>
|
||
</Grid>
|
||
</Stack>
|
||
</Stack>
|
||
</Card>
|
||
</Stack>
|
||
|
||
{/* Right Column */}
|
||
<Stack gap={4}>
|
||
{/* Circuit Breakers */}
|
||
<Card p={0} rounded="lg" overflow="hidden" variant="outline" borderColor="border-charcoal-outline" className="bg-panel-gray/40">
|
||
<Stack bg="bg-charcoal-outline" px={4} py={2} direction="row" align="center" gap={2}>
|
||
<Text size="lg">⚡</Text>
|
||
<Text weight="semibold" color="text-white">Circuit Breakers</Text>
|
||
</Stack>
|
||
<Stack p={4}>
|
||
{Object.keys(circuitBreakers).length === 0 ? (
|
||
<Stack align="center" py={4}>
|
||
<Text color="text-gray-500">No circuit breakers active</Text>
|
||
</Stack>
|
||
) : (
|
||
<Stack gap={2} maxHeight="12rem" overflow="auto" style={{ fontSize: '0.75rem' }}>
|
||
{Object.entries(circuitBreakers).map(([endpoint, status]) => (
|
||
<Stack key={endpoint} direction="row" align="center" justify="between" p={2} bg="bg-deep-graphite" rounded="md" border borderColor="border-charcoal-outline">
|
||
<Text color="text-primary-blue" truncate flexGrow={1}>{endpoint}</Text>
|
||
<Stack px={2} py={1} rounded="sm" bg={
|
||
status.state === 'CLOSED' ? 'bg-green-500/20' :
|
||
status.state === 'OPEN' ? 'bg-red-500/20' :
|
||
'bg-yellow-500/20'
|
||
}>
|
||
<Text color={
|
||
status.state === 'CLOSED' ? 'text-performance-green' :
|
||
status.state === 'OPEN' ? 'text-red-400' :
|
||
'text-warning-amber'
|
||
}>
|
||
{status.state}
|
||
</Text>
|
||
</Stack>
|
||
<Text color="text-gray-500" className="ml-2">{status.failures} failures</Text>
|
||
</Stack>
|
||
))}
|
||
</Stack>
|
||
)}
|
||
</Stack>
|
||
</Card>
|
||
|
||
{/* Actions */}
|
||
<Card p={0} rounded="lg" overflow="hidden" variant="outline" borderColor="border-charcoal-outline" className="bg-panel-gray/40">
|
||
<Stack bg="bg-charcoal-outline" px={4} py={2}>
|
||
<Text weight="semibold" color="text-white">Actions</Text>
|
||
</Stack>
|
||
<Stack p={4}>
|
||
<Stack gap={2}>
|
||
<Button
|
||
variant="primary"
|
||
onClick={triggerHealthCheck}
|
||
fullWidth
|
||
icon={<Icon icon={RefreshCw} size={4} />}
|
||
>
|
||
Run Health Check
|
||
</Button>
|
||
<Button
|
||
variant="secondary"
|
||
onClick={resetCircuitBreakers}
|
||
fullWidth
|
||
icon={<Text size="lg">🔄</Text>}
|
||
>
|
||
Reset Circuit Breakers
|
||
</Button>
|
||
<Button
|
||
variant="danger"
|
||
onClick={() => {
|
||
connectionMonitor.reset();
|
||
setConnectionStatus(connectionMonitor.getHealth());
|
||
}}
|
||
fullWidth
|
||
icon={<Text size="lg">🗑️</Text>}
|
||
>
|
||
Reset Connection Stats
|
||
</Button>
|
||
</Stack>
|
||
</Stack>
|
||
</Card>
|
||
|
||
{/* Quick Fixes */}
|
||
<Card p={0} rounded="lg" overflow="hidden" variant="outline" borderColor="border-charcoal-outline" className="bg-panel-gray/40">
|
||
<Stack bg="bg-charcoal-outline" px={4} py={2}>
|
||
<Text weight="semibold" color="text-white">Quick Fixes</Text>
|
||
</Stack>
|
||
<Stack p={4}>
|
||
<Stack gap={2} style={{ fontSize: '0.75rem' }}>
|
||
<Text color="text-gray-400">Common solutions:</Text>
|
||
<Stack as="ul" gap={1} pl={4}>
|
||
<Stack as="li"><Text color="text-gray-300">Check API server is running</Text></Stack>
|
||
<Stack as="li"><Text color="text-gray-300">Verify CORS configuration</Text></Stack>
|
||
<Stack as="li"><Text color="text-gray-300">Check environment variables</Text></Stack>
|
||
<Stack as="li"><Text color="text-gray-300">Review network connectivity</Text></Stack>
|
||
<Stack as="li"><Text color="text-gray-300">Check API rate limits</Text></Stack>
|
||
</Stack>
|
||
</Stack>
|
||
</Stack>
|
||
</Card>
|
||
|
||
{/* Raw Error */}
|
||
<Card p={0} rounded="lg" overflow="hidden" variant="outline" borderColor="border-charcoal-outline" className="bg-panel-gray/40">
|
||
<Stack bg="bg-charcoal-outline" px={4} py={2}>
|
||
<Text weight="semibold" color="text-white">Raw Error</Text>
|
||
</Stack>
|
||
<Stack p={4}>
|
||
<Stack as="pre" p={2} bg="bg-deep-graphite" rounded="md" overflow="auto" maxHeight="8rem" style={{ fontSize: '0.75rem' }} color="text-gray-400">
|
||
{JSON.stringify({
|
||
type: error.type,
|
||
message: error.message,
|
||
context: error.context,
|
||
}, null, 2)}
|
||
</Stack>
|
||
</Stack>
|
||
</Card>
|
||
</Stack>
|
||
</Grid>
|
||
|
||
{/* Console Output */}
|
||
<Card p={0} rounded="lg" overflow="hidden" variant="outline" borderColor="border-charcoal-outline" className="bg-panel-gray/40">
|
||
<Stack bg="bg-charcoal-outline" px={4} py={2} direction="row" align="center" gap={2}>
|
||
<Icon icon={Terminal} size={4} color="text-white" />
|
||
<Text weight="semibold" color="text-white">Console Output</Text>
|
||
</Stack>
|
||
<Stack p={4} bg="bg-deep-graphite" style={{ fontSize: '0.75rem' }}>
|
||
<Text color="text-gray-500" block mb={2}>{'>'} {error.getDeveloperMessage()}</Text>
|
||
<Text color="text-gray-600" block>Check browser console for full stack trace and additional debug info.</Text>
|
||
</Stack>
|
||
</Card>
|
||
</Stack>
|
||
</Stack>
|
||
</Stack>
|
||
);
|
||
}
|