369 lines
13 KiB
TypeScript
369 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
|
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
|
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
|
|
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
|
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
|
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
|
import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench, X } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
// Import our new components
|
|
import { Accordion } from '@/ui/Accordion';
|
|
import { Badge } from '@/ui/Badge';
|
|
import { Button } from '@/ui/Button';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { IconButton } from '@/ui/IconButton';
|
|
import { Text } from '@/ui/Text';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { APIStatusSection } from './sections/APIStatusSection';
|
|
import { NotificationSendSection } from './sections/NotificationSendSection';
|
|
import { NotificationTypeSection } from './sections/NotificationTypeSection';
|
|
import { UrgencySection } from './sections/UrgencySection';
|
|
|
|
// Import types
|
|
import type { DemoNotificationType, DemoUrgency } from './types';
|
|
|
|
export function DevToolbar() {
|
|
const { addNotification } = useNotifications();
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const [isMinimized, setIsMinimized] = useState(false);
|
|
const [selectedType, setSelectedType] = useState<DemoNotificationType>('protest_filed');
|
|
const [selectedUrgency, setSelectedUrgency] = useState<DemoUrgency>('toast');
|
|
const [sending, setSending] = useState(false);
|
|
const [lastSent, setLastSent] = useState<string | null>(null);
|
|
|
|
// API Status Monitoring State
|
|
const [apiStatus, setApiStatus] = useState(() => ApiConnectionMonitor.getInstance().getStatus());
|
|
const [apiHealth, setApiHealth] = useState(() => ApiConnectionMonitor.getInstance().getHealth());
|
|
const [circuitBreakers, setCircuitBreakers] = useState(() => CircuitBreakerRegistry.getInstance().getStatus());
|
|
const [checkingHealth, setCheckingHealth] = useState(false);
|
|
|
|
// Error Stats State
|
|
const [errorStats, setErrorStats] = useState({ total: 0, byType: {} as Record<string, number> });
|
|
|
|
// Accordion state - only one open at a time
|
|
const [openAccordion, setOpenAccordion] = useState<string | null>('notifications');
|
|
|
|
const currentDriverId = useEffectiveDriverId();
|
|
|
|
// API Status Monitoring Effects
|
|
useEffect(() => {
|
|
const monitor = ApiConnectionMonitor.getInstance();
|
|
const registry = CircuitBreakerRegistry.getInstance();
|
|
|
|
const updateStatus = () => {
|
|
setApiStatus(monitor.getStatus());
|
|
setApiHealth(monitor.getHealth());
|
|
setCircuitBreakers(registry.getStatus());
|
|
};
|
|
|
|
// Initial update
|
|
updateStatus();
|
|
|
|
// Listen for status changes
|
|
monitor.on('connected', updateStatus);
|
|
monitor.on('disconnected', updateStatus);
|
|
monitor.on('degraded', updateStatus);
|
|
monitor.on('success', updateStatus);
|
|
monitor.on('failure', updateStatus);
|
|
|
|
// Poll for updates every 2 seconds
|
|
const interval = setInterval(updateStatus, 2000);
|
|
|
|
return () => {
|
|
monitor.off('connected', updateStatus);
|
|
monitor.off('disconnected', updateStatus);
|
|
monitor.off('degraded', updateStatus);
|
|
monitor.off('success', updateStatus);
|
|
monitor.off('failure', updateStatus);
|
|
clearInterval(interval);
|
|
};
|
|
}, []);
|
|
|
|
// Error Stats Effect
|
|
useEffect(() => {
|
|
const updateErrorStats = () => {
|
|
try {
|
|
const handler = getGlobalErrorHandler();
|
|
const stats = handler.getStats();
|
|
setErrorStats(stats);
|
|
} catch {
|
|
// Handler might not be initialized yet
|
|
setErrorStats({ total: 0, byType: {} });
|
|
}
|
|
};
|
|
|
|
// Initial update
|
|
updateErrorStats();
|
|
|
|
// Poll for updates every 3 seconds
|
|
const interval = setInterval(updateErrorStats, 3000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
// API Health Check Handler
|
|
const handleApiHealthCheck = async () => {
|
|
setCheckingHealth(true);
|
|
try {
|
|
const monitor = ApiConnectionMonitor.getInstance();
|
|
const result = await monitor.performHealthCheck();
|
|
|
|
addNotification({
|
|
type: result.healthy ? 'api_healthy' : 'api_unhealthy',
|
|
title: result.healthy ? 'API Health Check Passed' : 'API Health Check Failed',
|
|
message: result.healthy
|
|
? `API responded in ${result.responseTime}ms`
|
|
: `Health check failed: ${result.error}`,
|
|
variant: 'toast',
|
|
});
|
|
} catch (error) {
|
|
addNotification({
|
|
type: 'api_error',
|
|
title: 'Health Check Error',
|
|
message: 'Failed to perform health check',
|
|
variant: 'toast',
|
|
});
|
|
} finally {
|
|
setCheckingHealth(false);
|
|
}
|
|
};
|
|
|
|
// Reset API Stats
|
|
const handleResetApiStats = () => {
|
|
ApiConnectionMonitor.getInstance().reset();
|
|
CircuitBreakerRegistry.getInstance().resetAll();
|
|
|
|
addNotification({
|
|
type: 'api_reset',
|
|
title: 'API Stats Reset',
|
|
message: 'All API connection statistics have been reset',
|
|
variant: 'toast',
|
|
});
|
|
};
|
|
|
|
// Test API Error
|
|
const handleTestApiError = async () => {
|
|
try {
|
|
// This will intentionally fail to test error handling
|
|
const response = await fetch('http://localhost:3001/api/nonexistent', {
|
|
method: 'GET',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Test error: ${response.status}`);
|
|
}
|
|
} catch (error) {
|
|
addNotification({
|
|
type: 'api_test_error',
|
|
title: 'Test Error Triggered',
|
|
message: 'This is a test API error to demonstrate error handling',
|
|
variant: 'toast',
|
|
});
|
|
}
|
|
};
|
|
|
|
// Only show in development
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return null;
|
|
}
|
|
|
|
const handleSendNotification = async () => {
|
|
setSending(true);
|
|
try {
|
|
const actionUrlByType: Record<DemoNotificationType, string> = {
|
|
protest_filed: '/races',
|
|
defense_requested: '/races',
|
|
vote_required: '/leagues',
|
|
race_performance_summary: '/races',
|
|
race_final_results: '/races',
|
|
};
|
|
|
|
const titleByType: Record<DemoNotificationType, string> = {
|
|
protest_filed: 'Protest Filed Against You',
|
|
defense_requested: 'Defense Requested',
|
|
vote_required: 'Vote Required',
|
|
race_performance_summary: 'Race Performance Summary',
|
|
race_final_results: 'Race Final Results',
|
|
};
|
|
|
|
const messageByType: Record<DemoNotificationType, string> = {
|
|
protest_filed: 'A protest has been filed against you. Please review the incident details.',
|
|
defense_requested: 'A steward requests your defense. Please respond within the deadline.',
|
|
vote_required: 'A protest vote is pending. Please review and vote.',
|
|
race_performance_summary: 'Your race is complete. View your provisional results.',
|
|
race_final_results: 'Stewarding is closed. Your final results are available.',
|
|
};
|
|
|
|
const notificationTypeByDemoType: Record<DemoNotificationType, string> = {
|
|
protest_filed: 'protest_filed',
|
|
defense_requested: 'protest_defense_requested',
|
|
vote_required: 'protest_vote_required',
|
|
race_performance_summary: 'race_performance_summary',
|
|
race_final_results: 'race_final_results',
|
|
};
|
|
|
|
const variant: NotificationVariant = selectedUrgency === 'modal' ? 'modal' : 'toast';
|
|
|
|
addNotification({
|
|
type: notificationTypeByDemoType[selectedType],
|
|
title: titleByType[selectedType],
|
|
message: messageByType[selectedType],
|
|
variant,
|
|
actionUrl: actionUrlByType[selectedType],
|
|
data: {
|
|
driverId: currentDriverId,
|
|
demo: true,
|
|
},
|
|
});
|
|
|
|
setLastSent(`${selectedType}-${selectedUrgency}`);
|
|
setTimeout(() => setLastSent(null), 3000);
|
|
} catch (error) {
|
|
// Silent failure for demo notifications
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
if (isMinimized) {
|
|
return (
|
|
<Stack align="end" justify="end">
|
|
<IconButton
|
|
icon={Wrench}
|
|
onClick={() => setIsMinimized(false)}
|
|
variant="secondary"
|
|
title="Open Dev Toolbar"
|
|
size="lg"
|
|
/>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Stack gap={4}>
|
|
{/* Header */}
|
|
<Stack direction="row" align="center" justify="between" gap={4}>
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Icon icon={Wrench} size={4} intent="primary" />
|
|
<Text size="sm" weight="semibold" variant="high">Dev Toolbar</Text>
|
|
<Badge variant="primary" size="sm">
|
|
DEMO
|
|
</Badge>
|
|
</Stack>
|
|
<Stack direction="row" align="center" gap={1}>
|
|
<IconButton
|
|
icon={isExpanded ? ChevronDown : ChevronUp}
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
variant="ghost"
|
|
size="sm"
|
|
/>
|
|
<IconButton
|
|
icon={X}
|
|
onClick={() => setIsMinimized(true)}
|
|
variant="ghost"
|
|
size="sm"
|
|
/>
|
|
</Stack>
|
|
</Stack>
|
|
|
|
{/* Content */}
|
|
{isExpanded && (
|
|
<Stack gap={4}>
|
|
<Stack gap={3}>
|
|
{/* Notification Section - Accordion */}
|
|
<Accordion
|
|
title="Notifications"
|
|
isOpen={openAccordion === 'notifications'}
|
|
onToggle={() => setOpenAccordion(openAccordion === 'notifications' ? null : 'notifications')}
|
|
>
|
|
<Stack gap={3}>
|
|
<NotificationTypeSection
|
|
selectedType={selectedType}
|
|
onSelectType={setSelectedType}
|
|
/>
|
|
<UrgencySection
|
|
selectedUrgency={selectedUrgency}
|
|
onSelectUrgency={setSelectedUrgency}
|
|
/>
|
|
<NotificationSendSection
|
|
selectedType={selectedType}
|
|
selectedUrgency={selectedUrgency}
|
|
sending={sending}
|
|
lastSent={lastSent}
|
|
onSend={handleSendNotification}
|
|
/>
|
|
</Stack>
|
|
</Accordion>
|
|
|
|
{/* API Status Section - Accordion */}
|
|
<Accordion
|
|
title="API Status"
|
|
isOpen={openAccordion === 'apiStatus'}
|
|
onToggle={() => setOpenAccordion(openAccordion === 'apiStatus' ? null : 'apiStatus')}
|
|
>
|
|
<APIStatusSection
|
|
apiStatus={apiStatus}
|
|
apiHealth={apiHealth}
|
|
circuitBreakers={circuitBreakers}
|
|
checkingHealth={checkingHealth}
|
|
onHealthCheck={handleApiHealthCheck}
|
|
onResetStats={handleResetApiStats}
|
|
onTestError={handleTestApiError}
|
|
/>
|
|
</Accordion>
|
|
|
|
{/* Error Stats Section - Accordion */}
|
|
<Accordion
|
|
title="Error Stats"
|
|
isOpen={openAccordion === 'errors'}
|
|
onToggle={() => setOpenAccordion(openAccordion === 'errors' ? null : 'errors')}
|
|
>
|
|
<Stack gap={2}>
|
|
<Stack direction="row" justify="between" align="center" gap={2}>
|
|
<Text size="xs" variant="low">Total Errors</Text>
|
|
<Text size="xs" font="mono" weight="bold" variant="critical">{errorStats.total}</Text>
|
|
</Stack>
|
|
{Object.keys(errorStats.byType).length > 0 ? (
|
|
<Stack gap={1}>
|
|
{Object.entries(errorStats.byType).map(([type, count]) => (
|
|
<Stack key={type} direction="row" justify="between" align="center" gap={2}>
|
|
<Text size="xs" variant="low">{type}</Text>
|
|
<Text size="xs" font="mono" variant="warning">{count}</Text>
|
|
</Stack>
|
|
))}
|
|
</Stack>
|
|
) : (
|
|
<Stack align="center">
|
|
<Text size="xs" variant="low">No errors yet</Text>
|
|
</Stack>
|
|
)}
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => {
|
|
const handler = getGlobalErrorHandler();
|
|
handler.clearHistory();
|
|
setErrorStats({ total: 0, byType: {} });
|
|
}}
|
|
fullWidth
|
|
size="sm"
|
|
>
|
|
Clear Error History
|
|
</Button>
|
|
</Stack>
|
|
</Accordion>
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Collapsed state hint */}
|
|
{!isExpanded && (
|
|
<Stack>
|
|
<Text size="xs" variant="low">Click ↑ to expand dev tools</Text>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|