329 lines
12 KiB
TypeScript
329 lines
12 KiB
TypeScript
'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>
|
||
);
|
||
} |