feat: market events filters, no-auto-bootstrap, star label deoverlap
- MarketEvents: date presets (7j/30j/3m/6m/1an/Tout/Perso), instrument impact filter (ticker + min score + direction → auto-sort by inst score), sort controls (date/score/nom + asc/desc), clear-all button, count bar - Market events list endpoint: full SQL JOIN rewrite supporting instrument filter, date range, origin, sort_by=instrument_score - Disable auto-bootstrap on startup (macro/eco/categories) — manual only - CycleActions: bootstrap group (Macro/Eco/Categories) with force checkbox, grouped layout (detection / bootstrap / data / ai / portfolio) - cycle_actions router: /bootstrap-macro, /bootstrap-eco, /bootstrap-categories endpoints + group field on all action catalogue entries - InstrumentChart: greedy row placement for star labels (4 rows × 20px) to eliminate horizontal overlap; labels now inline (★ + text) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Play, CheckSquare, Clock, AlertCircle, ChevronDown, ChevronUp, Loader2 } from 'lucide-react'
|
||||
import { Play, CheckSquare, Clock, AlertCircle, ChevronDown, ChevronUp, Loader2, Database } from 'lucide-react'
|
||||
|
||||
const API = ''
|
||||
|
||||
@@ -9,6 +9,7 @@ interface ActionDef {
|
||||
description: string
|
||||
status: 'idle' | 'running'
|
||||
implemented: boolean
|
||||
group: string
|
||||
}
|
||||
|
||||
interface CreatedEvent {
|
||||
@@ -49,6 +50,73 @@ const CAT_COLORS: Record<string, string> = {
|
||||
technical: 'text-cyan-400',
|
||||
}
|
||||
|
||||
// ── Simple bootstrap action button ───────────────────────────────────────────
|
||||
|
||||
function BootstrapAction({ action }: { action: ActionDef }) {
|
||||
const [running, setRunning] = useState(false)
|
||||
const [result, setResult] = useState<{ inserted?: number; updated?: number; skipped?: number } | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [force, setForce] = useState(false)
|
||||
|
||||
const run = async () => {
|
||||
setRunning(true)
|
||||
setResult(null)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${API}/api/actions/${action.id}?force=${force}`, { method: 'POST' })
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
||||
throw new Error(err.detail || res.statusText)
|
||||
}
|
||||
const data = await res.json()
|
||||
setResult(data)
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-dark-800/60 border border-slate-700/40 rounded-xl px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database size={14} className="text-blue-400 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-slate-100 text-sm font-medium">{action.label}</div>
|
||||
<div className="text-slate-500 text-xs mt-0.5">{action.description}</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-1.5 text-xs text-slate-500 cursor-pointer shrink-0">
|
||||
<input type="checkbox" checked={force} onChange={e => setForce(e.target.checked)}
|
||||
className="w-3 h-3 accent-blue-500" />
|
||||
Force
|
||||
</label>
|
||||
<button
|
||||
disabled={running}
|
||||
onClick={run}
|
||||
className="flex items-center gap-2 bg-blue-700 hover:bg-blue-600 disabled:bg-slate-700
|
||||
disabled:text-slate-500 text-white text-xs font-medium rounded-lg px-3 py-2 transition-colors shrink-0"
|
||||
>
|
||||
{running ? <Loader2 size={12} className="animate-spin" /> : <Play size={12} />}
|
||||
{running ? 'En cours...' : 'Lancer'}
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="mt-2 flex items-center gap-2 text-red-400 text-xs bg-red-900/20 rounded px-3 py-1.5 border border-red-800/30">
|
||||
<AlertCircle size={12} /> {error}
|
||||
</div>
|
||||
)}
|
||||
{result && !error && (
|
||||
<div className="mt-2 text-xs text-emerald-300 bg-emerald-900/20 rounded px-3 py-1.5 border border-emerald-800/30">
|
||||
✓ Terminé —
|
||||
{result.inserted != null && ` ${result.inserted} insérés`}
|
||||
{result.updated != null && ` · ${result.updated} mis à jour`}
|
||||
{result.skipped != null && ` · ${result.skipped} ignorés`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Check Market Events config panel ────────────────────────────────────────
|
||||
|
||||
interface CheckEventsConfig {
|
||||
@@ -121,7 +189,6 @@ function CheckEventsPanel() {
|
||||
|
||||
return (
|
||||
<div className="bg-dark-800/60 border border-slate-700/40 rounded-xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 px-5 py-4">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
||||
@@ -147,10 +214,8 @@ function CheckEventsPanel() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Config panel */}
|
||||
{showConfig && (
|
||||
<div className="px-5 pb-4 border-t border-slate-700/30 pt-4 grid grid-cols-2 gap-4 text-sm">
|
||||
{/* Sources */}
|
||||
<div>
|
||||
<div className="text-slate-400 mb-2 text-xs uppercase tracking-wide">Sources actives</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -169,8 +234,6 @@ function CheckEventsPanel() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thresholds */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-slate-400">News impact min</span>
|
||||
@@ -208,18 +271,14 @@ function CheckEventsPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mx-5 mb-4 flex items-center gap-2 text-red-400 text-sm bg-red-900/20 rounded-lg px-3 py-2 border border-red-800/30">
|
||||
<AlertCircle size={14} />
|
||||
{error}
|
||||
<AlertCircle size={14} /> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result summary */}
|
||||
{result && (
|
||||
<div className="border-t border-slate-700/30">
|
||||
{/* Summary bar */}
|
||||
<div
|
||||
className="flex items-center gap-4 px-5 py-3 cursor-pointer hover:bg-slate-800/30"
|
||||
onClick={() => setExpanded(x => !x)}
|
||||
@@ -239,8 +298,6 @@ function CheckEventsPanel() {
|
||||
<span className="ml-auto text-xs text-slate-500">{result.duration_s}s</span>
|
||||
{expanded ? <ChevronUp size={14} className="text-slate-400" /> : <ChevronDown size={14} className="text-slate-400" />}
|
||||
</div>
|
||||
|
||||
{/* Event list */}
|
||||
{expanded && allEvents.length > 0 && (
|
||||
<div className="px-5 pb-4 space-y-1">
|
||||
{allEvents.map((ev, i) => (
|
||||
@@ -255,7 +312,6 @@ function CheckEventsPanel() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expanded && allEvents.length === 0 && (
|
||||
<div className="px-5 pb-4 text-sm text-slate-500 italic">
|
||||
Aucun nouvel événement — tout était déjà enregistré ou sous le seuil.
|
||||
@@ -284,6 +340,14 @@ function PlannedActionCard({ action }: { action: ActionDef }) {
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
detection: 'Détection',
|
||||
bootstrap: 'Bootstrap (données initiales)',
|
||||
data: 'Données & Indicateurs',
|
||||
ai: 'Analyse IA',
|
||||
portfolio: 'Portfolio & Rapport',
|
||||
}
|
||||
|
||||
export default function CycleActions() {
|
||||
const [actions, setActions] = useState<ActionDef[]>([])
|
||||
|
||||
@@ -294,30 +358,35 @@ export default function CycleActions() {
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const planned = actions.filter(a => !a.implemented)
|
||||
const groups = ['detection', 'bootstrap', 'data', 'ai', 'portfolio']
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="p-6 space-y-8 max-w-4xl">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-100">Actions Cycle</h1>
|
||||
<p className="text-slate-400 text-sm mt-1">
|
||||
Décomposition du cycle en 8 actions isolées — lancez-les indépendamment pour tester ou rejouer une étape.
|
||||
Décomposition du cycle en actions isolées — lancez-les indépendamment pour tester ou rejouer une étape.
|
||||
Les bootstraps auto au démarrage ont été désactivés.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Implemented actions */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs uppercase tracking-widest text-slate-500 font-semibold">Actions disponibles</div>
|
||||
<CheckEventsPanel />
|
||||
</div>
|
||||
|
||||
{/* Planned */}
|
||||
{planned.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs uppercase tracking-widest text-slate-500 font-semibold">Prochaines actions</div>
|
||||
{planned.map(a => <PlannedActionCard key={a.id} action={a} />)}
|
||||
</div>
|
||||
)}
|
||||
{groups.map(group => {
|
||||
const groupActions = actions.filter(a => a.group === group)
|
||||
if (groupActions.length === 0) return null
|
||||
const hasAny = groupActions.some(a => a.implemented)
|
||||
return (
|
||||
<div key={group} className="space-y-3">
|
||||
<div className="text-xs uppercase tracking-widest text-slate-500 font-semibold border-b border-slate-800/60 pb-1.5">
|
||||
{GROUP_LABELS[group] ?? group}
|
||||
</div>
|
||||
{groupActions.map(action => {
|
||||
if (!action.implemented) return <PlannedActionCard key={action.id} action={action} />
|
||||
if (action.id === 'check-market-events') return <CheckEventsPanel key={action.id} />
|
||||
return <BootstrapAction key={action.id} action={action} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user