Backend:
- DB: add system_logs table (level/source/cycle_id/ticker/message) and
iv_watchlist table (ticker/added_by/is_active); seed builtin 18 tickers
- DBLogHandler attached at startup — all WARNING+ logs auto-persist to DB
- log_system_event() helper for structured manual events
- New router /api/logs: GET with filters (level, source, cycle_id, ticker,
date range), GET /sources, GET /cycles for dropdowns, DELETE /clear
- iv_watchlist now read from DB instead of hardcoded constant; options_vol
watchlist/refresh/bootstrap endpoints all use get_watchlist_tickers()
- New endpoints: POST/DELETE /options-vol/watchlist-tickers/{ticker} to
add/remove tickers; adding triggers background 1-year bootstrap
- auto_cycle: after log_trade_entries(), auto-detect new underlying proxies
not yet in watchlist, add them and bootstrap their IV history
Frontend:
- New page SystemLogs (/logs): log table with level/source/cycle/ticker/date
filters, color-coded rows, expandable JSON details, auto-refresh 30s
- Options Lab: WatchlistManager section — add ticker input, chip list with
builtin/auto/manual color coding, remove button for non-builtins
- Sidebar: Logs Système nav link (ScrollText icon)
- useApi: useSystemLogs, useLogSources, useLogCycles, useClearLogs,
useWatchlistTickers, useAddWatchlistTicker, useRemoveWatchlistTicker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
4.7 KiB
TypeScript
110 lines
4.7 KiB
TypeScript
import { NavLink } from 'react-router-dom'
|
|
import {
|
|
LayoutDashboard, Globe, BarChart2, FlaskConical,
|
|
History, Calendar, TrendingUp, Zap, DollarSign, Settings, BrainCircuit, Activity, BookOpen, FileBarChart, Brain, ShieldAlert, Microscope, ScrollText
|
|
} from 'lucide-react'
|
|
import { useGeoRiskScore, useAiStatus, usePortfolioSummary } from '../../hooks/useApi'
|
|
import clsx from 'clsx'
|
|
|
|
const nav = [
|
|
{ to: '/', icon: LayoutDashboard, label: 'Cockpit' },
|
|
{ to: '/geo', icon: Globe, label: 'Radar Géopolitique' },
|
|
{ to: '/markets', icon: BarChart2, label: 'Marchés & Prix' },
|
|
{ to: '/macro', icon: Activity, label: 'Régime Macro' },
|
|
{ to: '/options', icon: TrendingUp, label: 'Options Lab' },
|
|
{ to: '/patterns', icon: Zap, label: 'Patterns' },
|
|
{ to: '/portfolio', icon: DollarSign, label: 'Portefeuille' },
|
|
{ to: '/journal', icon: BookOpen, label: 'Journal de Bord' },
|
|
{ to: '/rapport', icon: FileBarChart, label: 'Rapport IA' },
|
|
{ to: '/super-contexte', icon: Brain, label: 'Super Contexte' },
|
|
{ to: '/analytics', icon: FlaskConical, label: 'Analytics' },
|
|
{ to: '/analytics-advanced', icon: Microscope, label: 'Analytics Avancées' },
|
|
{ to: '/risk', icon: ShieldAlert, label: 'Risk Dashboard' },
|
|
{ to: '/backtest', icon: History, label: 'Backtest' },
|
|
{ to: '/calendar', icon: Calendar, label: 'Calendrier' },
|
|
{ to: '/logs', icon: ScrollText, label: 'Logs Système' },
|
|
{ to: '/config', icon: Settings, label: 'Configuration' },
|
|
]
|
|
|
|
const riskColors: Record<string, string> = {
|
|
low: 'text-emerald-400 bg-emerald-900/30 border-emerald-700/40',
|
|
medium: 'text-yellow-400 bg-yellow-900/30 border-yellow-700/40',
|
|
high: 'text-orange-400 bg-orange-900/30 border-orange-700/40',
|
|
extreme: 'text-red-400 bg-red-900/30 border-red-700/40 animate-pulse',
|
|
}
|
|
|
|
export default function Sidebar() {
|
|
const { data: riskScore } = useGeoRiskScore()
|
|
const { data: aiStatus } = useAiStatus()
|
|
const { data: summary } = usePortfolioSummary()
|
|
|
|
return (
|
|
<aside className="w-56 bg-dark-800 border-r border-slate-700/40 flex flex-col h-screen sticky top-0">
|
|
{/* Logo */}
|
|
<div className="p-4 border-b border-slate-700/40">
|
|
<div className="flex items-center gap-2">
|
|
<Zap className="w-5 h-5 text-blue-400" />
|
|
<div>
|
|
<div className="text-sm font-bold text-white tracking-wide">GeoOptions</div>
|
|
<div className="text-xs text-slate-500">Intelligence v2.0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Geo risk indicator */}
|
|
{riskScore && (
|
|
<div className={clsx('mx-3 mt-3 px-3 py-2 rounded border text-xs', riskColors[riskScore.level])}>
|
|
<div className="font-semibold uppercase tracking-wider">Risque Géo</div>
|
|
<div className="text-lg font-bold">{riskScore.score}/100</div>
|
|
<div className="capitalize">{riskScore.level}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Portfolio mini summary */}
|
|
{summary && (summary.open_positions > 0 || summary.unrealized_pnl !== 0) && (
|
|
<div className="mx-3 mt-2 px-3 py-2 rounded border border-slate-700/30 bg-dark-700/50 text-xs">
|
|
<div className="text-slate-500 uppercase tracking-wider text-xs mb-1">Portefeuille</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-slate-400">{summary.open_positions} positions</span>
|
|
<span className={clsx('font-bold', summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
|
{summary.unrealized_pnl >= 0 ? '+' : ''}{summary.unrealized_pnl?.toFixed(0)}€
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 p-2 mt-2 space-y-0.5 overflow-y-auto">
|
|
{nav.map(({ to, icon: Icon, label }) => (
|
|
<NavLink
|
|
key={to}
|
|
to={to}
|
|
end={to === '/'}
|
|
className={({ isActive }) => clsx('nav-link', isActive && 'active')}
|
|
>
|
|
<Icon className="w-4 h-4 shrink-0" />
|
|
<span>{label}</span>
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
|
|
{/* AI status badge */}
|
|
<div className="px-3 py-2 border-t border-slate-700/40">
|
|
<div className={clsx('flex items-center gap-2 text-xs px-2 py-1.5 rounded', {
|
|
'bg-blue-900/30 text-blue-400': aiStatus?.enabled,
|
|
'bg-dark-700 text-slate-600': !aiStatus?.enabled,
|
|
})}>
|
|
<BrainCircuit className="w-3.5 h-3.5" />
|
|
<span>{aiStatus?.enabled ? 'IA GPT-4o active' : 'IA non configurée'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="p-3 border-t border-slate-700/40 text-xs text-slate-600">
|
|
<div>© 2026 GeoOptions</div>
|
|
<div className="text-slate-700 mt-0.5">Local · IBKR ready</div>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|