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:
OpenSquared
2026-06-25 21:27:21 +02:00
parent 0b1fcff49c
commit f71e3be3ff
6 changed files with 545 additions and 190 deletions

View File

@@ -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 é 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>
)
}