Files
gridpilot.gg/apps/website/components/errors/DevErrorPanel.tsx
2026-01-18 16:43:32 +01:00

383 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}