feat(finlab): console embarquée sous le graphique + vue chaîne de valeur IA

- Dashboard : la console Claude Code (/console/) est désormais embarquée en iframe
  sous le graphique (boutons reconnecter / plein écran), en plus du lien topbar
- Nouvelle vue pleine largeur « Chaîne de valeur IA — par couche » : regroupe les
  watchlists par couche de l'électron au logiciel ( énergie → 🔧 puces →
  🏢 datacenter → 🔌 câbles/optique → ☁️ software), tickers cliquables colorés
  par biais (haussier/baissier), tooltip cours/RSI/MACD
- Backend : endpoint /api/layers (scan groupé par thème, ordre des couches)

Vérifié au navigateur (Playwright, instance locale) : layout complet OK —
portefeuille + graphique chandeliers MM50/200 + console embarquée + alertes +
5 cartes de couches colorées. Endpoints et JS validés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
alkatrazz 2026-06-30 01:21:15 +02:00
parent 4c61758899
commit b704860227
2 changed files with 99 additions and 1 deletions

View file

@ -94,6 +94,24 @@ def api_scan(theme: str = "all", target: float = 5.0, bullish: bool = False):
return {"theme": theme, "target": target, "rows": _records(df)}
# Couches de la chaîne de valeur IA/datacenter, de l'électron au logiciel.
LAYER_ORDER = ["energy_power", "chips", "datacenter_infra", "cables_optical_network", "software_cloud"]
@app.get("/api/layers")
def api_layers(target: float = 5.0):
"""Watchlists regroupées par couche de la chaîne IA, avec biais/signal par titre."""
themes = yaml.safe_load(open(scanner.WATCHLISTS_FILE, encoding="utf-8"))["themes"]
order = LAYER_ORDER + [k for k in themes if k not in LAYER_ORDER]
layers = []
for key in order:
if key not in themes:
continue
df = scanner.scan(themes[key], target)
layers.append({"key": key, "tickers": _records(df)})
return {"layers": layers}
@app.get("/api/ohlc")
def api_ohlc(ticker: str, period: str = "6mo"):
df = data.history(ticker, period=period)

View file

@ -60,6 +60,23 @@
.modal pre{background:var(--bg);padding:12px;border-radius:8px;overflow:auto;white-space:pre-wrap;font-size:13px;color:var(--txt)}
.err{color:var(--down);font-size:12px}
.loading{color:var(--mut);font-size:12px;padding:8px}
/* Console embarquée */
#consoleFrame{width:100%;height:340px;border:0;border-radius:8px;background:#0b0e14}
/* Chaîne de valeur IA */
.layers-wrap{padding:0 12px 16px}
.layers{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
.layer-card{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px;position:relative}
.layer-card:not(:last-child)::after{content:"→";position:absolute;right:-9px;top:50%;transform:translateY(-50%);color:var(--mut);font-size:14px;z-index:1}
.layer-h{display:flex;gap:8px;align-items:center;margin-bottom:9px}
.layer-ic{font-size:20px;line-height:1}
.layer-h b{color:#fff}
.lchips{display:flex;flex-wrap:wrap;gap:5px}
.lchip{background:#0e131d;border:1px solid var(--line);border-radius:6px;padding:3px 7px;font-size:12px;cursor:pointer;font-weight:600}
.lchip:hover{border-color:var(--acc);color:#fff}
.lchip.up{border-color:var(--up);color:#7fdfd0}
.lchip.down{border-color:var(--down);color:#f0a08c}
.lchip.sel{outline:1px solid var(--acc)}
@media(max-width:1100px){.layers{grid-template-columns:repeat(2,1fr)} .layer-card::after{display:none}}
</style>
</head>
<body>
@ -116,6 +133,16 @@
<button class="btn" onclick="loadAlerts()">🔔 Recharger alertes</button>
</div>
</div>
<!-- Console IA embarquée, sous le graphique -->
<div class="panel">
<h3>Console IA
<span>
<button class="btn perbtn" onclick="document.getElementById('consoleFrame').src='/console/'">↻ reconnecter</button>
<a class="btn perbtn" href="/console/" target="_blank">plein écran ↗</a>
</span>
</h3>
<iframe id="consoleFrame" src="/console/" title="Console Claude Code"></iframe>
</div>
</div>
<!-- Colonne droite : alertes -->
@ -132,6 +159,17 @@
</div>
</div>
<!-- Chaîne de valeur IA — pleine largeur -->
<div class="layers-wrap">
<div class="panel">
<h3>Chaîne de valeur IA — par couche
<span class="muted" style="text-transform:none;letter-spacing:0">de l'électron au logiciel &nbsp; ⚡→🔧→🏢→🔌→☁️ &nbsp;·&nbsp;
<span class="pos">haussier</span> / <span class="neg">baissier</span> · clic = graphique</span>
</h3>
<div class="layers" id="layers"><div class="loading">Analyse des couches…</div></div>
</div>
</div>
<!-- Modal plan de trade -->
<div class="modal" id="planModal" onclick="if(event.target===this)this.classList.remove('on')">
<div class="box">
@ -318,12 +356,54 @@ async function runPlan(){
}catch(e){ out.textContent='Erreur: '+e.message; }
}
// ── Chaîne de valeur IA (couches) ──────────────────────────
const LAYER_META = {
energy_power: {ic:'⚡', lb:'Énergie', ds:'alimente les datacenters'},
chips: {ic:'🔧', lb:'Puces / Semi', ds:'le calcul'},
datacenter_infra: {ic:'🏢', lb:'Datacenter / Infra', ds:'serveurs · refroidissement'},
cables_optical_network: {ic:'🔌', lb:'Câbles / Optique', ds:'interconnexion'},
software_cloud: {ic:'☁️', lb:'Software / Cloud', ds:'la couche logicielle'},
};
const LAYER_ORDER = ['energy_power','chips','datacenter_infra','cables_optical_network','software_cloud'];
function biasCls(b){
if(!b) return '';
if(b.includes('haussier')||b.includes('survendu')) return 'up';
if(b.includes('baissier')||b.includes('surachat')) return 'down';
return '';
}
async function loadLayers(){
const wrap=document.getElementById('layers');
// 1) structure instantanée depuis /api/themes
let t;
try{ t=(await api('/api/themes')).themes; }
catch(e){ wrap.innerHTML='<div class="err">'+e.message+'</div>'; return; }
const order=LAYER_ORDER.filter(k=>t[k]).concat(Object.keys(t).filter(k=>!LAYER_ORDER.includes(k)));
wrap.innerHTML=order.map(k=>{
const m=LAYER_META[k]||{ic:'•',lb:k,ds:''};
const chips=(t[k]||[]).map(tk=>`<span class="lchip" data-tk="${tk}" onclick="loadChart('${tk}')">${tk}</span>`).join('');
return `<div class="layer-card" data-layer="${k}">
<div class="layer-h"><span class="layer-ic">${m.ic}</span>
<div><b>${m.lb}</b><div class="muted" style="font-size:11px">${m.ds}</div></div></div>
<div class="lchips">${chips}</div></div>`;
}).join('');
// 2) enrichissement biais (scan de toutes les couches — peut être un peu long, caché ensuite)
try{
const d=await api('/api/layers');
d.layers.forEach(L=>(L.tickers||[]).forEach(r=>{
const el=document.querySelector(`.layer-card[data-layer="${L.key}"] .lchip[data-tk="${r.ticker}"]`);
if(el){ const c=biasCls(r.biais); if(c) el.classList.add(c);
el.title=`${r.ticker} · ${r.cours??'—'} · RSI ${r.RSI??'—'} · MACD ${r.MACD??''} · ${r.biais??''}`; }
}));
}catch(e){ /* on garde la structure non colorée */ }
}
// ── Init ───────────────────────────────────────────────────
function refreshAll(){ loadPortfolio(); loadScan(); loadAlerts(); if(SEL) loadChart(SEL); }
function refreshAll(){ loadPortfolio(); loadScan(); loadAlerts(); loadLayers(); if(SEL) loadChart(SEL); }
initChart();
loadPortfolio();
loadThemes();
loadAlerts();
loadLayers();
</script>
</body>
</html>