website refactor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import React, { Component, ReactNode, useState } from 'react';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
||||
import { ErrorDisplay } from './ErrorDisplay';
|
||||
@@ -45,7 +45,7 @@ export class ApiErrorBoundary extends Component<Props, State> {
|
||||
throw error;
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
componentDidCatch(error: Error): void {
|
||||
if (error instanceof ApiError) {
|
||||
// Report to connection monitor
|
||||
connectionMonitor.recordFailure(error);
|
||||
@@ -130,8 +130,8 @@ export class ApiErrorBoundary extends Component<Props, State> {
|
||||
* 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 [error, setError] = useState<ApiError | null>(null);
|
||||
const [isDev] = useState(process.env.NODE_ENV === 'development');
|
||||
|
||||
const handleError = (err: ApiError) => {
|
||||
setError(err);
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
'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 { useState, useEffect } from 'react';
|
||||
import { X, RefreshCw, Copy, Terminal, Activity, AlertTriangle } from 'lucide-react';
|
||||
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;
|
||||
@@ -80,268 +88,295 @@ export function DevErrorPanel({ error, onReset }: DevErrorPanelProps) {
|
||||
setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus());
|
||||
};
|
||||
|
||||
const getSeverityColor = (type: string) => {
|
||||
const getSeverityVariant = (): 'danger' | 'warning' | 'info' | 'default' => {
|
||||
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';
|
||||
case 'error': return 'danger';
|
||||
case 'warn': return 'warning';
|
||||
case 'info': return 'info';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<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 */}
|
||||
<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>
|
||||
{/* 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 */}
|
||||
<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>
|
||||
{/* 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 */}
|
||||
<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>
|
||||
{/* 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 */}
|
||||
<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>
|
||||
{/* 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 */}
|
||||
<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>
|
||||
{/* 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 */}
|
||||
<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>
|
||||
{/* 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 */}
|
||||
<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>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { Component, ReactNode, ErrorInfo } from 'react';
|
||||
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import { DevErrorPanel } from './DevErrorPanel';
|
||||
@@ -28,6 +28,15 @@ interface State {
|
||||
isDev: boolean;
|
||||
}
|
||||
|
||||
interface GridPilotWindow extends Window {
|
||||
__GRIDPILOT_REACT_ERRORS__?: Array<{
|
||||
error: Error;
|
||||
errorInfo: ErrorInfo;
|
||||
timestamp: string;
|
||||
componentStack?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced React Error Boundary with maximum developer transparency
|
||||
* Integrates with GlobalErrorHandler and provides detailed debugging info
|
||||
@@ -49,7 +58,7 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
// Don't catch Next.js navigation errors (redirect, notFound, etc.)
|
||||
if (error && typeof error === 'object' && 'digest' in error) {
|
||||
const digest = (error as any).digest;
|
||||
const digest = (error as Record<string, unknown>).digest;
|
||||
if (typeof digest === 'string' && (
|
||||
digest.startsWith('NEXT_REDIRECT') ||
|
||||
digest.startsWith('NEXT_NOT_FOUND')
|
||||
@@ -70,7 +79,7 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
// Don't catch Next.js navigation errors (redirect, notFound, etc.)
|
||||
if (error && typeof error === 'object' && 'digest' in error) {
|
||||
const digest = (error as any).digest;
|
||||
const digest = (error as Record<string, unknown>).digest;
|
||||
if (typeof digest === 'string' && (
|
||||
digest.startsWith('NEXT_REDIRECT') ||
|
||||
digest.startsWith('NEXT_NOT_FOUND')
|
||||
@@ -81,21 +90,26 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
|
||||
// Add to React error history
|
||||
const reactErrors = (window as any).__GRIDPILOT_REACT_ERRORS__ || [];
|
||||
reactErrors.push({
|
||||
error,
|
||||
errorInfo,
|
||||
timestamp: new Date().toISOString(),
|
||||
componentStack: errorInfo.componentStack,
|
||||
});
|
||||
(window as any).__GRIDPILOT_REACT_ERRORS__ = reactErrors;
|
||||
if (typeof window !== 'undefined') {
|
||||
const gpWindow = window as unknown as GridPilotWindow;
|
||||
const reactErrors = gpWindow.__GRIDPILOT_REACT_ERRORS__ || [];
|
||||
gpWindow.__GRIDPILOT_REACT_ERRORS__ = [
|
||||
...reactErrors,
|
||||
{
|
||||
error,
|
||||
errorInfo,
|
||||
timestamp: new Date().toISOString(),
|
||||
componentStack: errorInfo.componentStack || undefined,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Report to global error handler with enhanced context
|
||||
const enhancedContext = {
|
||||
...this.props.context,
|
||||
source: 'react_error_boundary',
|
||||
componentStack: errorInfo.componentStack,
|
||||
reactVersion: React.version,
|
||||
reactVersion: version,
|
||||
componentName: this.getComponentName(errorInfo),
|
||||
};
|
||||
|
||||
@@ -107,12 +121,6 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
this.props.onError(error, errorInfo);
|
||||
}
|
||||
|
||||
// Show dev overlay if enabled
|
||||
if (this.props.enableDevOverlay && this.state.isDev) {
|
||||
// The global handler will show the overlay, but we can add additional React-specific info
|
||||
this.showReactDevOverlay(error, errorInfo);
|
||||
}
|
||||
|
||||
// Log to console with maximum detail
|
||||
if (this.state.isDev) {
|
||||
this.logReactErrorWithMaximumDetail(error, errorInfo);
|
||||
@@ -126,10 +134,6 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
// Clean up if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract component name from error info
|
||||
*/
|
||||
@@ -146,108 +150,6 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show React-specific dev overlay
|
||||
*/
|
||||
private showReactDevOverlay(error: Error, errorInfo: ErrorInfo): void {
|
||||
const existingOverlay = document.getElementById('gridpilot-react-overlay');
|
||||
if (existingOverlay) {
|
||||
this.updateReactDevOverlay(existingOverlay, error, errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'gridpilot-react-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
z-index: 999998;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
border: 3px solid #ff6600;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 40px rgba(255, 102, 0, 0.6);
|
||||
`;
|
||||
|
||||
this.updateReactDevOverlay(overlay, error, errorInfo);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Add keyboard shortcut to dismiss
|
||||
const dismissHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
overlay.remove();
|
||||
document.removeEventListener('keydown', dismissHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', dismissHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update React dev overlay
|
||||
*/
|
||||
private updateReactDevOverlay(overlay: HTMLElement, error: Error, errorInfo: ErrorInfo): void {
|
||||
overlay.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 15px;">
|
||||
<h2 style="color: #ff6600; margin: 0; font-size: 18px;">⚛️ React Component Error</h2>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
style="background: #ff6600; color: white; border: none; padding: 6px 12px; cursor: pointer; border-radius: 4px; font-weight: bold;">
|
||||
CLOSE
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #ff6600;">
|
||||
<div style="color: #ff6600; font-weight: bold; margin-bottom: 5px;">Error Message</div>
|
||||
<div style="color: #fff;">${error.message}</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #00aaff;">
|
||||
<div style="color: #00aaff; font-weight: bold; margin-bottom: 5px;">Component Stack Trace</div>
|
||||
<pre style="margin: 0; white-space: pre-wrap; color: #888;">${errorInfo.componentStack || 'No component stack available'}</pre>
|
||||
</div>
|
||||
|
||||
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #ffaa00;">
|
||||
<div style="color: #ffaa00; font-weight: bold; margin-bottom: 5px;">JavaScript Stack Trace</div>
|
||||
<pre style="margin: 0; white-space: pre-wrap; color: #888; overflow-x: auto;">${error.stack || 'No stack trace available'}</pre>
|
||||
</div>
|
||||
|
||||
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #00ff88;">
|
||||
<div style="color: #00ff88; font-weight: bold; margin-bottom: 5px;">React Information</div>
|
||||
<div style="line-height: 1.6; color: #888;">
|
||||
<div>React Version: ${React.version}</div>
|
||||
<div>Error Boundary: Active</div>
|
||||
<div>Timestamp: ${new Date().toLocaleTimeString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #000; padding: 12px; border-radius: 4px; border-left: 3px solid #aa00ff;">
|
||||
<div style="color: #aa00ff; font-weight: bold; margin-bottom: 5px;">Quick Actions</div>
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button onclick="navigator.clipboard.writeText(\`${error.message}\n\nComponent Stack:\n${errorInfo.componentStack}\n\nStack:\n${error.stack}\`)"
|
||||
style="background: #0066cc; color: white; border: none; padding: 6px 10px; cursor: pointer; border-radius: 4px; font-size: 11px;">
|
||||
📋 Copy Details
|
||||
</button>
|
||||
<button onclick="window.location.reload()"
|
||||
style="background: #cc6600; color: white; border: none; padding: 6px 10px; cursor: pointer; border-radius: 4px; font-size: 11px;">
|
||||
🔄 Reload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; padding: 10px; background: #222; border-radius: 4px; border-left: 3px solid #888; font-size: 11px; color: #888;">
|
||||
💡 This React error boundary caught a component rendering error. Check the console for additional details from the global error handler.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log React error with maximum detail
|
||||
*/
|
||||
@@ -265,7 +167,7 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
console.log('Component Stack:', errorInfo.componentStack);
|
||||
|
||||
console.log('React Context:', {
|
||||
reactVersion: React.version,
|
||||
reactVersion: version,
|
||||
component: this.getComponentName(errorInfo),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
@@ -273,28 +175,9 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
console.log('Props:', this.props);
|
||||
console.log('State:', this.state);
|
||||
|
||||
// Show component hierarchy if available
|
||||
try {
|
||||
const hierarchy = this.getComponentHierarchy();
|
||||
if (hierarchy) {
|
||||
console.log('Component Hierarchy:', hierarchy);
|
||||
}
|
||||
} catch {
|
||||
// Ignore hierarchy extraction errors
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to extract component hierarchy (for debugging)
|
||||
*/
|
||||
private getComponentHierarchy(): string | null {
|
||||
// This is a simplified version - in practice, you might want to use React DevTools
|
||||
// or other methods to get the full component tree
|
||||
return null;
|
||||
}
|
||||
|
||||
resetError = (): void => {
|
||||
this.setState({ hasError: false, error: null, errorInfo: null });
|
||||
if (this.props.onReset) {
|
||||
@@ -350,9 +233,9 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
|
||||
* Hook-based alternative for functional components
|
||||
*/
|
||||
export function useEnhancedErrorBoundary() {
|
||||
const [error, setError] = React.useState<Error | ApiError | null>(null);
|
||||
const [errorInfo, setErrorInfo] = React.useState<ErrorInfo | null>(null);
|
||||
const [isDev] = React.useState(process.env.NODE_ENV === 'development');
|
||||
const [error, setError] = useState<Error | ApiError | null>(null);
|
||||
const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null);
|
||||
const [isDev] = useState(process.env.NODE_ENV === 'development');
|
||||
|
||||
const handleError = (err: Error, info: ErrorInfo) => {
|
||||
setError(err);
|
||||
@@ -402,4 +285,4 @@ export function withEnhancedErrorBoundary<P extends object>(
|
||||
);
|
||||
WrappedComponent.displayName = `withEnhancedErrorBoundary(${Component.displayName || Component.name || 'Component'})`;
|
||||
return WrappedComponent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -15,13 +15,18 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { parseApiError, getErrorSeverity, isRetryable, isConnectivityError } from '@/lib/utils/errorUtils';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
interface EnhancedFormErrorProps {
|
||||
error: unknown;
|
||||
onRetry?: () => void;
|
||||
onDismiss?: () => void;
|
||||
showDeveloperDetails?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +40,6 @@ export function EnhancedFormError({
|
||||
onRetry,
|
||||
onDismiss,
|
||||
showDeveloperDetails = process.env.NODE_ENV === 'development',
|
||||
className = ''
|
||||
}: EnhancedFormErrorProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const parsed = parseApiError(error);
|
||||
@@ -44,10 +48,10 @@ export function EnhancedFormError({
|
||||
const connectivity = isConnectivityError(error);
|
||||
|
||||
const getIcon = () => {
|
||||
if (connectivity) return <Wifi className="w-5 h-5" />;
|
||||
if (severity === 'error') return <AlertTriangle className="w-5 h-5" />;
|
||||
if (severity === 'warning') return <AlertCircle className="w-5 h-5" />;
|
||||
return <Info className="w-5 h-5" />;
|
||||
if (connectivity) return Wifi;
|
||||
if (severity === 'error') return AlertTriangle;
|
||||
if (severity === 'warning') return AlertCircle;
|
||||
return Info;
|
||||
};
|
||||
|
||||
const getColor = () => {
|
||||
@@ -62,179 +66,165 @@ export function EnhancedFormError({
|
||||
const color = getColor();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className={`bg-${color}-500/10 border-${color}-500/30 rounded-lg overflow-hidden ${className}`}
|
||||
>
|
||||
{/* Main Error Message */}
|
||||
<div className="p-4 flex items-start gap-3">
|
||||
<div className={`text-${color}-400 flex-shrink-0 mt-0.5`}>
|
||||
{getIcon()}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className={`text-sm font-medium text-${color}-200`}>
|
||||
{parsed.userMessage}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{retryable && onRetry && (
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="p-1.5 hover:bg-white/5 rounded transition-colors"
|
||||
title="Retry"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
)}
|
||||
<Box
|
||||
bg={`bg-${color}-500/10`}
|
||||
border
|
||||
borderColor={`border-${color}-500/30`}
|
||||
rounded="lg"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Main Error Message */}
|
||||
<Box p={4} display="flex" alignItems="start" gap={3}>
|
||||
<Box color={`text-${color}-400`} flexShrink={0} mt={0.5}>
|
||||
<Icon icon={getIcon()} size={5} />
|
||||
</Box>
|
||||
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" justifyContent="between" gap={2}>
|
||||
<Text size="sm" weight="medium" color={`text-${color}-200`}>
|
||||
{parsed.userMessage}
|
||||
</Text>
|
||||
|
||||
{onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="p-1.5 hover:bg-white/5 rounded transition-colors"
|
||||
title="Dismiss"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showDeveloperDetails && (
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="p-1.5 hover:bg-white/5 rounded transition-colors"
|
||||
title="Toggle technical details"
|
||||
>
|
||||
{showDetails ? (
|
||||
<ChevronUp className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
{retryable && onRetry && (
|
||||
<IconButton
|
||||
icon={RefreshCw}
|
||||
onClick={onRetry}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Retry"
|
||||
/>
|
||||
)}
|
||||
|
||||
{onDismiss && (
|
||||
<IconButton
|
||||
icon={X}
|
||||
onClick={onDismiss}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Dismiss"
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeveloperDetails && (
|
||||
<IconButton
|
||||
icon={showDetails ? ChevronUp : ChevronDown}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Toggle technical details"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Validation Errors List */}
|
||||
{parsed.isValidationError && parsed.validationErrors.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{parsed.validationErrors.map((validationError, index) => (
|
||||
<div key={index} className="text-xs text-${color}-300/80">
|
||||
• {validationError.field}: {validationError.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Validation Errors List */}
|
||||
{parsed.isValidationError && parsed.validationErrors.length > 0 && (
|
||||
<Stack gap={1} mt={2}>
|
||||
{parsed.validationErrors.map((validationError, index) => (
|
||||
<Text key={index} size="xs" color={`text-${color}-300/80`} block>
|
||||
• {validationError.field}: {validationError.message}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Action Hint */}
|
||||
<Box mt={2}>
|
||||
<Text size="xs" color="text-gray-400">
|
||||
{connectivity && "Check your internet connection and try again"}
|
||||
{parsed.isValidationError && "Please review your input and try again"}
|
||||
{retryable && !connectivity && !parsed.isValidationError && "Please try again in a moment"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Developer Details */}
|
||||
<AnimatePresence>
|
||||
{showDetails && (
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
>
|
||||
<Box borderTop borderColor={`border-${color}-500/20`} bg="bg-black/20" p={4}>
|
||||
<Stack gap={3} fontSize="0.75rem">
|
||||
<Box display="flex" alignItems="center" gap={2} color="text-gray-400">
|
||||
<Icon icon={Bug} size={3} />
|
||||
<Text weight="semibold">Developer Details</Text>
|
||||
</Box>
|
||||
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Error Type:</Text>
|
||||
<Text color="text-white">{error instanceof ApiError ? error.type : 'Unknown'}</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Developer Message:</Text>
|
||||
<Text color="text-white" transform="break-all">{parsed.developerMessage}</Text>
|
||||
</Stack>
|
||||
|
||||
{error instanceof ApiError && error.context.endpoint && (
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Endpoint:</Text>
|
||||
<Text color="text-white">{error.context.method} {error.context.endpoint}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.statusCode && (
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Status Code:</Text>
|
||||
<Text color="text-white">{error.context.statusCode}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Box pt={2} borderTop borderColor="border-charcoal-outline/50">
|
||||
<Text color="text-gray-500" block mb={1}>Quick Actions:</Text>
|
||||
<Box display="flex" gap={2}>
|
||||
{retryable && onRetry && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onRetry}
|
||||
size="sm"
|
||||
bg="bg-blue-600/20"
|
||||
color="text-primary-blue"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (error instanceof Error) {
|
||||
console.error('Full error details:', error);
|
||||
if (error.stack) {
|
||||
console.log('Stack trace:', error.stack);
|
||||
}
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
bg="bg-purple-600/20"
|
||||
color="text-purple-400"
|
||||
>
|
||||
Log to Console
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Action Hint */}
|
||||
<div className="mt-2 text-xs text-gray-400">
|
||||
{connectivity && "Check your internet connection and try again"}
|
||||
{parsed.isValidationError && "Please review your input and try again"}
|
||||
{retryable && !connectivity && !parsed.isValidationError && "Please try again in a moment"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Developer Details */}
|
||||
<AnimatePresence>
|
||||
{showDeveloperDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-${color}-500/20 bg-black/20"
|
||||
>
|
||||
<div className="p-4 space-y-3 text-xs font-mono">
|
||||
<div className="flex items-center gap-2 text-gray-400">
|
||||
<Bug className="w-3 h-3" />
|
||||
<span className="font-semibold">Developer Details</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Error Type:</div>
|
||||
<div className="text-white">{error instanceof ApiError ? error.type : 'Unknown'}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Developer Message:</div>
|
||||
<div className="text-white break-all">{parsed.developerMessage}</div>
|
||||
</div>
|
||||
|
||||
{error instanceof ApiError && error.context.endpoint && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Endpoint:</div>
|
||||
<div className="text-white">{error.context.method} {error.context.endpoint}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.statusCode && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Status Code:</div>
|
||||
<div className="text-white">{error.context.statusCode}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.retryCount !== undefined && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Retry Count:</div>
|
||||
<div className="text-white">{error.context.retryCount}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.timestamp && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Timestamp:</div>
|
||||
<div className="text-white">{error.context.timestamp}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.troubleshooting && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Troubleshooting:</div>
|
||||
<div className="text-yellow-400">{error.context.troubleshooting}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{parsed.validationErrors.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Validation Errors:</div>
|
||||
<div className="text-white">{JSON.stringify(parsed.validationErrors, null, 2)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 border-t border-gray-700/50">
|
||||
<div className="text-gray-500 mb-1">Quick Actions:</div>
|
||||
<div className="flex gap-2">
|
||||
{retryable && onRetry && (
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="px-2 py-1 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 rounded transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (error instanceof Error) {
|
||||
console.error('Full error details:', error);
|
||||
if (error.stack) {
|
||||
console.log('Stack trace:', error.stack);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 bg-purple-600/20 hover:bg-purple-600/30 text-purple-400 rounded transition-colors"
|
||||
>
|
||||
Log to Console
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,30 +248,33 @@ export function FormErrorSummary({
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 flex items-start gap-2"
|
||||
>
|
||||
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-red-200">{summary.title}</div>
|
||||
<div className="text-xs text-red-300/80 mt-0.5">{summary.description}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{summary.action}</div>
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="p-1 hover:bg-red-500/10 rounded transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5 text-red-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<Box bg="bg-red-500/10" border borderColor="border-red-500/30" rounded="lg" p={3} display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={AlertCircle} size={4} color="rgb(239, 68, 68)" mt={0.5} />
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" justifyContent="between" gap={2}>
|
||||
<Box>
|
||||
<Text size="sm" weight="medium" color="text-red-200" block>{summary.title}</Text>
|
||||
<Text size="xs" color="text-red-300/80" block mt={0.5}>{summary.description}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>{summary.action}</Text>
|
||||
</Box>
|
||||
{onDismiss && (
|
||||
<IconButton
|
||||
icon={X}
|
||||
onClick={onDismiss}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="rgb(239, 68, 68)"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { getErrorAnalyticsStats, type ErrorStats } from '@/lib/services/error/ErrorAnalyticsService';
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Copy,
|
||||
RefreshCw,
|
||||
Terminal,
|
||||
Database,
|
||||
Zap,
|
||||
Bug,
|
||||
Shield,
|
||||
Globe,
|
||||
Cpu,
|
||||
FileText,
|
||||
Trash2,
|
||||
Download,
|
||||
Search
|
||||
Search,
|
||||
ChevronDown,
|
||||
Zap,
|
||||
Terminal
|
||||
} from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Input } from '@/ui/Input';
|
||||
|
||||
interface ErrorAnalyticsDashboardProps {
|
||||
/**
|
||||
@@ -34,27 +41,32 @@ interface ErrorAnalyticsDashboardProps {
|
||||
showInProduction?: boolean;
|
||||
}
|
||||
|
||||
interface ErrorStats {
|
||||
totalErrors: number;
|
||||
errorsByType: Record<string, number>;
|
||||
errorsByTime: Array<{ time: string; count: number }>;
|
||||
recentErrors: Array<{
|
||||
timestamp: string;
|
||||
message: string;
|
||||
type: string;
|
||||
context?: unknown;
|
||||
}>;
|
||||
apiStats: {
|
||||
totalRequests: number;
|
||||
successful: number;
|
||||
failed: number;
|
||||
averageDuration: number;
|
||||
slowestRequests: Array<{ url: string; duration: number }>;
|
||||
function formatDuration(duration: number): string {
|
||||
return duration.toFixed(2) + 'ms';
|
||||
}
|
||||
|
||||
function formatPercentage(value: number, total: number): string {
|
||||
if (total === 0) return '0%';
|
||||
return ((value / total) * 100).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
function formatMemory(bytes: number): string {
|
||||
return (bytes / 1024 / 1024).toFixed(1) + 'MB';
|
||||
}
|
||||
|
||||
interface PerformanceWithMemory extends Performance {
|
||||
memory?: {
|
||||
usedJSHeapSize: number;
|
||||
totalJSHeapSize: number;
|
||||
jsHeapSizeLimit: number;
|
||||
};
|
||||
environment: {
|
||||
mode: string;
|
||||
version?: string;
|
||||
buildTime?: string;
|
||||
}
|
||||
|
||||
interface NavigatorWithConnection extends Navigator {
|
||||
connection?: {
|
||||
effectiveType: string;
|
||||
downlink: number;
|
||||
rtt: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,77 +85,32 @@ export function ErrorAnalyticsDashboard({
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const shouldShow = isDev || showInProduction;
|
||||
|
||||
// Don't show in production unless explicitly enabled
|
||||
if (!isDev && !showInProduction) {
|
||||
const perf = typeof performance !== 'undefined' ? performance as PerformanceWithMemory : null;
|
||||
const nav = typeof navigator !== 'undefined' ? navigator as NavigatorWithConnection : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldShow) return;
|
||||
|
||||
const update = () => {
|
||||
setStats(getErrorAnalyticsStats());
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(update, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [refreshInterval, shouldShow]);
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateStats();
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(updateStats, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [refreshInterval]);
|
||||
|
||||
const updateStats = () => {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
const errorHistory = globalHandler.getErrorHistory();
|
||||
const errorStats = globalHandler.getStats();
|
||||
const apiHistory = apiLogger.getHistory();
|
||||
const apiStats = apiLogger.getStats();
|
||||
|
||||
// Group errors by time (last 10 minutes)
|
||||
const timeGroups = new Map<string, number>();
|
||||
const now = Date.now();
|
||||
const tenMinutesAgo = now - (10 * 60 * 1000);
|
||||
|
||||
errorHistory.forEach(entry => {
|
||||
const entryTime = new Date(entry.timestamp).getTime();
|
||||
if (entryTime >= tenMinutesAgo) {
|
||||
const timeKey = new Date(entry.timestamp).toLocaleTimeString();
|
||||
timeGroups.set(timeKey, (timeGroups.get(timeKey) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
const errorsByTime = Array.from(timeGroups.entries())
|
||||
.map(([time, count]) => ({ time, count }))
|
||||
.sort((a, b) => a.time.localeCompare(b.time));
|
||||
|
||||
const recentErrors = errorHistory.slice(-10).reverse().map(entry => ({
|
||||
timestamp: entry.timestamp,
|
||||
message: entry.error.message,
|
||||
type: entry.error instanceof ApiError ? entry.error.type : entry.error.name || 'Error',
|
||||
context: entry.context,
|
||||
}));
|
||||
|
||||
const slowestRequests = apiLogger.getSlowestRequests(5).map(log => ({
|
||||
url: log.url,
|
||||
duration: log.response?.duration || 0,
|
||||
}));
|
||||
|
||||
setStats({
|
||||
totalErrors: errorStats.total,
|
||||
errorsByType: errorStats.byType,
|
||||
errorsByTime,
|
||||
recentErrors,
|
||||
apiStats: {
|
||||
totalRequests: apiStats.total,
|
||||
successful: apiStats.successful,
|
||||
failed: apiStats.failed,
|
||||
averageDuration: apiStats.averageDuration,
|
||||
slowestRequests,
|
||||
},
|
||||
environment: {
|
||||
mode: process.env.NODE_ENV || 'unknown',
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
|
||||
},
|
||||
});
|
||||
const updateStatsManual = () => {
|
||||
setStats(getErrorAnalyticsStats());
|
||||
};
|
||||
|
||||
const copyToClipboard = async (data: unknown) => {
|
||||
@@ -183,7 +150,7 @@ export function ErrorAnalyticsDashboard({
|
||||
|
||||
globalHandler.clearHistory();
|
||||
apiLogger.clearHistory();
|
||||
updateStats();
|
||||
updateStatsManual();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -195,382 +162,438 @@ export function ErrorAnalyticsDashboard({
|
||||
|
||||
if (!isExpanded) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="fixed bottom-4 left-4 z-50 p-3 bg-iron-gray border border-charcoal-outline rounded-full shadow-lg hover:bg-charcoal-outline transition-colors"
|
||||
title="Open Error Analytics"
|
||||
>
|
||||
<Activity className="w-5 h-5 text-red-400" />
|
||||
</button>
|
||||
<Box position="fixed" bottom="4" left="4" zIndex={50}>
|
||||
<IconButton
|
||||
icon={Activity}
|
||||
onClick={() => setIsExpanded(true)}
|
||||
variant="secondary"
|
||||
title="Open Error Analytics"
|
||||
size="lg"
|
||||
color="rgb(239, 68, 68)"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 z-50 w-96 max-h-[80vh] bg-deep-graphite border border-charcoal-outline rounded-xl shadow-2xl overflow-hidden flex flex-col">
|
||||
<Box
|
||||
position="fixed"
|
||||
bottom="4"
|
||||
left="4"
|
||||
zIndex={50}
|
||||
w="96"
|
||||
bg="bg-deep-graphite"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
rounded="xl"
|
||||
shadow="2xl"
|
||||
overflow="hidden"
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
maxHeight="80vh"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-iron-gray/50 border-b border-charcoal-outline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-red-400" />
|
||||
<span className="text-sm font-semibold text-white">Error Analytics</span>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" px={4} py={3} bg="bg-iron-gray/50" borderBottom borderColor="border-charcoal-outline">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Activity} size={4} color="rgb(239, 68, 68)" />
|
||||
<Text size="sm" weight="semibold" color="text-white">Error Analytics</Text>
|
||||
{isDev && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-red-500/20 text-red-400 rounded">
|
||||
<Badge variant="danger" size="xs">
|
||||
DEV
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={updateStats}
|
||||
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<IconButton
|
||||
icon={RefreshCw}
|
||||
onClick={updateStatsManual}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Refresh"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<IconButton
|
||||
icon={ChevronDown}
|
||||
onClick={() => setIsExpanded(false)}
|
||||
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Minimize"
|
||||
>
|
||||
<span className="text-gray-400 text-xs font-bold">_</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-charcoal-outline bg-iron-gray/30">
|
||||
<Box display="flex" borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/30">
|
||||
{[
|
||||
{ id: 'errors', label: 'Errors', icon: AlertTriangle },
|
||||
{ id: 'api', label: 'API', icon: Globe },
|
||||
{ id: 'environment', label: 'Env', icon: Cpu },
|
||||
{ id: 'raw', label: 'Raw', icon: FileText },
|
||||
].map(tab => (
|
||||
<button
|
||||
<Box
|
||||
key={tab.id}
|
||||
onClick={() => setSelectedTab(tab.id as any)}
|
||||
className={`flex-1 flex items-center justify-center gap-1 px-2 py-2 text-xs font-medium transition-colors ${
|
||||
selectedTab === tab.id
|
||||
? 'bg-deep-graphite text-white border-b-2 border-red-400'
|
||||
: 'text-gray-400 hover:bg-charcoal-outline hover:text-gray-200'
|
||||
}`}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => setSelectedTab(tab.id as 'errors' | 'api' | 'environment' | 'raw')}
|
||||
display="flex"
|
||||
flexGrow={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={1}
|
||||
px={2}
|
||||
py={2}
|
||||
cursor="pointer"
|
||||
transition
|
||||
bg={selectedTab === tab.id ? 'bg-deep-graphite' : ''}
|
||||
borderBottom={selectedTab === tab.id}
|
||||
borderColor={selectedTab === tab.id ? 'border-red-400' : ''}
|
||||
borderWidth={selectedTab === tab.id ? '2px' : '0'}
|
||||
>
|
||||
<tab.icon className="w-3 h-3" />
|
||||
{tab.label}
|
||||
</button>
|
||||
<Icon icon={tab.icon} size={3} color={selectedTab === tab.id ? 'text-white' : 'text-gray-400'} />
|
||||
<Text
|
||||
size="xs"
|
||||
weight="medium"
|
||||
color={selectedTab === tab.id ? 'text-white' : 'text-gray-400'}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-4 space-y-4">
|
||||
{/* Search Bar */}
|
||||
{selectedTab === 'errors' && (
|
||||
<div className="relative">
|
||||
<input
|
||||
<Box flexGrow={1} overflow="auto" p={4}>
|
||||
<Stack gap={4}>
|
||||
{/* Search Bar */}
|
||||
{selectedTab === 'errors' && (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search errors..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-iron-gray border border-charcoal-outline rounded px-3 py-2 pl-8 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-red-400"
|
||||
icon={<Icon icon={Search} size={3} color="rgb(107, 114, 128)" />}
|
||||
/>
|
||||
<Search className="w-3 h-3 text-gray-500 absolute left-2.5 top-2.5" />
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Errors Tab */}
|
||||
{selectedTab === 'errors' && stats && (
|
||||
<div className="space-y-4">
|
||||
{/* Error Summary */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs text-gray-500">Total Errors</div>
|
||||
<div className="text-xl font-bold text-red-400">{stats.totalErrors}</div>
|
||||
</div>
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs text-gray-500">Error Types</div>
|
||||
<div className="text-xl font-bold text-yellow-400">
|
||||
{Object.keys(stats.errorsByType).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Errors Tab */}
|
||||
{selectedTab === 'errors' && stats && (
|
||||
<Stack gap={4}>
|
||||
{/* Error Summary */}
|
||||
<Box display="grid" gridCols={2} gap={2}>
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Text size="xs" color="text-gray-500" block>Total Errors</Text>
|
||||
<Text size="xl" weight="bold" color="text-red-400">{stats.totalErrors}</Text>
|
||||
</Box>
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Text size="xs" color="text-gray-500" block>Error Types</Text>
|
||||
<Text size="xl" weight="bold" color="text-warning-amber">
|
||||
{Object.keys(stats.errorsByType).length}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Error Types Breakdown */}
|
||||
{Object.keys(stats.errorsByType).length > 0 && (
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Bug className="w-3 h-3" /> Error Types
|
||||
</div>
|
||||
<div className="space-y-1 max-h-32 overflow-auto">
|
||||
{Object.entries(stats.errorsByType).map(([type, count]) => (
|
||||
<div key={type} className="flex justify-between text-xs">
|
||||
<span className="text-gray-300">{type}</span>
|
||||
<span className="text-red-400 font-mono">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Error Types Breakdown */}
|
||||
{Object.keys(stats.errorsByType).length > 0 && (
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Bug} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Error Types</Text>
|
||||
</Box>
|
||||
<Stack gap={1} maxHeight="8rem" overflow="auto">
|
||||
{Object.entries(stats.errorsByType).map(([type, count]) => (
|
||||
<Box key={type} display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-300">{type}</Text>
|
||||
<Text size="xs" color="text-red-400" font="mono">{count}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Recent Errors */}
|
||||
{filteredRecentErrors.length > 0 && (
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<AlertTriangle className="w-3 h-3" /> Recent Errors
|
||||
</div>
|
||||
<div className="space-y-2 max-h-64 overflow-auto">
|
||||
{filteredRecentErrors.map((error, idx) => (
|
||||
<div key={idx} className="bg-deep-graphite border border-charcoal-outline rounded p-2 text-xs">
|
||||
<div className="flex justify-between items-start gap-2 mb-1">
|
||||
<span className="font-mono text-red-400 font-bold">{error.type}</span>
|
||||
<span className="text-gray-500 text-[10px]">
|
||||
{new Date(error.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-300 break-words mb-1">{error.message}</div>
|
||||
<button
|
||||
onClick={() => copyToClipboard(error)}
|
||||
className="text-[10px] text-gray-500 hover:text-gray-300 flex items-center gap-1"
|
||||
>
|
||||
<Copy className="w-3 h-3" /> Copy Details
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Recent Errors */}
|
||||
{filteredRecentErrors.length > 0 && (
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={AlertTriangle} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Recent Errors</Text>
|
||||
</Box>
|
||||
<Stack gap={2} maxHeight="16rem" overflow="auto">
|
||||
{filteredRecentErrors.map((error, idx) => (
|
||||
<Box key={idx} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="md" p={2}>
|
||||
<Box display="flex" justifyContent="between" alignItems="start" gap={2} mb={1}>
|
||||
<Text size="xs" font="mono" weight="bold" color="text-red-400" truncate>{error.type}</Text>
|
||||
<Text size="xs" color="text-gray-500" fontSize="10px">
|
||||
{new Date(error.timestamp).toLocaleTimeString()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text size="xs" color="text-gray-300" block mb={1}>{error.message}</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(error)}
|
||||
size="sm"
|
||||
p={0}
|
||||
minHeight="0"
|
||||
icon={<Icon icon={Copy} size={3} />}
|
||||
>
|
||||
<Text size="xs" color="text-gray-500" fontSize="10px">Copy Details</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Error Timeline */}
|
||||
{stats.errorsByTime.length > 0 && (
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" /> Last 10 Minutes
|
||||
</div>
|
||||
<div className="space-y-1 max-h-32 overflow-auto">
|
||||
{stats.errorsByTime.map((point, idx) => (
|
||||
<div key={idx} className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{point.time}</span>
|
||||
<span className="text-red-400 font-mono">{point.count} errors</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Error Timeline */}
|
||||
{stats.errorsByTime.length > 0 && (
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Clock} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Last 10 Minutes</Text>
|
||||
</Box>
|
||||
<Stack gap={1} maxHeight="8rem" overflow="auto">
|
||||
{stats.errorsByTime.map((point, idx) => (
|
||||
<Box key={idx} display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">{point.time}</Text>
|
||||
<Text size="xs" color="text-red-400" font="mono">{point.count} errors</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* API Tab */}
|
||||
{selectedTab === 'api' && stats && (
|
||||
<div className="space-y-4">
|
||||
{/* API Summary */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs text-gray-500">Total Requests</div>
|
||||
<div className="text-xl font-bold text-blue-400">{stats.apiStats.totalRequests}</div>
|
||||
</div>
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs text-gray-500">Success Rate</div>
|
||||
<div className="text-xl font-bold text-green-400">
|
||||
{stats.apiStats.totalRequests > 0
|
||||
? ((stats.apiStats.successful / stats.apiStats.totalRequests) * 100).toFixed(1)
|
||||
: 0}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* API Tab */}
|
||||
{selectedTab === 'api' && stats && (
|
||||
<Stack gap={4}>
|
||||
{/* API Summary */}
|
||||
<Box display="grid" gridCols={2} gap={2}>
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Text size="xs" color="text-gray-500" block>Total Requests</Text>
|
||||
<Text size="xl" weight="bold" color="text-primary-blue">{stats.apiStats.totalRequests}</Text>
|
||||
</Box>
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Text size="xs" color="text-gray-500" block>Success Rate</Text>
|
||||
<Text size="xl" weight="bold" color="text-performance-green">
|
||||
{formatPercentage(stats.apiStats.successful, stats.apiStats.totalRequests)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* API Stats */}
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Globe className="w-3 h-3" /> API Metrics
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Successful</span>
|
||||
<span className="text-green-400 font-mono">{stats.apiStats.successful}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Failed</span>
|
||||
<span className="text-red-400 font-mono">{stats.apiStats.failed}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Avg Duration</span>
|
||||
<span className="text-yellow-400 font-mono">{stats.apiStats.averageDuration.toFixed(2)}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* API Stats */}
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Globe} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">API Metrics</Text>
|
||||
</Box>
|
||||
<Stack gap={1}>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Successful</Text>
|
||||
<Text size="xs" color="text-performance-green" font="mono">{stats.apiStats.successful}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Failed</Text>
|
||||
<Text size="xs" color="text-red-400" font="mono">{stats.apiStats.failed}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Avg Duration</Text>
|
||||
<Text size="xs" color="text-warning-amber" font="mono">{formatDuration(stats.apiStats.averageDuration)}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Slowest Requests */}
|
||||
{stats.apiStats.slowestRequests.length > 0 && (
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Zap className="w-3 h-3" /> Slowest Requests
|
||||
</div>
|
||||
<div className="space-y-1 max-h-40 overflow-auto">
|
||||
{stats.apiStats.slowestRequests.map((req, idx) => (
|
||||
<div key={idx} className="flex justify-between text-xs bg-deep-graphite p-1.5 rounded border border-charcoal-outline">
|
||||
<span className="text-gray-300 truncate flex-1">{req.url}</span>
|
||||
<span className="text-red-400 font-mono ml-2">{req.duration.toFixed(2)}ms</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Slowest Requests */}
|
||||
{stats.apiStats.slowestRequests.length > 0 && (
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Zap} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Slowest Requests</Text>
|
||||
</Box>
|
||||
<Stack gap={1} maxHeight="10rem" overflow="auto">
|
||||
{stats.apiStats.slowestRequests.map((req, idx) => (
|
||||
<Box key={idx} display="flex" justifyContent="between" bg="bg-deep-graphite" p={1.5} rounded="sm" border borderColor="border-charcoal-outline">
|
||||
<Text size="xs" color="text-gray-300" truncate flexGrow={1}>{req.url}</Text>
|
||||
<Text size="xs" color="text-red-400" font="mono" ml={2}>{formatDuration(req.duration)}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Environment Tab */}
|
||||
{selectedTab === 'environment' && stats && (
|
||||
<div className="space-y-4">
|
||||
{/* Environment Info */}
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Cpu className="w-3 h-3" /> Environment
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Node Environment</span>
|
||||
<span className={`font-mono font-bold ${
|
||||
stats.environment.mode === 'development' ? 'text-green-400' : 'text-yellow-400'
|
||||
}`}>{stats.environment.mode}</span>
|
||||
</div>
|
||||
{stats.environment.version && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Version</span>
|
||||
<span className="text-gray-300 font-mono">{stats.environment.version}</span>
|
||||
</div>
|
||||
)}
|
||||
{stats.environment.buildTime && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Build Time</span>
|
||||
<span className="text-gray-500 font-mono text-[10px]">{stats.environment.buildTime}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Environment Tab */}
|
||||
{selectedTab === 'environment' && stats && (
|
||||
<Stack gap={4}>
|
||||
{/* Environment Info */}
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Cpu} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Environment</Text>
|
||||
</Box>
|
||||
<Stack gap={1}>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Node Environment</Text>
|
||||
<Text size="xs" font="mono" weight="bold" color={stats.environment.mode === 'development' ? 'text-performance-green' : 'text-warning-amber'}>
|
||||
{stats.environment.mode}
|
||||
</Text>
|
||||
</Box>
|
||||
{stats.environment.version && (
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Version</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{stats.environment.version}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{stats.environment.buildTime && (
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Build Time</Text>
|
||||
<Text size="xs" color="text-gray-500" font="mono" fontSize="10px">{stats.environment.buildTime}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Browser Info */}
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Globe className="w-3 h-3" /> Browser
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">User Agent</span>
|
||||
<span className="text-gray-300 text-[9px] truncate max-w-[150px]">{navigator.userAgent}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Language</span>
|
||||
<span className="text-gray-300 font-mono">{navigator.language}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Platform</span>
|
||||
<span className="text-gray-300 font-mono">{navigator.platform}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Browser Info */}
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Globe} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Browser</Text>
|
||||
</Box>
|
||||
<Stack gap={1}>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">User Agent</Text>
|
||||
<Text size="xs" color="text-gray-300" truncate maxWidth="150px">{navigator.userAgent}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Language</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{navigator.language}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Platform</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{navigator.platform}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Performance */}
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Activity className="w-3 h-3" /> Performance
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Viewport</span>
|
||||
<span className="text-gray-300 font-mono">{window.innerWidth}x{window.innerHeight}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Screen</span>
|
||||
<span className="text-gray-300 font-mono">{window.screen.width}x{window.screen.height}</span>
|
||||
</div>
|
||||
{(performance as any).memory && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">JS Heap</span>
|
||||
<span className="text-gray-300 font-mono">
|
||||
{((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Performance */}
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Activity} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Performance</Text>
|
||||
</Box>
|
||||
<Stack gap={1}>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Viewport</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{window.innerWidth}x{window.innerHeight}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Screen</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{window.screen.width}x{window.screen.height}</Text>
|
||||
</Box>
|
||||
{perf?.memory && (
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">JS Heap</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">
|
||||
{formatMemory(perf.memory.usedJSHeapSize)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Connection */}
|
||||
{(navigator as any).connection && (
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Zap className="w-3 h-3" /> Network
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Type</span>
|
||||
<span className="text-gray-300 font-mono">{(navigator as any).connection.effectiveType}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Downlink</span>
|
||||
<span className="text-gray-300 font-mono">{(navigator as any).connection.downlink}Mbps</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">RTT</span>
|
||||
<span className="text-gray-300 font-mono">{(navigator as any).connection.rtt}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Connection */}
|
||||
{nav?.connection && (
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Zap} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Network</Text>
|
||||
</Box>
|
||||
<Stack gap={1}>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Type</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{nav.connection.effectiveType}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">Downlink</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{nav.connection.downlink}Mbps</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">RTT</Text>
|
||||
<Text size="xs" color="text-gray-300" font="mono">{nav.connection.rtt}ms</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Raw Data Tab */}
|
||||
{selectedTab === 'raw' && stats && (
|
||||
<div className="space-y-3">
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<FileText className="w-3 h-3" /> Export Options
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={exportAllData}
|
||||
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs"
|
||||
{/* Raw Data Tab */}
|
||||
{selectedTab === 'raw' && stats && (
|
||||
<Stack gap={3}>
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={FileText} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Export Options</Text>
|
||||
</Box>
|
||||
<Box display="flex" gap={2}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={exportAllData}
|
||||
fullWidth
|
||||
size="sm"
|
||||
icon={<Icon icon={Download} size={3} />}
|
||||
>
|
||||
Export JSON
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => copyToClipboard(stats)}
|
||||
fullWidth
|
||||
size="sm"
|
||||
icon={<Icon icon={Copy} size={3} />}
|
||||
>
|
||||
Copy Stats
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Trash2} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Maintenance</Text>
|
||||
</Box>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={clearAllData}
|
||||
fullWidth
|
||||
size="sm"
|
||||
icon={<Icon icon={Trash2} size={3} />}
|
||||
>
|
||||
<Download className="w-3 h-3" /> Export JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={() => copyToClipboard(stats)}
|
||||
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs border border-charcoal-outline"
|
||||
>
|
||||
<Copy className="w-3 h-3" /> Copy Stats
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
Clear All Logs
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Trash2 className="w-3 h-3" /> Maintenance
|
||||
</div>
|
||||
<button
|
||||
onClick={clearAllData}
|
||||
className="w-full flex items-center justify-center gap-1 px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded text-xs"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" /> Clear All Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
|
||||
<Terminal className="w-3 h-3" /> Console Commands
|
||||
</div>
|
||||
<div className="space-y-1 text-[10px] font-mono text-gray-400">
|
||||
<div>• window.__GRIDPILOT_GLOBAL_HANDLER__</div>
|
||||
<div>• window.__GRIDPILOT_API_LOGGER__</div>
|
||||
<div>• window.__GRIDPILOT_REACT_ERRORS__</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={Terminal} size={3} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Console Commands</Text>
|
||||
</Box>
|
||||
<Stack gap={1} fontSize="10px">
|
||||
<Text color="text-gray-400" font="mono">• window.__GRIDPILOT_GLOBAL_HANDLER__</Text>
|
||||
<Text color="text-gray-400" font="mono">• window.__GRIDPILOT_API_LOGGER__</Text>
|
||||
<Text color="text-gray-400" font="mono">• window.__GRIDPILOT_REACT_ERRORS__</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 py-2 bg-iron-gray/30 border-t border-charcoal-outline text-[10px] text-gray-500 flex justify-between items-center">
|
||||
<span>Auto-refresh: {refreshInterval}ms</span>
|
||||
{copied && <span className="text-green-400">Copied!</span>}
|
||||
</div>
|
||||
</div>
|
||||
<Box px={4} py={2} bg="bg-iron-gray/30" borderTop borderColor="border-charcoal-outline" display="flex" justifyContent="between" alignItems="center">
|
||||
<Text size="xs" color="text-gray-500" fontSize="10px">Auto-refresh: {refreshInterval}ms</Text>
|
||||
{copied && <Text size="xs" color="text-performance-green" fontSize="10px">Copied!</Text>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -608,4 +631,4 @@ export function useErrorAnalytics() {
|
||||
apiLogger.clearHistory();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { AlertTriangle, Wifi, RefreshCw, ArrowLeft } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { ErrorDisplay as UiErrorDisplay } from '@/ui/ErrorDisplay';
|
||||
|
||||
interface ErrorDisplayProps {
|
||||
error: ApiError;
|
||||
@@ -14,123 +13,12 @@ interface ErrorDisplayProps {
|
||||
* 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>
|
||||
<UiErrorDisplay
|
||||
error={error}
|
||||
onRetry={onRetry}
|
||||
variant="full-screen"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,8 +27,10 @@ export function ErrorDisplay({ error, onRetry }: ErrorDisplayProps) {
|
||||
*/
|
||||
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>
|
||||
<UiErrorDisplay
|
||||
error={error}
|
||||
onRetry={onRetry}
|
||||
variant="full-screen"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function NotificationIntegration() {
|
||||
useEffect(() => {
|
||||
// Listen for custom notification events from error reporter
|
||||
const handleNotificationEvent = (event: CustomEvent) => {
|
||||
const { type, title, message, variant, autoDismiss } = event.detail;
|
||||
const { type, title, message, variant } = event.detail;
|
||||
|
||||
addNotification({
|
||||
type: type || 'error',
|
||||
|
||||
Reference in New Issue
Block a user