Files
gridpilot.gg/apps/website/components/dev/DevToolbar.tsx
2026-01-20 00:41:57 +01:00

372 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 '@/components/shared/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 { Toolbar } from '@/ui/Toolbar';
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 (
<Toolbar minimized bottom="6rem">
<IconButton
icon={Wrench}
onClick={() => setIsMinimized(false)}
variant="secondary"
title="Open Dev Toolbar"
size="lg"
/>
</Toolbar>
);
}
return (
<Toolbar bottom="6rem">
{/* 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"
title={isExpanded ? "Collapse" : "Expand"}
/>
<IconButton
icon={X}
onClick={() => setIsMinimized(true)}
variant="ghost"
size="sm"
title="Minimize"
/>
</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>
)}
</Toolbar>
);
}