119 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|