feat: Phase 2 + context log — FRED releases, cycle context snapshot, onglet Contexte IA
Phase 2 — Données macro FRED :
- fred_fetcher.py (nouveau) : 7 séries FRED (CPI, NFP, UNRATE, FEDFUNDS, GDP, ICSA,
spread 10Y-2Y) avec détection direction bullish/bearish et block prompt formaté
- ai_analyzer.py : param fred_block dans suggest + score, injecté dans les deux prompts
- auto_cycle.py : fetch FRED non-bloquant avant la suggestion
Context log — Snapshot du contexte complet :
- database.py : table cycle_context_snapshots + save/get/list fonctions
- auto_cycle.py : sauvegarde le snapshot (meta, news partitionnées, FRED, tech, IV, quotes)
- cycle.py : GET /api/cycle/contexts + GET /api/cycle/contexts/{run_id}
- useApi.ts : hooks useCycleContextSnapshots + useCycleContextSnapshot
- SystemLogs.tsx : onglet "Contexte IA" avec liste de cycles et visualiseur JSON
par section (cycle_meta, macro, news, FRED, tech) avec accordéon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -867,6 +867,21 @@ export const useClearLogs = () =>
|
||||
mutationFn: (days: number) => api.delete('/logs/clear', { params: { older_than_days: days } }).then(r => r.data),
|
||||
})
|
||||
|
||||
export const useCycleContextSnapshots = (limit = 30) =>
|
||||
useQuery({
|
||||
queryKey: ['cycle-context-snapshots'],
|
||||
queryFn: () => api.get('/cycle/contexts', { params: { limit } }).then(r => r.data),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
export const useCycleContextSnapshot = (runId: string | null) =>
|
||||
useQuery({
|
||||
queryKey: ['cycle-context-snapshot', runId],
|
||||
queryFn: () => api.get(`/cycle/contexts/${runId}`).then(r => r.data),
|
||||
enabled: !!runId,
|
||||
staleTime: 300_000,
|
||||
})
|
||||
|
||||
// ── IV Watchlist Management ───────────────────────────────────────────────────
|
||||
|
||||
export const useWatchlistTickers = () =>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { AlertTriangle, XCircle, Info, RefreshCw, Trash2, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { AlertTriangle, XCircle, Info, RefreshCw, Trash2, ChevronDown, ChevronRight, Brain } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { useSystemLogs, useLogSources, useLogCycles, useClearLogs, type LogFilters } from '../hooks/useApi'
|
||||
import { useSystemLogs, useLogSources, useLogCycles, useClearLogs, useCycleContextSnapshots, useCycleContextSnapshot, type LogFilters } from '../hooks/useApi'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
const LEVELS = ['', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
@@ -76,8 +76,124 @@ function LogRow({ log }: { log: any }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ContextTab() {
|
||||
const [selectedRunId, setSelectedRunId] = useState<string | null>(null)
|
||||
const { data: snapshotsData, isLoading: loadingList } = useCycleContextSnapshots(30)
|
||||
const { data: snapData, isLoading: loadingSnap } = useCycleContextSnapshot(selectedRunId)
|
||||
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 }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[280px_1fr] gap-4 min-h-[500px]">
|
||||
{/* Left: list of snapshots */}
|
||||
<div className="card overflow-y-auto max-h-[700px]">
|
||||
{loadingList ? (
|
||||
<div className="p-4 text-xs text-slate-500">Chargement…</div>
|
||||
) : snapshots.length === 0 ? (
|
||||
<div className="p-4 text-xs text-slate-600">
|
||||
Aucun snapshot — les contextes seront sauvegardés lors du prochain cycle.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-800">
|
||||
{snapshots.map(s => (
|
||||
<button
|
||||
key={s.run_id}
|
||||
onClick={() => setSelectedRunId(s.run_id)}
|
||||
className={clsx(
|
||||
'w-full text-left px-3 py-2.5 hover:bg-slate-800/60 transition-colors',
|
||||
selectedRunId === s.run_id && 'bg-purple-900/30 border-l-2 border-purple-500'
|
||||
)}
|
||||
>
|
||||
<div className="text-xs font-mono text-slate-300">{fmtTs(s.ts)}</div>
|
||||
<div className="text-[10px] text-slate-600 font-mono mt-0.5">{s.run_id.slice(0, 20)}…</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: context JSON viewer */}
|
||||
<div className="card overflow-hidden">
|
||||
{!selectedRunId ? (
|
||||
<div className="p-8 text-center text-slate-600 text-sm">
|
||||
<Brain className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
||||
Sélectionne un cycle à gauche pour voir le contexte complet envoyé à l'IA.
|
||||
</div>
|
||||
) : loadingSnap ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm">Chargement du contexte…</div>
|
||||
) : !snapData ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm">Erreur lors du chargement.</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header strip with meta */}
|
||||
<div className="flex items-center gap-4 px-4 py-2 border-b border-slate-800 bg-slate-900/60">
|
||||
<span className="text-[10px] font-mono text-slate-400">{snapData.run_id}</span>
|
||||
<span className="text-[10px] text-slate-500">{fmtTs(snapData.ts)}</span>
|
||||
{snapData.context?.cycle_meta && (
|
||||
<>
|
||||
<span className="text-[10px] text-purple-400">
|
||||
Δ {snapData.context.cycle_meta.delta_minutes?.toFixed(0)}min
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400">{snapData.context.cycle_meta.calibration_label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sections accordion */}
|
||||
<div className="overflow-y-auto flex-1 p-3 space-y-2">
|
||||
{snapData.context && Object.entries(snapData.context).map(([key, val]) => (
|
||||
<ContextSection key={key} label={key} value={val} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextSection({ label, value }: { label: string; value: any }) {
|
||||
const [open, setOpen] = useState(['cycle_meta', 'news_partitioned', 'fred_releases'].includes(label))
|
||||
const json = JSON.stringify(value, null, 2)
|
||||
const lineCount = json.split('\n').length
|
||||
|
||||
const labelColor: Record<string, string> = {
|
||||
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',
|
||||
tech_indicators_block: 'text-cyan-400',
|
||||
iv_context_preview: 'text-pink-400',
|
||||
calendar: 'text-slate-300',
|
||||
quotes_summary: 'text-slate-300',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-slate-800 rounded overflow-hidden">
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 bg-slate-900/60 hover:bg-slate-800/60 text-left"
|
||||
>
|
||||
{open ? <ChevronDown className="w-3 h-3 text-slate-500 shrink-0" /> : <ChevronRight className="w-3 h-3 text-slate-500 shrink-0" />}
|
||||
<span className={clsx('text-xs font-mono font-semibold', labelColor[label] ?? 'text-slate-300')}>{label}</span>
|
||||
<span className="text-[10px] text-slate-600 ml-auto">{lineCount} lignes</span>
|
||||
</button>
|
||||
{open && (
|
||||
<pre className="text-[10px] font-mono text-slate-400 bg-slate-950 p-3 overflow-x-auto max-h-80 leading-relaxed">
|
||||
{json}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SystemLogs() {
|
||||
const qc = useQueryClient()
|
||||
const [activeTab, setActiveTab] = useState<'logs' | 'context'>('logs')
|
||||
const [filters, setFilters] = useState<LogFilters>({ limit: 300 })
|
||||
const [tickerInput, setTickerInput] = useState('')
|
||||
const [cycleInput, setCycleInput] = useState('')
|
||||
@@ -114,7 +230,7 @@ export default function SystemLogs() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">Logs Système</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Erreurs, avertissements et événements des cycles IA</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Erreurs, avertissements et contextes complets des cycles IA</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -124,17 +240,43 @@ export default function SystemLogs() {
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', (isLoading || isRefetching) && 'animate-spin')} />
|
||||
Actualiser
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
disabled={clearing}
|
||||
className="btn-secondary flex items-center gap-1.5 text-xs px-3 py-1.5 text-red-400 border-red-800/40 hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Purger >30j
|
||||
</button>
|
||||
{activeTab === 'logs' && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
disabled={clearing}
|
||||
className="btn-secondary flex items-center gap-1.5 text-xs px-3 py-1.5 text-red-400 border-red-800/40 hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Purger >30j
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 border-b border-slate-800">
|
||||
{([
|
||||
{ key: 'logs', label: 'Logs système', icon: <Info className="w-3.5 h-3.5" /> },
|
||||
{ key: 'context', label: 'Contexte IA', icon: <Brain className="w-3.5 h-3.5" /> },
|
||||
] as const).map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={clsx(
|
||||
'flex items-center gap-1.5 px-4 py-2 text-xs font-medium border-b-2 -mb-px transition-colors',
|
||||
activeTab === tab.key
|
||||
? 'border-purple-500 text-purple-300'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-300'
|
||||
)}
|
||||
>
|
||||
{tab.icon} {tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === 'context' && <ContextTab />}
|
||||
|
||||
{activeTab === 'logs' && <>
|
||||
{/* Summary chips */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{[
|
||||
@@ -251,6 +393,7 @@ export default function SystemLogs() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user