Files
gridpilot.gg/apps/website/components/errors/ApiStatusToolbar.tsx
2025-12-31 21:24:42 +01:00

329 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import { ApiConnectionMonitor, ConnectionStatus } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
import {
Activity,
Wifi,
WifiOff,
AlertTriangle,
CheckCircle2,
RefreshCw,
Terminal,
Shield,
Clock,
TrendingUp
} from 'lucide-react';
interface ApiStatusToolbarProps {
position?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left';
autoHide?: boolean;
}
/**
* Development toolbar showing real-time API connection status
* Integrates with existing DevToolbar or works standalone
*/
export function ApiStatusToolbar({ position = 'bottom-right', autoHide = false }: ApiStatusToolbarProps) {
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
const [health, setHealth] = useState(ApiConnectionMonitor.getInstance().getHealth());
const [expanded, setExpanded] = useState(false);
const [show, setShow] = useState(true);
useEffect(() => {
const monitor = ApiConnectionMonitor.getInstance();
const registry = CircuitBreakerRegistry.getInstance();
const updateState = () => {
setStatus(monitor.getStatus());
setHealth(monitor.getHealth());
};
// Initial state
updateState();
// Listen for events
monitor.on('connected', updateState);
monitor.on('disconnected', updateState);
monitor.on('degraded', updateState);
monitor.on('success', updateState);
monitor.on('failure', updateState);
// Auto-hide logic
if (autoHide) {
const hideTimer = setTimeout(() => setShow(false), 5000);
const showOnInteraction = () => setShow(true);
document.addEventListener('mousemove', showOnInteraction);
document.addEventListener('click', showOnInteraction);
return () => {
clearTimeout(hideTimer);
document.removeEventListener('mousemove', showOnInteraction);
document.removeEventListener('click', showOnInteraction);
monitor.off('connected', updateState);
monitor.off('disconnected', updateState);
monitor.off('degraded', updateState);
monitor.off('success', updateState);
monitor.off('failure', updateState);
};
}
return () => {
monitor.off('connected', updateState);
monitor.off('disconnected', updateState);
monitor.off('degraded', updateState);
monitor.off('success', updateState);
monitor.off('failure', updateState);
};
}, [autoHide]);
const handleHealthCheck = async () => {
const monitor = ApiConnectionMonitor.getInstance();
await monitor.performHealthCheck();
};
const handleReset = () => {
ApiConnectionMonitor.getInstance().reset();
CircuitBreakerRegistry.getInstance().resetAll();
};
const getReliabilityColor = (reliability: number) => {
if (reliability >= 95) return 'text-green-400';
if (reliability >= 80) return 'text-yellow-400';
return 'text-red-400';
};
const getStatusIcon = () => {
switch (status) {
case 'connected':
return <CheckCircle2 className="w-4 h-4 text-green-400" />;
case 'degraded':
return <AlertTriangle className="w-4 h-4 text-yellow-400" />;
case 'disconnected':
return <WifiOff className="w-4 h-4 text-red-400" />;
case 'checking':
return <RefreshCw className="w-4 h-4 animate-spin text-blue-400" />;
default:
return <Wifi className="w-4 h-4 text-gray-400" />;
}
};
const getStatusColor = () => {
switch (status) {
case 'connected': return 'bg-green-500/20 border-green-500/40';
case 'degraded': return 'bg-yellow-500/20 border-yellow-500/40';
case 'disconnected': return 'bg-red-500/20 border-red-500/40';
default: return 'bg-gray-500/20 border-gray-500/40';
}
};
const reliability = ((health.successfulRequests / Math.max(health.totalRequests, 1)) * 100).toFixed(1);
if (!show) {
return (
<button
onClick={() => setShow(true)}
className={`fixed p-2 bg-iron-gray border border-charcoal-outline rounded-lg shadow-lg hover:bg-charcoal-outline transition-all ${
position === 'bottom-right' ? 'bottom-4 right-4' :
position === 'top-right' ? 'top-4 right-4' :
position === 'bottom-left' ? 'bottom-4 left-4' :
'top-4 left-4'
}`}
>
<Activity className="w-5 h-5 text-primary-blue" />
</button>
);
}
return (
<div className={`fixed z-50 transition-all ${
position === 'bottom-right' ? 'bottom-4 right-4' :
position === 'top-right' ? 'top-4 right-4' :
position === 'bottom-left' ? 'bottom-4 left-4' :
'top-4 left-4'
}`}>
{/* Compact Status Indicator */}
{!expanded ? (
<button
onClick={() => setExpanded(true)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg border shadow-lg backdrop-blur-md transition-all hover:scale-105 ${getStatusColor()}`}
>
{getStatusIcon()}
<span className="text-sm font-semibold text-white">{status.toUpperCase()}</span>
<span className={`text-xs ${getReliabilityColor(parseFloat(reliability))}`}>
{reliability}%
</span>
</button>
) : (
/* Expanded Panel */
<div className={`w-80 rounded-lg border shadow-2xl backdrop-blur-md overflow-hidden ${getStatusColor()}`}>
{/* Header */}
<div className="bg-iron-gray/80 border-b border-charcoal-outline px-3 py-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-primary-blue" />
<span className="text-xs font-bold text-white">API STATUS</span>
</div>
<div className="flex gap-1">
<button
onClick={handleHealthCheck}
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
title="Run Health Check"
>
<RefreshCw className="w-3 h-3 text-gray-400 hover:text-white" />
</button>
<button
onClick={handleReset}
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
title="Reset Stats"
>
<span className="text-xs text-gray-400 hover:text-white">R</span>
</button>
<button
onClick={() => setExpanded(false)}
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
>
<span className="text-xs text-gray-400 hover:text-white">×</span>
</button>
</div>
</div>
{/* Body */}
<div className="px-3 py-2 space-y-2 bg-deep-graphite/90">
{/* Status Row */}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Status</span>
<span className={`text-xs font-bold uppercase ${status === 'connected' ? 'text-green-400' : status === 'degraded' ? 'text-yellow-400' : 'text-red-400'}`}>
{status}
</span>
</div>
{/* Reliability */}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Reliability</span>
<span className={`text-xs font-bold ${getReliabilityColor(parseFloat(reliability))}`}>
{reliability}%
</span>
</div>
{/* Request Stats */}
<div className="grid grid-cols-3 gap-2 text-center">
<div className="bg-iron-gray/50 rounded p-1">
<div className="text-[10px] text-gray-400">Total</div>
<div className="text-sm font-bold text-white">{health.totalRequests}</div>
</div>
<div className="bg-iron-gray/50 rounded p-1">
<div className="text-[10px] text-gray-400">Success</div>
<div className="text-sm font-bold text-green-400">{health.successfulRequests}</div>
</div>
<div className="bg-iron-gray/50 rounded p-1">
<div className="text-[10px] text-gray-400">Failed</div>
<div className="text-sm font-bold text-red-400">{health.failedRequests}</div>
</div>
</div>
{/* Performance */}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Avg Response</span>
<span className="text-xs font-mono text-blue-400">
{health.averageResponseTime.toFixed(0)}ms
</span>
</div>
{/* Consecutive Failures */}
{health.consecutiveFailures > 0 && (
<div className="flex items-center justify-between bg-red-500/10 rounded px-2 py-1">
<span className="text-xs text-red-400">Consecutive Failures</span>
<span className="text-xs font-bold text-red-400">{health.consecutiveFailures}</span>
</div>
)}
{/* Circuit Breakers */}
<div className="border-t border-charcoal-outline pt-2">
<div className="flex items-center gap-1 mb-1">
<Shield className="w-3 h-3 text-gray-400" />
<span className="text-[10px] text-gray-400 font-bold">CIRCUIT BREAKERS</span>
</div>
<CircuitBreakerStatus />
</div>
{/* Last Check */}
<div className="border-t border-charcoal-outline pt-2 flex items-center justify-between">
<span className="text-[10px] text-gray-500">Last Check</span>
<span className="text-[10px] text-gray-400 font-mono">
{health.lastCheck ? new Date(health.lastCheck).toLocaleTimeString() : 'Never'}
</span>
</div>
{/* Actions */}
<div className="grid grid-cols-2 gap-2 pt-1">
<button
onClick={handleHealthCheck}
className="px-2 py-1 bg-primary-blue hover:bg-primary-blue/80 text-white text-xs rounded transition-colors"
>
Check Health
</button>
<button
onClick={() => {
const monitor = ApiConnectionMonitor.getInstance();
const report = monitor.getDebugReport();
alert(report);
}}
className="px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 text-xs rounded transition-colors border border-charcoal-outline"
>
Debug Report
</button>
</div>
</div>
</div>
)}
</div>
);
}
/**
* Circuit Breaker Status Component
*/
function CircuitBreakerStatus() {
const [status, setStatus] = useState(CircuitBreakerRegistry.getInstance().getStatus());
useEffect(() => {
const registry = CircuitBreakerRegistry.getInstance();
// Poll for updates every 2 seconds
const interval = setInterval(() => {
setStatus(registry.getStatus());
}, 2000);
return () => clearInterval(interval);
}, []);
const entries = Object.entries(status);
if (entries.length === 0) {
return (
<div className="text-[10px] text-gray-500 italic">No active circuit breakers</div>
);
}
return (
<div className="space-y-1 max-h-20 overflow-auto">
{entries.map(([endpoint, breaker]) => (
<div key={endpoint} className="flex items-center justify-between text-[10px]">
<span className="text-gray-400 truncate flex-1">{endpoint.split('/').pop() || endpoint}</span>
<span className={`px-1 rounded ${
breaker.state === 'CLOSED' ? 'bg-green-500/20 text-green-400' :
breaker.state === 'OPEN' ? 'bg-red-500/20 text-red-400' :
'bg-yellow-500/20 text-yellow-400'
}`}>
{breaker.state}
</span>
{breaker.failures > 0 && (
<span className="text-red-400 ml-1">({breaker.failures})</span>
)}
</div>
))}
</div>
);
}