Files
gridpilot.gg/apps/website/components/errors/DevErrorPanel.tsx
2026-01-15 17:12:24 +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 React, { useState, useEffect } from 'react';
import { X, RefreshCw, Copy, Terminal, Activity, AlertTriangle } from 'lucide-react';
import { ApiError } from '@/lib/api/base/ApiError';
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Surface } from '@/ui/Surface';
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 (
<Box
position="fixed"
inset="0"
zIndex={50}
overflow="auto"
bg="bg-deep-graphite"
p={4}
>
<Box maxWidth="6xl" mx="auto">
<Stack gap={4}>
{/* Header */}
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="lg" p={4} display="flex" alignItems="center" justifyContent="between">
<Box display="flex" alignItems="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>
</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>
{/* Error Details */}
<Box display="grid" gridCols={{ base: 1, lg: 2 }} gap={4}>
<Stack gap={4}>
<Surface variant="muted" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Box bg="bg-charcoal-outline" px={4} py={2} display="flex" alignItems="center" gap={2}>
<Icon icon={AlertTriangle} size={4} color="text-white" />
<Text weight="semibold" color="text-white">Error Details</Text>
</Box>
<Box p={4}>
<Stack gap={2} fontSize="0.75rem">
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Type:</Text>
<Text colSpan={2} color="text-red-400" weight="bold">{error.type}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Message:</Text>
<Text colSpan={2} color="text-gray-300">{error.message}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Endpoint:</Text>
<Text colSpan={2} color="text-primary-blue">{error.context.endpoint || 'N/A'}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Method:</Text>
<Text colSpan={2} color="text-warning-amber">{error.context.method || 'N/A'}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Status:</Text>
<Text colSpan={2}>{error.context.statusCode || 'N/A'}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Retry Count:</Text>
<Text colSpan={2}>{error.context.retryCount || 0}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Timestamp:</Text>
<Text colSpan={2} color="text-gray-500">{error.context.timestamp}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Retryable:</Text>
<Text colSpan={2} color={error.isRetryable() ? 'text-performance-green' : 'text-red-400'}>
{error.isRetryable() ? 'Yes' : 'No'}
</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Connectivity:</Text>
<Text colSpan={2} color={error.isConnectivityIssue() ? 'text-red-400' : 'text-performance-green'}>
{error.isConnectivityIssue() ? 'Yes' : 'No'}
</Text>
</Box>
{error.context.troubleshooting && (
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Troubleshoot:</Text>
<Text colSpan={2} color="text-warning-amber">{error.context.troubleshooting}</Text>
</Box>
)}
</Stack>
</Box>
</Surface>
{/* Connection Status */}
<Surface variant="muted" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Box bg="bg-charcoal-outline" px={4} py={2} display="flex" alignItems="center" gap={2}>
<Icon icon={Activity} size={4} color="text-white" />
<Text weight="semibold" color="text-white">Connection Health</Text>
</Box>
<Box p={4}>
<Stack gap={2} fontSize="0.75rem">
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Status:</Text>
<Text colSpan={2} weight="bold" color={
connectionStatus.status === 'connected' ? 'text-performance-green' :
connectionStatus.status === 'degraded' ? 'text-warning-amber' :
'text-red-400'
}>
{connectionStatus.status.toUpperCase()}
</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Reliability:</Text>
<Text colSpan={2}>{reliability.toFixed(2)}%</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Total Requests:</Text>
<Text colSpan={2}>{connectionStatus.totalRequests}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Successful:</Text>
<Text colSpan={2} color="text-performance-green">{connectionStatus.successfulRequests}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Failed:</Text>
<Text colSpan={2} color="text-red-400">{connectionStatus.failedRequests}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Consecutive Failures:</Text>
<Text colSpan={2}>{connectionStatus.consecutiveFailures}</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Avg Response:</Text>
<Text colSpan={2}>{connectionStatus.averageResponseTime.toFixed(2)}ms</Text>
</Box>
<Box display="grid" gridCols={3} gap={2}>
<Text color="text-gray-500">Last Check:</Text>
<Text colSpan={2} color="text-gray-500">
{connectionStatus.lastCheck?.toLocaleTimeString() || 'Never'}
</Text>
</Box>
</Stack>
</Box>
</Surface>
</Stack>
{/* Right Column */}
<Stack gap={4}>
{/* Circuit Breakers */}
<Surface variant="muted" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Box bg="bg-charcoal-outline" px={4} py={2} display="flex" alignItems="center" gap={2}>
<Text size="lg"></Text>
<Text weight="semibold" color="text-white">Circuit Breakers</Text>
</Box>
<Box p={4}>
{Object.keys(circuitBreakers).length === 0 ? (
<Box textAlign="center" py={4}>
<Text color="text-gray-500">No circuit breakers active</Text>
</Box>
) : (
<Stack gap={2} maxHeight="12rem" overflow="auto" fontSize="0.75rem">
{Object.entries(circuitBreakers).map(([endpoint, status]) => (
<Box key={endpoint} display="flex" alignItems="center" justifyContent="between" p={2} bg="bg-deep-graphite" rounded="md" border borderColor="border-charcoal-outline">
<Text color="text-primary-blue" truncate flexGrow={1}>{endpoint}</Text>
<Box 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>
</Box>
<Text color="text-gray-500" ml={2}>{status.failures} failures</Text>
</Box>
))}
</Stack>
)}
</Box>
</Surface>
{/* Actions */}
<Surface variant="muted" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Box bg="bg-charcoal-outline" px={4} py={2}>
<Text weight="semibold" color="text-white">Actions</Text>
</Box>
<Box 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>
</Box>
</Surface>
{/* Quick Fixes */}
<Surface variant="muted" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Box bg="bg-charcoal-outline" px={4} py={2}>
<Text weight="semibold" color="text-white">Quick Fixes</Text>
</Box>
<Box p={4}>
<Stack gap={2} fontSize="0.75rem">
<Text color="text-gray-400">Common solutions:</Text>
<Stack as="ul" gap={1} pl={4}>
<Box as="li"><Text color="text-gray-300">Check API server is running</Text></Box>
<Box as="li"><Text color="text-gray-300">Verify CORS configuration</Text></Box>
<Box as="li"><Text color="text-gray-300">Check environment variables</Text></Box>
<Box as="li"><Text color="text-gray-300">Review network connectivity</Text></Box>
<Box as="li"><Text color="text-gray-300">Check API rate limits</Text></Box>
</Stack>
</Stack>
</Box>
</Surface>
{/* Raw Error */}
<Surface variant="muted" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Box bg="bg-charcoal-outline" px={4} py={2}>
<Text weight="semibold" color="text-white">Raw Error</Text>
</Box>
<Box p={4}>
<Box as="pre" p={2} bg="bg-deep-graphite" rounded="md" overflow="auto" maxHeight="8rem" fontSize="0.75rem" color="text-gray-400">
{JSON.stringify({
type: error.type,
message: error.message,
context: error.context,
}, null, 2)}
</Box>
</Box>
</Surface>
</Stack>
</Box>
{/* Console Output */}
<Surface variant="muted" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Box bg="bg-charcoal-outline" px={4} py={2} display="flex" alignItems="center" gap={2}>
<Icon icon={Terminal} size={4} color="text-white" />
<Text weight="semibold" color="text-white">Console Output</Text>
</Box>
<Box p={4} bg="bg-deep-graphite" 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>
</Box>
</Surface>
</Stack>
</Box>
</Box>
);
}