import { useState, useMemo } from 'react' import { format } from 'date-fns' import { fr } from 'date-fns/locale' import { AlertTriangle, XCircle, Info, RefreshCw, Trash2, ChevronDown, ChevronRight, Brain, Play, Loader2 } from 'lucide-react' import clsx from 'clsx' import { useSystemLogs, useLogSources, useLogCycles, useClearLogs, useCycleContextSnapshots, useCycleContextSnapshot, useReplayCycle, type LogFilters } from '../hooks/useApi' import { useQueryClient } from '@tanstack/react-query' const LEVELS = ['', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] function LevelBadge({ level }: { level: string }) { const cfg: Record = { INFO: { cls: 'bg-slate-700 text-slate-300', icon: }, WARNING: { cls: 'bg-amber-900/60 text-amber-300 border border-amber-700/40', icon: }, ERROR: { cls: 'bg-red-900/60 text-red-300 border border-red-700/40', icon: }, CRITICAL: { cls: 'bg-red-800 text-white border border-red-600', icon: }, } const { cls, icon } = cfg[level] ?? cfg['INFO'] return ( {icon} {level} ) } function LogRow({ log }: { log: any }) { const [expanded, setExpanded] = useState(false) const hasDetails = !!log.details const ts = (() => { try { return format(new Date(log.ts), 'dd/MM HH:mm:ss', { locale: fr }) } catch { return log.ts } })() const rowCls = clsx('border-b border-slate-800 hover:bg-slate-800/40 transition-colors', { 'bg-amber-950/10': log.level === 'WARNING', 'bg-red-950/20': log.level === 'ERROR' || log.level === 'CRITICAL', }) return ( <> {ts} {log.source} {log.cycle_id ? log.cycle_id.slice(0, 16) + '…' : '—'} {log.ticker ?? '—'}
{hasDetails && ( )} {log.message}
{expanded && hasDetails && (
              {JSON.stringify(JSON.parse(log.details), null, 2)}
            
)} ) } function ContextTab() { const [selectedRunId, setSelectedRunId] = useState(null) const [replayNotes, setReplayNotes] = useState('') const [replayResult, setReplayResult] = useState(null) const { data: snapshotsData, isLoading: loadingList } = useCycleContextSnapshots(30) const { data: snapData, isLoading: loadingSnap } = useCycleContextSnapshot(selectedRunId) const { mutate: replayCycle, isPending: replaying } = useReplayCycle() const snapshots: any[] = snapshotsData?.snapshots ?? [] const fmtTs = (ts: string) => { try { return format(new Date(ts), 'dd/MM HH:mm:ss', { locale: fr }) } catch { return ts } } const handleReplay = () => { if (!selectedRunId) return setReplayResult(null) replayCycle( { runId: selectedRunId, notes: replayNotes || undefined }, { onSuccess: (data) => setReplayResult(data) } ) } return (
{/* Left: list of snapshots */}
{loadingList ? (
Chargement…
) : snapshots.length === 0 ? (
Aucun snapshot — les contextes seront sauvegardés lors du prochain cycle.
) : (
{snapshots.map(s => ( ))}
)}
{/* Right: context JSON viewer + replay */}
{!selectedRunId ? (
Sélectionne un cycle à gauche pour voir le contexte complet envoyé à l'IA.
) : loadingSnap ? (
Chargement du contexte…
) : !snapData ? (
Erreur lors du chargement.
) : ( <> {/* Header strip with meta + replay */}
{snapData.run_id.slice(0, 24)}… {fmtTs(snapData.ts)} {snapData.context?.cycle_meta && ( <> Δ {snapData.context.cycle_meta.delta_minutes?.toFixed(0)}min {snapData.context.cycle_meta.calibration_label} )}
setReplayNotes(e.target.value)} />
{/* Replay result */} {replayResult && (
Replay terminé — {replayResult.suggestions_count} patterns générés à {fmtTs(replayResult.replayed_at)}
{(replayResult.suggestions || []).map((s: any, i: number) => (
{s.name} {s.macro_fit && — {s.macro_fit?.slice(0, 80)}}
))}
)} {/* Context sections */}
{snapData.context && Object.entries(snapData.context).map(([key, val]) => ( ))}
)}
) } function ContextSection({ label, value }: { label: string; value: any }) { const [open, setOpen] = useState(['cycle_meta', 'news_partitioned', 'fred_releases', 'price_discovery'].includes(label)) const json = JSON.stringify(value, null, 2) const lineCount = json.split('\n').length const labelColor: Record = { cycle_meta: 'text-purple-400', macro_regime: 'text-blue-400', geo_score: 'text-orange-400', news_partitioned: 'text-yellow-400', fred_releases: 'text-green-400', price_discovery: 'text-red-400', tech_indicators_block: 'text-cyan-400', iv_context_preview: 'text-pink-400', calendar: 'text-slate-300', quotes_summary: 'text-slate-300', } return (
{open && (
          {json}
        
)}
) } export default function SystemLogs() { const qc = useQueryClient() const [activeTab, setActiveTab] = useState<'logs' | 'context'>('logs') const [filters, setFilters] = useState({ limit: 300 }) const [tickerInput, setTickerInput] = useState('') const [cycleInput, setCycleInput] = useState('') const { data, isLoading, isRefetching } = useSystemLogs(filters) const { data: sourcesData } = useLogSources() const { data: cyclesData } = useLogCycles() const { mutate: clearLogs, isPending: clearing } = useClearLogs() const logs: any[] = data?.logs ?? [] const sources: string[] = sourcesData?.sources ?? [] const cycles: any[] = cyclesData?.cycles ?? [] // Summary counts const counts = useMemo(() => { const c = { INFO: 0, WARNING: 0, ERROR: 0, CRITICAL: 0 } for (const l of logs) c[l.level as keyof typeof c] = (c[l.level as keyof typeof c] ?? 0) + 1 return c }, [logs]) const applyFilter = (key: keyof LogFilters, val: string) => { setFilters(f => ({ ...f, [key]: val || undefined })) } const handleClear = () => { if (window.confirm('Supprimer les logs de plus de 30 jours ?')) { clearLogs(30, { onSuccess: () => qc.invalidateQueries({ queryKey: ['system-logs'] }) }) } } return (
{/* Header */}

Logs Système

Erreurs, avertissements et contextes complets des cycles IA

{activeTab === 'logs' && ( )}
{/* Tabs */}
{([ { key: 'logs', label: 'Logs système', icon: }, { key: 'context', label: 'Contexte IA', icon: }, ] as const).map(tab => ( ))}
{activeTab === 'context' && } {activeTab === 'logs' && <> {/* Summary chips */}
{[ { level: 'INFO', cls: 'bg-slate-800 text-slate-300' }, { level: 'WARNING', cls: 'bg-amber-900/40 text-amber-300 border border-amber-700/30' }, { level: 'ERROR', cls: 'bg-red-900/40 text-red-300 border border-red-700/30' }, { level: 'CRITICAL', cls: 'bg-red-800/60 text-red-200 border border-red-600/30' }, ].map(({ level, cls }) => ( ))} {logs.length} entrées
{/* Filters */}
setTickerInput(e.target.value.toUpperCase())} onBlur={() => applyFilter('ticker', tickerInput)} onKeyDown={e => e.key === 'Enter' && applyFilter('ticker', tickerInput)} />
applyFilter('date_from', e.target.value)} />
applyFilter('date_to', e.target.value)} />
{/* Log table */}
{isLoading ? (
Chargement…
) : logs.length === 0 ? (
Aucun log trouvé pour ces filtres.
Les WARNING et ERROR des cycles apparaissent ici automatiquement.
) : (
{logs.map(log => )}
Horodatage Niveau Source Cycle Ticker Message
)}
}
) }