feat: system logs page + dynamic IV watchlist with auto-add from cycle
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>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { useIvWatchlist, useIvSnapshot, useIvHistory } from '../hooks/useApi'
|
||||
import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp, Database } from 'lucide-react'
|
||||
import { useIvWatchlist, useIvSnapshot, useIvHistory, useWatchlistTickers, useAddWatchlistTicker, useRemoveWatchlistTicker } from '../hooks/useApi'
|
||||
import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp, Database, Plus, Trash2, List } from 'lucide-react'
|
||||
import { api } from '../hooks/useApi'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -247,6 +248,106 @@ function WatchlistRow({ item }: { item: any }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Watchlist Manager ─────────────────────────────────────────────────────────
|
||||
function WatchlistManager() {
|
||||
const qc = useQueryClient()
|
||||
const { data } = useWatchlistTickers()
|
||||
const { mutate: addTicker, isPending: adding } = useAddWatchlistTicker()
|
||||
const { mutate: removeTicker } = useRemoveWatchlistTicker()
|
||||
const [input, setInput] = useState('')
|
||||
const [showInactive, setShowInactive] = useState(false)
|
||||
|
||||
const entries: any[] = data?.tickers ?? []
|
||||
const active = entries.filter(e => e.is_active)
|
||||
const inactive = entries.filter(e => !e.is_active)
|
||||
|
||||
const handleAdd = () => {
|
||||
const t = input.trim().toUpperCase()
|
||||
if (!t) return
|
||||
addTicker(t, {
|
||||
onSuccess: () => {
|
||||
setInput('')
|
||||
qc.invalidateQueries({ queryKey: ['watchlist-tickers'] })
|
||||
qc.invalidateQueries({ queryKey: ['iv-watchlist'] })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemove = (ticker: string) => {
|
||||
removeTicker(ticker, {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['watchlist-tickers'] })
|
||||
qc.invalidateQueries({ queryKey: ['iv-watchlist'] })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<List className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm font-semibold text-slate-300">Gestion de la Watchlist IV</span>
|
||||
<span className="ml-auto text-xs text-slate-600">{active.length} tickers actifs</span>
|
||||
</div>
|
||||
|
||||
{/* Add ticker */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-xs text-slate-300 placeholder:text-slate-600"
|
||||
placeholder="Ajouter ticker (ex: AAPL, FXI, EEM…)"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value.toUpperCase())}
|
||||
onKeyDown={e => e.key === 'Enter' && handleAdd()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={adding || !input.trim()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-600/40 text-blue-400 rounded disabled:opacity-40 transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
{adding ? 'Ajout...' : 'Ajouter'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600">Le système bootstrappe automatiquement 1 an d'historique IV pour chaque nouveau ticker ajouté.</p>
|
||||
|
||||
{/* Active tickers */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{active.map(e => (
|
||||
<span key={e.ticker} className={clsx(
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-mono',
|
||||
e.added_by === 'builtin' ? 'bg-slate-800 text-slate-400 border border-slate-700/50' :
|
||||
e.added_by === 'cycle' ? 'bg-blue-900/30 text-blue-300 border border-blue-700/30' :
|
||||
'bg-emerald-900/20 text-emerald-300 border border-emerald-700/30'
|
||||
)}>
|
||||
{e.ticker}
|
||||
{e.added_by !== 'builtin' && (
|
||||
<button onClick={() => handleRemove(e.ticker)} className="opacity-50 hover:opacity-100 ml-0.5">
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-700 flex gap-3">
|
||||
<span className="text-slate-800 border-b border-slate-700">■</span> Builtins
|
||||
<span className="text-blue-700 border-b border-blue-700">■</span> Auto (cycle)
|
||||
<span className="text-emerald-700 border-b border-emerald-700">■</span> Manuel
|
||||
</div>
|
||||
|
||||
{inactive.length > 0 && (
|
||||
<button onClick={() => setShowInactive(!showInactive)} className="text-[10px] text-slate-600 hover:text-slate-400">
|
||||
{showInactive ? 'Masquer' : `Voir ${inactive.length} ticker(s) désactivés`}
|
||||
</button>
|
||||
)}
|
||||
{showInactive && inactive.map(e => (
|
||||
<span key={e.ticker} className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-mono bg-slate-900 text-slate-700 line-through mr-1">
|
||||
{e.ticker}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
export default function OptionsLab() {
|
||||
const { data, isLoading, refetch, isFetching } = useIvWatchlist()
|
||||
@@ -390,6 +491,8 @@ export default function OptionsLab() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<WatchlistManager />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user