324 lines
13 KiB
TypeScript
324 lines
13 KiB
TypeScript
/**
|
|
* Admin Dashboard Page
|
|
* Main dashboard for admin users showing ML models, agents, and system health
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
CpuChipIcon,
|
|
ChartBarIcon,
|
|
UserGroupIcon,
|
|
ServerIcon,
|
|
ArrowTrendingUpIcon,
|
|
ArrowTrendingDownIcon,
|
|
CheckCircleIcon,
|
|
ExclamationTriangleIcon,
|
|
XCircleIcon,
|
|
} from '@heroicons/react/24/solid';
|
|
import {
|
|
getAdminDashboard,
|
|
getSystemHealth,
|
|
getMLModels,
|
|
getAgents,
|
|
type AdminStats,
|
|
type SystemHealth,
|
|
type MLModel,
|
|
type AgentPerformance,
|
|
} from '../../../services/adminService';
|
|
|
|
export default function AdminDashboard() {
|
|
const [stats, setStats] = useState<AdminStats | null>(null);
|
|
const [health, setHealth] = useState<SystemHealth | null>(null);
|
|
const [models, setModels] = useState<MLModel[]>([]);
|
|
const [agents, setAgents] = useState<AgentPerformance[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadDashboardData();
|
|
}, []);
|
|
|
|
const loadDashboardData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [dashboardData, healthData, modelsData, agentsData] = await Promise.all([
|
|
getAdminDashboard(),
|
|
getSystemHealth(),
|
|
getMLModels(),
|
|
getAgents(),
|
|
]);
|
|
|
|
setStats(dashboardData);
|
|
setHealth(healthData);
|
|
setModels(Array.isArray(modelsData) ? modelsData : []);
|
|
setAgents(Array.isArray(agentsData) ? agentsData : []);
|
|
} catch (error) {
|
|
console.error('Error loading dashboard data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getHealthIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
return <CheckCircleIcon className="w-5 h-5 text-green-400" />;
|
|
case 'degraded':
|
|
return <ExclamationTriangleIcon className="w-5 h-5 text-yellow-400" />;
|
|
default:
|
|
return <XCircleIcon className="w-5 h-5 text-red-400" />;
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 2,
|
|
}).format(value);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-500"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white">Admin Dashboard</h1>
|
|
<p className="text-gray-400">OrbiQuant IA Platform Overview</p>
|
|
</div>
|
|
<button
|
|
onClick={loadDashboardData}
|
|
className="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg transition-colors"
|
|
>
|
|
Refresh Data
|
|
</button>
|
|
</div>
|
|
|
|
{/* System Health Banner */}
|
|
{health && (
|
|
<div className={`p-4 rounded-lg ${
|
|
health.status === 'healthy' ? 'bg-green-900/30 border border-green-700' :
|
|
health.status === 'degraded' ? 'bg-yellow-900/30 border border-yellow-700' :
|
|
'bg-red-900/30 border border-red-700'
|
|
}`}>
|
|
<div className="flex items-center gap-3">
|
|
{getHealthIcon(health.status)}
|
|
<span className="text-white font-semibold">
|
|
System Status: {health.status.charAt(0).toUpperCase() + health.status.slice(1)}
|
|
</span>
|
|
<span className="text-gray-400 text-sm ml-auto">
|
|
Last updated: {new Date(health.timestamp).toLocaleString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* ML Models Card */}
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 bg-blue-600 rounded-lg">
|
|
<CpuChipIcon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<span className="text-gray-400">ML Models</span>
|
|
</div>
|
|
<p className="text-3xl font-bold text-white">{models.length}</p>
|
|
<p className="text-sm text-green-400">
|
|
{models.filter(m => m.status === 'active' || m.status === 'training').length} active
|
|
</p>
|
|
</div>
|
|
|
|
{/* Trading Agents Card */}
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 bg-purple-600 rounded-lg">
|
|
<UserGroupIcon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<span className="text-gray-400">Trading Agents</span>
|
|
</div>
|
|
<p className="text-3xl font-bold text-white">{agents.length}</p>
|
|
<p className="text-sm text-green-400">
|
|
{agents.filter(a => a.status === 'active').length} running
|
|
</p>
|
|
</div>
|
|
|
|
{/* Total P&L Card */}
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 bg-green-600 rounded-lg">
|
|
<ChartBarIcon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<span className="text-gray-400">Today's P&L</span>
|
|
</div>
|
|
<p className={`text-3xl font-bold ${(stats?.total_pnl_today || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
|
{formatCurrency(stats?.total_pnl_today || 0)}
|
|
</p>
|
|
<div className="flex items-center gap-1 text-sm">
|
|
{(stats?.total_pnl_today || 0) >= 0 ? (
|
|
<ArrowTrendingUpIcon className="w-4 h-4 text-green-400" />
|
|
) : (
|
|
<ArrowTrendingDownIcon className="w-4 h-4 text-red-400" />
|
|
)}
|
|
<span className="text-gray-400">vs yesterday</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Predictions Card */}
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 bg-orange-600 rounded-lg">
|
|
<ServerIcon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<span className="text-gray-400">Predictions Today</span>
|
|
</div>
|
|
<p className="text-3xl font-bold text-white">{stats?.total_predictions_today || 0}</p>
|
|
<p className="text-sm text-blue-400">
|
|
{((stats?.overall_accuracy || 0) * 100).toFixed(1)}% accuracy
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Services Health */}
|
|
{health && (
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<h2 className="text-lg font-bold text-white mb-4">Services Health</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="flex items-center justify-between p-4 bg-gray-900 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<ServerIcon className="w-5 h-5 text-gray-400" />
|
|
<span className="text-white">Database</span>
|
|
</div>
|
|
{getHealthIcon(health.services.database.status)}
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-gray-900 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<CpuChipIcon className="w-5 h-5 text-gray-400" />
|
|
<span className="text-white">ML Engine</span>
|
|
</div>
|
|
{getHealthIcon(health.services.mlEngine.status)}
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-gray-900 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<UserGroupIcon className="w-5 h-5 text-gray-400" />
|
|
<span className="text-white">Trading Agents</span>
|
|
</div>
|
|
{getHealthIcon(health.services.tradingAgents.status)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ML Models Overview */}
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-bold text-white">ML Models</h2>
|
|
<a href="/admin/models" className="text-primary-400 hover:text-primary-300 text-sm">
|
|
View All
|
|
</a>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{models.slice(0, 6).map((model, index) => (
|
|
<div key={index} className="p-4 bg-gray-900 rounded-lg">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-white font-semibold">{model.name || model.type}</span>
|
|
<span className={`px-2 py-1 rounded-full text-xs font-semibold ${
|
|
model.status === 'active' ? 'bg-green-900/50 text-green-400' :
|
|
model.status === 'training' ? 'bg-yellow-900/50 text-yellow-400' :
|
|
'bg-gray-700 text-gray-400'
|
|
}`}>
|
|
{model.status}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm text-gray-400">
|
|
<span>Accuracy: {((model.accuracy || 0) * 100).toFixed(1)}%</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trading Agents Overview */}
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-bold text-white">Trading Agents</h2>
|
|
<a href="/admin/agents" className="text-primary-400 hover:text-primary-300 text-sm">
|
|
View All
|
|
</a>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{agents.map((agent, index) => (
|
|
<div key={index} className="p-4 bg-gray-900 rounded-lg">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-white font-semibold">{agent.name}</span>
|
|
<span className={`px-2 py-1 rounded-full text-xs font-semibold ${
|
|
agent.status === 'active' ? 'bg-green-900/50 text-green-400' :
|
|
agent.status === 'paused' ? 'bg-yellow-900/50 text-yellow-400' :
|
|
'bg-red-900/50 text-red-400'
|
|
}`}>
|
|
{agent.status}
|
|
</span>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Win Rate</span>
|
|
<span className="text-white">{((agent.win_rate || 0) * 100).toFixed(1)}%</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Total P&L</span>
|
|
<span className={agent.total_pnl >= 0 ? 'text-green-400' : 'text-red-400'}>
|
|
{formatCurrency(agent.total_pnl || 0)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Trades</span>
|
|
<span className="text-white">{agent.total_trades || 0}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{agents.length === 0 && (
|
|
<div className="col-span-3 text-center text-gray-500 py-8">
|
|
No trading agents configured yet
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* System Info */}
|
|
{health && (
|
|
<div className="bg-gray-800 rounded-xl p-5 border border-gray-700">
|
|
<h2 className="text-lg font-bold text-white mb-4">System Info</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="p-4 bg-gray-900 rounded-lg">
|
|
<span className="text-gray-400 text-sm">Uptime</span>
|
|
<p className="text-white font-semibold">
|
|
{Math.floor(health.system.uptime / 3600)}h {Math.floor((health.system.uptime % 3600) / 60)}m
|
|
</p>
|
|
</div>
|
|
<div className="p-4 bg-gray-900 rounded-lg">
|
|
<span className="text-gray-400 text-sm">Memory Usage</span>
|
|
<p className="text-white font-semibold">
|
|
{health.system.memory.percentage.toFixed(1)}%
|
|
</p>
|
|
</div>
|
|
<div className="p-4 bg-gray-900 rounded-lg">
|
|
<span className="text-gray-400 text-sm">Last Update</span>
|
|
<p className="text-white font-semibold">
|
|
{new Date(health.timestamp).toLocaleTimeString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|