Files
gridpilot.gg/apps/website/components/shared/DevErrorPanel.tsx
2026-01-24 12:47:49 +01:00

160 lines
6.5 KiB
TypeScript

import { connectionMonitor } from '@/lib/gateways/api/base/ApiConnectionMonitor';
import { ApiError } from '@/lib/gateways/api/base/ApiError';
import { CircuitBreakerRegistry } from '@/lib/gateways/api/base/RetryHandler';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Copy, RefreshCw, Terminal, X } from 'lucide-react';
import { useEffect, useState } from 'react';
interface DevErrorPanelProps {
error: ApiError;
onReset: () => void;
}
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(() => {
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
}
};
const getSeverityVariant = (): 'critical' | 'warning' | 'primary' | 'default' => {
switch (error.getSeverity()) {
case 'error': return 'critical';
case 'warn': return 'warning';
case 'info': return 'primary';
default: return 'default';
}
};
return (
<Box position="fixed" inset={0} zIndex={100} bg="var(--ui-color-bg-base)" padding={4} style={{ overflowY: 'auto' }}>
<Box maxWidth="80rem" marginX="auto" fullWidth>
<Box display="flex" flexDirection="col" gap={4}>
{/* Header */}
<Card variant="dark" padding={4}>
<Box display="flex" alignItems="center" justifyContent="between">
<Box display="flex" alignItems="center" gap={3}>
<Icon icon={Terminal} size={5} intent="primary" />
<Heading level={2}>API Error Debug Panel</Heading>
<Badge variant={getSeverityVariant() as any}>
{error.type}
</Badge>
</Box>
<Box display="flex" 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>
</Box>
</Box>
</Card>
{/* Details Grid */}
<Box display="grid" gridCols={{ base: 1, lg: 2 }} gap={4}>
<Box display="flex" flexDirection="col" gap={4}>
<Card title="Error Details" variant="outline">
<Box display="flex" flexDirection="col" gap={2}>
<DetailRow label="Type" value={error.type} intent="critical" />
<DetailRow label="Message" value={error.message} />
<DetailRow label="Endpoint" value={error.context.endpoint || 'N/A'} intent="primary" />
<DetailRow label="Method" value={error.context.method || 'N/A'} intent="warning" />
<DetailRow label="Status" value={error.context.statusCode || 'N/A'} />
</Box>
</Card>
<Card title="Connection Health" variant="outline">
<Box display="flex" flexDirection="col" gap={2}>
<DetailRow label="Status" value={connectionStatus.status.toUpperCase()} intent={connectionStatus.status === 'connected' ? 'success' : 'critical'} />
<DetailRow label="Reliability" value={`${connectionMonitor.getReliability().toFixed(2)}%`} />
<DetailRow label="Total Requests" value={connectionStatus.totalRequests} />
</Box>
</Card>
</Box>
<Box display="flex" flexDirection="col" gap={4}>
<Card title="Actions" variant="outline">
<Box display="flex" flexDirection="col" gap={2}>
<Button variant="primary" onClick={() => connectionMonitor.performHealthCheck()} fullWidth icon={<Icon icon={RefreshCw} size={4} />}>
Run Health Check
</Button>
<Button variant="secondary" onClick={() => CircuitBreakerRegistry.getInstance().resetAll()} fullWidth>
Reset Circuit Breakers
</Button>
</Box>
</Card>
<Card title="Raw Error" variant="outline">
<Box padding={2} bg="var(--ui-color-bg-surface-muted)" rounded="md" style={{ maxHeight: '10rem', overflow: 'auto' }}>
<Text size="xs" variant="low" font="mono">
{JSON.stringify(error.context, null, 2)}
</Text>
</Box>
</Card>
</Box>
</Box>
</Box>
</Box>
</Box>
);
}
const DetailRow = ({ label, value, intent = 'low' }: { label: string; value: any; intent?: any }) => (
<Box display="grid" gridCols={3} gap={2}>
<Text size="xs" variant="low" weight="bold">{label}:</Text>
<Box style={{ gridColumn: 'span 2' }}>
<Text size="xs" variant={intent} weight="bold">{String(value)}</Text>
</Box>
</Box>
);