119 lines
4.7 KiB
TypeScript

import { useEffect, useState, useCallback, useRef } from 'react';
import type { AuditLog } from '../api/client';
import { api } from '../api/client';
import { useWebSocket } from '../api/useWebSocket';
const ACTION_COLORS: Record<string, string> = {
PROJECT_CREATED: 'text-green-400',
WORKFLOW_STARTED: 'text-blue-400',
WORKFLOW_PAUSED: 'text-yellow-400',
WORKFLOW_STOPPED: 'text-red-400',
WORKFLOW_RESET: 'text-purple-400',
STATUS_CHANGED: 'text-fox-500',
TASK_CREATED: 'text-teal-400',
QA_APPROVED: 'text-green-400',
QA_REJECTED: 'text-red-400',
DEPLOYED: 'text-green-400',
ROLLBACK: 'text-red-400',
};
export default function LogsPage() {
const [logs, setLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const [agentFilter, setAgentFilter] = useState('');
const [autoScroll, setAutoScroll] = useState(true);
const listRef = useRef<HTMLDivElement>(null);
const fetchLogs = useCallback(async () => {
const params: Record<string, string> = { limit: '200' };
if (agentFilter) params.agent = agentFilter;
try { setLogs(await api.listLogs(params)); } catch { /* ignore */ }
setLoading(false);
}, [agentFilter]);
useEffect(() => { fetchLogs(); }, [fetchLogs]);
// Live updates
const { connected } = useWebSocket(useCallback((msg) => {
if (msg.type === 'log' || msg.type === 'project_update') fetchLogs();
}, [fetchLogs]));
useEffect(() => {
if (autoScroll && listRef.current) {
listRef.current.scrollTop = 0;
}
}, [logs, autoScroll]);
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-4 animate-fade-in">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold text-white flex items-center gap-2">
📜 Logs en temps réel
<span className={`w-2 h-2 rounded-full ${connected ? 'bg-green-400' : 'bg-red-400'}`}></span>
</h1>
<div className="flex items-center gap-3">
<select
className="input w-auto text-xs"
value={agentFilter}
onChange={(e) => setAgentFilter(e.target.value)}
>
<option value="">Tous les agents</option>
<option value="system">Système</option>
<option value="Foxy-Conductor">Conductor</option>
<option value="Foxy-Architect">Architect</option>
<option value="Foxy-Dev">Dev</option>
<option value="Foxy-UIUX">UIUX</option>
<option value="Foxy-QA">QA</option>
<option value="Foxy-Admin">Admin</option>
</select>
<label className="flex items-center gap-2 text-xs text-gray-400">
<input type="checkbox" checked={autoScroll} onChange={e => setAutoScroll(e.target.checked)} className="accent-fox-500" />
Auto-scroll
</label>
<button className="btn btn-ghost text-xs" onClick={fetchLogs}>🔄 Refresh</button>
</div>
</div>
<div ref={listRef} className="glass-card p-4 max-h-[calc(100vh-200px)] overflow-y-auto font-mono text-xs">
{logs.length === 0 ? (
<p className="text-gray-500 text-center py-8">Aucun log disponible</p>
) : (
<table className="w-full">
<thead>
<tr className="text-gray-500 text-left border-b border-surface-700">
<th className="pb-2 pr-4">Heure</th>
<th className="pb-2 pr-4">Agent</th>
<th className="pb-2 pr-4">Action</th>
<th className="pb-2 pr-4">Cible</th>
<th className="pb-2">Message</th>
</tr>
</thead>
<tbody>
{logs.map((l) => (
<tr key={l.id} className="border-b border-surface-800/50 hover:bg-surface-800/30">
<td className="py-2 pr-4 text-gray-500 whitespace-nowrap">
{new Date(l.timestamp).toLocaleString('fr-FR', {
month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
})}
</td>
<td className="py-2 pr-4 text-blue-400 whitespace-nowrap">{l.agent}</td>
<td className={`py-2 pr-4 whitespace-nowrap font-semibold ${ACTION_COLORS[l.action] || 'text-gray-400'}`}>
{l.action}
</td>
<td className="py-2 pr-4 text-gray-400 whitespace-nowrap">{l.target || '—'}</td>
<td className="py-2 text-gray-300 truncate max-w-xs">{l.message || '—'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}