186 lines
7.6 KiB
TypeScript
186 lines
7.6 KiB
TypeScript
import { useEffect, useState, useCallback } from 'react';
|
|
import type { ProjectSummary, AgentStatus, AuditLog } from '../api/client';
|
|
import { api } from '../api/client';
|
|
import { useWebSocket } from '../api/useWebSocket';
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
COMPLETED: 'badge-completed',
|
|
FAILED: 'badge-failed',
|
|
PAUSED: 'badge-paused',
|
|
};
|
|
|
|
function getStatusBadge(status: string) {
|
|
if (status.endsWith('_RUNNING')) return 'badge-running';
|
|
if (status.startsWith('AWAITING_')) return 'badge-awaiting';
|
|
return STATUS_COLORS[status] || 'badge-awaiting';
|
|
}
|
|
|
|
const STATUS_ICONS: Record<string, string> = {
|
|
COMPLETED: '✅', FAILED: '❌', PAUSED: '⏸️',
|
|
};
|
|
function getStatusIcon(s: string) {
|
|
if (s.endsWith('_RUNNING')) return '⚡';
|
|
if (s.startsWith('AWAITING_')) return '⏳';
|
|
return STATUS_ICONS[s] || '❓';
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const [projects, setProjects] = useState<ProjectSummary[]>([]);
|
|
const [agents, setAgents] = useState<AgentStatus[]>([]);
|
|
const [logs, setLogs] = useState<AuditLog[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const fetchAll = useCallback(async () => {
|
|
try {
|
|
const [p, a, l] = await Promise.all([
|
|
api.listProjects(),
|
|
api.listAgents(),
|
|
api.listLogs({ limit: '10' }),
|
|
]);
|
|
setProjects(p); setAgents(a); setLogs(l);
|
|
} catch { /* ignore */ }
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => { fetchAll(); }, [fetchAll]);
|
|
|
|
const { connected } = useWebSocket(useCallback((msg) => {
|
|
if (msg.type === 'project_update' || msg.type === 'agent_status') fetchAll();
|
|
}, [fetchAll]));
|
|
|
|
const active = projects.filter(p => !['COMPLETED', 'FAILED'].includes(p.status));
|
|
const completed = projects.filter(p => p.status === 'COMPLETED');
|
|
const runningAgents = agents.filter(a => a.current_status === 'running');
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-fox-500 animate-spin-slow text-4xl">🦊</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 animate-fade-in">
|
|
{/* Connection indicator */}
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
<span className={`w-2 h-2 rounded-full ${connected ? 'bg-green-400' : 'bg-red-400'}`}></span>
|
|
{connected ? 'Connecté en temps réel' : 'Déconnecté — reconnexion...'}
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<StatCard icon="📋" label="Projets actifs" value={active.length} accent="fox" />
|
|
<StatCard icon="✅" label="Projets terminés" value={completed.length} accent="green" />
|
|
<StatCard icon="🤖" label="Agents en cours" value={runningAgents.length} accent="blue" />
|
|
<StatCard icon="📊" label="Total projets" value={projects.length} accent="purple" />
|
|
</div>
|
|
|
|
{/* Active Projects */}
|
|
<div className="glass-card p-6">
|
|
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
|
<span>⚡</span> Projets en cours
|
|
</h2>
|
|
{active.length === 0 ? (
|
|
<p className="text-gray-500 text-sm">Aucun projet actif</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{active.map((p) => (
|
|
<div key={p.id} className="flex items-center justify-between p-3 rounded-xl bg-surface-800/50 hover:bg-surface-700/50 transition-all">
|
|
<div>
|
|
<span className="text-white font-semibold text-sm">{p.name}</span>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className={`badge ${getStatusBadge(p.status)}`}>
|
|
{getStatusIcon(p.status)} {p.status.replace(/_/g, ' ')}
|
|
</span>
|
|
<span className="text-xs text-gray-500">{p.workflow_type.replace(/_/g, ' ')}</span>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-sm text-gray-400">{p.tasks_done}/{p.task_count} tâches</div>
|
|
<div className="w-24 h-1.5 bg-surface-700 rounded-full mt-1 overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-fox-600 to-fox-400 rounded-full transition-all duration-500"
|
|
style={{ width: `${p.task_count > 0 ? (p.tasks_done / p.task_count) * 100 : 0}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Agents + Recent Logs */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{/* Agent Grid */}
|
|
<div className="glass-card p-6">
|
|
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
|
<span>🤖</span> Agents
|
|
</h2>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{agents.map((a) => (
|
|
<div key={a.name} className="p-3 rounded-xl bg-surface-800/50 text-center">
|
|
<div className="text-2xl mb-1">
|
|
{a.current_status === 'running' ? '⚡' : a.current_status === 'failed' ? '❌' : '💤'}
|
|
</div>
|
|
<div className="text-xs text-white font-semibold">{a.display_name}</div>
|
|
<div className="text-[10px] text-gray-500 mt-0.5">{a.model}</div>
|
|
<span className={`badge mt-1 text-[10px] ${
|
|
a.current_status === 'running' ? 'badge-running' :
|
|
a.current_status === 'failed' ? 'badge-failed' : 'badge-paused'
|
|
}`}>
|
|
{a.current_status}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Logs */}
|
|
<div className="glass-card p-6">
|
|
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
|
<span>📜</span> Activité récente
|
|
</h2>
|
|
<div className="space-y-2 max-h-72 overflow-y-auto">
|
|
{logs.length === 0 ? (
|
|
<p className="text-gray-500 text-sm">Aucune activité</p>
|
|
) : logs.map((l) => (
|
|
<div key={l.id} className="flex gap-3 p-2 rounded-lg hover:bg-surface-800/50 text-xs">
|
|
<span className="text-fox-500 font-mono whitespace-nowrap">
|
|
{new Date(l.timestamp).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
|
</span>
|
|
<span className="text-blue-400 font-semibold whitespace-nowrap">{l.agent}</span>
|
|
<span className="text-gray-400 truncate">{l.message || l.action}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatCard({ icon, label, value, accent }: { icon: string; label: string; value: number; accent: string }) {
|
|
const bgMap: Record<string, string> = {
|
|
fox: 'from-fox-600/10 to-fox-700/5',
|
|
green: 'from-green-500/10 to-green-600/5',
|
|
blue: 'from-blue-500/10 to-blue-600/5',
|
|
purple: 'from-purple-500/10 to-purple-600/5',
|
|
};
|
|
const textMap: Record<string, string> = {
|
|
fox: 'text-fox-500', green: 'text-green-400', blue: 'text-blue-400', purple: 'text-purple-400',
|
|
};
|
|
return (
|
|
<div className={`glass-card p-5 bg-gradient-to-br ${bgMap[accent]}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-xs text-gray-400 uppercase tracking-wider mb-1">{label}</div>
|
|
<div className={`text-3xl font-extrabold ${textMap[accent]}`}>{value}</div>
|
|
</div>
|
|
<div className="text-3xl opacity-60">{icon}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|