418 lines
14 KiB
TypeScript
418 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
|
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
|
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
|
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn } from 'lucide-react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useEffect, useState } from 'react';
|
|
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
|
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
|
|
|
|
// Import our new components
|
|
import { Accordion } from './Accordion';
|
|
import { NotificationTypeSection } from './sections/NotificationTypeSection';
|
|
import { UrgencySection } from './sections/UrgencySection';
|
|
import { NotificationSendSection } from './sections/NotificationSendSection';
|
|
import { APIStatusSection } from './sections/APIStatusSection';
|
|
import { LoginSection } from './sections/LoginSection';
|
|
|
|
// Import types
|
|
import type { DemoNotificationType, DemoUrgency, LoginMode } from './types';
|
|
|
|
export default function DevToolbar() {
|
|
const router = useRouter();
|
|
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);
|
|
const [loginMode, setLoginMode] = useState<LoginMode>('none');
|
|
const [loggingIn, setLoggingIn] = useState(false);
|
|
|
|
// 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);
|
|
|
|
const currentDriverId = useEffectiveDriverId();
|
|
|
|
// Sync login mode with actual session state on mount
|
|
useEffect(() => {
|
|
if (typeof document !== 'undefined') {
|
|
// Check for actual session cookie first
|
|
const cookies = document.cookie.split(';');
|
|
const sessionCookie = cookies.find(c => c.trim().startsWith('gp_session='));
|
|
|
|
if (sessionCookie) {
|
|
// User has a session cookie, check if it's valid by calling the API
|
|
fetch('/api/auth/session', {
|
|
method: 'GET',
|
|
credentials: 'include'
|
|
})
|
|
.then(res => {
|
|
if (res.ok) {
|
|
return res.json();
|
|
}
|
|
throw new Error('No valid session');
|
|
})
|
|
.then(session => {
|
|
if (session && session.user) {
|
|
// Determine login mode based on user email patterns
|
|
const email = session.user.email?.toLowerCase() || '';
|
|
const displayName = session.user.displayName?.toLowerCase() || '';
|
|
|
|
let mode: LoginMode = 'none';
|
|
if (email.includes('sponsor') || displayName.includes('sponsor')) {
|
|
mode = 'sponsor';
|
|
} else if (email.includes('league-owner') || displayName.includes('owner')) {
|
|
mode = 'league-owner';
|
|
} else if (email.includes('league-steward') || displayName.includes('steward')) {
|
|
mode = 'league-steward';
|
|
} else if (email.includes('league-admin') || displayName.includes('admin')) {
|
|
mode = 'league-admin';
|
|
} else if (email.includes('system-owner') || displayName.includes('system owner')) {
|
|
mode = 'system-owner';
|
|
} else if (email.includes('super-admin') || displayName.includes('super admin')) {
|
|
mode = 'super-admin';
|
|
} else if (email.includes('driver') || displayName.includes('demo')) {
|
|
mode = 'driver';
|
|
}
|
|
|
|
setLoginMode(mode);
|
|
} else {
|
|
setLoginMode('none');
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// Session invalid or expired
|
|
setLoginMode('none');
|
|
});
|
|
} else {
|
|
// No session cookie means not logged in
|
|
setLoginMode('none');
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
// 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);
|
|
};
|
|
}, []);
|
|
|
|
// 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',
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDemoLogin = async (role: LoginMode) => {
|
|
if (role === 'none') return;
|
|
|
|
setLoggingIn(true);
|
|
try {
|
|
// Use the demo login API
|
|
const response = await fetch('/api/auth/demo-login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ role }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Demo login failed');
|
|
}
|
|
|
|
setLoginMode(role);
|
|
|
|
// Navigate based on role
|
|
if (role === 'sponsor') {
|
|
window.location.href = '/sponsor/dashboard';
|
|
} else {
|
|
// For driver and league roles, go to dashboard
|
|
window.location.href = '/dashboard';
|
|
}
|
|
} catch (error) {
|
|
alert('Demo login failed. Please check the API server status.');
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
setLoggingIn(true);
|
|
try {
|
|
// Call logout API
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
|
|
setLoginMode('none');
|
|
// Refresh to update all components
|
|
window.location.href = '/';
|
|
} catch (error) {
|
|
alert('Logout failed. Please check the API server status.');
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
};
|
|
|
|
// 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 (
|
|
<button
|
|
onClick={() => setIsMinimized(false)}
|
|
className="fixed bottom-4 right-4 z-50 p-3 bg-iron-gray border border-charcoal-outline rounded-full shadow-lg hover:bg-charcoal-outline transition-colors"
|
|
title="Open Dev Toolbar"
|
|
>
|
|
<Wrench className="w-5 h-5 text-primary-blue" />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="fixed bottom-4 right-4 z-50 w-80 bg-deep-graphite border border-charcoal-outline rounded-xl shadow-2xl overflow-hidden">
|
|
{/* 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">
|
|
<Wrench className="w-4 h-4 text-primary-blue" />
|
|
<span className="text-sm font-semibold text-white">Dev Toolbar</span>
|
|
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-primary-blue/20 text-primary-blue rounded">
|
|
DEMO
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
|
|
>
|
|
{isExpanded ? (
|
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
|
) : (
|
|
<ChevronUp className="w-4 h-4 text-gray-400" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => setIsMinimized(true)}
|
|
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
|
|
>
|
|
<X className="w-4 h-4 text-gray-400" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
{isExpanded && (
|
|
<div className="p-4 space-y-3">
|
|
{/* Notification Section - Accordion */}
|
|
<Accordion
|
|
title="Notifications"
|
|
icon={<MessageSquare className="w-4 h-4 text-gray-400" />}
|
|
defaultOpen={true}
|
|
>
|
|
<div className="space-y-3">
|
|
<NotificationTypeSection
|
|
selectedType={selectedType}
|
|
onSelectType={setSelectedType}
|
|
/>
|
|
<UrgencySection
|
|
selectedUrgency={selectedUrgency}
|
|
onSelectUrgency={setSelectedUrgency}
|
|
/>
|
|
<NotificationSendSection
|
|
selectedType={selectedType}
|
|
selectedUrgency={selectedUrgency}
|
|
sending={sending}
|
|
lastSent={lastSent}
|
|
onSend={handleSendNotification}
|
|
/>
|
|
</div>
|
|
</Accordion>
|
|
|
|
{/* API Status Section - Accordion */}
|
|
<Accordion
|
|
title="API Status"
|
|
icon={<Activity className="w-4 h-4 text-gray-400" />}
|
|
defaultOpen={false}
|
|
>
|
|
<APIStatusSection
|
|
apiStatus={apiStatus}
|
|
apiHealth={apiHealth}
|
|
circuitBreakers={circuitBreakers}
|
|
checkingHealth={checkingHealth}
|
|
onHealthCheck={handleApiHealthCheck}
|
|
onResetStats={handleResetApiStats}
|
|
onTestError={handleTestApiError}
|
|
/>
|
|
</Accordion>
|
|
|
|
{/* Login Section - Accordion */}
|
|
<Accordion
|
|
title="Demo Login"
|
|
icon={<LogIn className="w-4 h-4 text-gray-400" />}
|
|
defaultOpen={false}
|
|
>
|
|
<LoginSection
|
|
loginMode={loginMode}
|
|
loggingIn={loggingIn}
|
|
onDemoLogin={handleDemoLogin}
|
|
onLogout={handleLogout}
|
|
/>
|
|
</Accordion>
|
|
</div>
|
|
)}
|
|
|
|
{/* Collapsed state hint */}
|
|
{!isExpanded && (
|
|
<div className="px-4 py-2 text-xs text-gray-500">
|
|
Click ↑ to expand dev tools
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |