mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-09 01:14:42 +02:00
* feat(finlab): console Claude Code finance in-cluster + toolkit d'analyse Intègre finlab (ex-projet Projets/Finance) au lab comme une console Claude Code web spécialisée finance — l'esprit OpenAlice, mais c'est le vrai Claude Code sur l'abonnement (login persisté, pas d'API facturée), agentique, avec la boîte à outils finlab (Yahoo Finance) branchée en MCP. - tools/finlab/ : source finlab rapatriée + Dockerfile (Python 3.12 + Node + claude-code + ttyd) + persona workspace/CLAUDE.md + branchement MCP + entrypoint (seed du workspace no-clobber sur le PVC) - .github/workflows/build-finlab.yml : build GHCR funk-finlab + bump manifest (main) - k8s/apps/finlab/ : Deployment/Service/PVC/IngressRoute (finance.lab.local) + Middleware basicAuth (shell web protégé) ; PVC = HOME (login) + workspace - k8s/apps-of-apps/apps/finlab.yaml : Application ArgoCD - .mcp.json (racine) : outils finlab dans les sessions Claude Code du lab - admin/ia/finlab.md + READMEs + CLAUDE.md : doc + enregistrement Analyse/aide à la décision uniquement — aucun ordre réel (paper trading Alpaca fictif seul exécutable). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(finlab): ttyd absent des dépôts bookworm → binaire statique GitHub Le build amont échouait (`E: Package 'ttyd' has no installation candidate`) : ttyd n'est pas packagé dans Debian bookworm. On récupère le binaire statique (musl, pin TTYD_VERSION=1.7.7) depuis les releases GitHub. Build complet validé en local (podman) : ttyd 1.7.7, claude-code 2.1.195, import finlab + seed OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
4.3 KiB
HTML
78 lines
4.3 KiB
HTML
<!doctype html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
<title>FinLab — analyste de marché</title>
|
|
<style>
|
|
:root { --bg:#0d1117; --panel:#161b22; --line:#30363d; --txt:#e6edf3; --mut:#8b949e; --accent:#2f81f7; --green:#3fb950; }
|
|
* { box-sizing:border-box; }
|
|
body { margin:0; font:15px/1.55 -apple-system,Segoe UI,Roboto,sans-serif; background:var(--bg); color:var(--txt); height:100vh; display:flex; flex-direction:column; }
|
|
header { padding:12px 18px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:10px; }
|
|
header b { font-size:16px; } header span { color:var(--mut); font-size:13px; }
|
|
#log { flex:1; overflow-y:auto; padding:20px; max-width:900px; width:100%; margin:0 auto; }
|
|
.msg { margin:0 0 18px; }
|
|
.msg .who { font-size:12px; color:var(--mut); margin-bottom:4px; }
|
|
.msg.user .bubble { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:10px 14px; }
|
|
.bubble { white-space:pre-wrap; word-wrap:break-word; }
|
|
.bubble pre { background:#0a0e14; border:1px solid var(--line); border-radius:8px; padding:10px; overflow-x:auto; font:12.5px/1.4 ui-monospace,Menlo,monospace; }
|
|
.tool { color:var(--mut); font-size:12px; font-style:italic; margin:4px 0; }
|
|
form { border-top:1px solid var(--line); padding:14px; display:flex; gap:10px; max-width:900px; width:100%; margin:0 auto; }
|
|
textarea { flex:1; background:var(--panel); border:1px solid var(--line); border-radius:10px; color:var(--txt); padding:10px 14px; resize:none; font:15px inherit; max-height:160px; }
|
|
button { background:var(--accent); border:0; color:#fff; border-radius:10px; padding:0 18px; font-weight:600; cursor:pointer; }
|
|
button:disabled { opacity:.5; cursor:default; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header><b>📈 FinLab</b><span>analyste de marché · Claude Opus · analyse uniquement, pas de conseil financier</span></header>
|
|
<div id="log"></div>
|
|
<form id="f">
|
|
<textarea id="q" rows="1" placeholder="Ex : fais le digest · scanne l'énergie · plan de trade sur TLN · faut-il s'inquiéter pour Micron ?"></textarea>
|
|
<button id="send">Envoyer</button>
|
|
</form>
|
|
<script>
|
|
const log = document.getElementById('log'), q = document.getElementById('q'), f = document.getElementById('f'), btn = document.getElementById('send');
|
|
let history = [];
|
|
|
|
function esc(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
// rendu léger : blocs ``` → <pre>, gras **x**, reste en texte (white-space:pre-wrap gère les sauts de ligne)
|
|
function render(t){
|
|
const parts = t.split(/```/);
|
|
return parts.map((p,i)=> i%2 ? '<pre>'+esc(p.replace(/^\w*\n/,''))+'</pre>'
|
|
: esc(p).replace(/\*\*(.+?)\*\*/g,'<b>$1</b>')).join('');
|
|
}
|
|
function add(role, html){
|
|
const d = document.createElement('div'); d.className='msg '+role;
|
|
d.innerHTML = '<div class="who">'+(role==='user'?'Toi':'FinLab')+'</div><div class="bubble">'+html+'</div>';
|
|
log.appendChild(d); log.scrollTop = log.scrollHeight; return d.querySelector('.bubble');
|
|
}
|
|
|
|
f.onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const text = q.value.trim(); if(!text) return;
|
|
q.value=''; btn.disabled=true;
|
|
add('user', esc(text));
|
|
history.push({role:'user', content:text});
|
|
|
|
const bubble = add('assistant',''); let raw='';
|
|
const res = await fetch('/api/chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({messages:history})});
|
|
const reader = res.body.getReader(); const dec = new TextDecoder(); let buf='';
|
|
while(true){
|
|
const {value,done} = await reader.read(); if(done) break;
|
|
buf += dec.decode(value,{stream:true});
|
|
const lines = buf.split('\n\n'); buf = lines.pop();
|
|
for(const line of lines){
|
|
if(!line.startsWith('data:')) continue;
|
|
const {event,data} = JSON.parse(line.slice(5));
|
|
if(event==='delta'){ raw+=data; bubble.innerHTML=render(raw); }
|
|
else if(event==='tool'){ bubble.insertAdjacentHTML('beforeend','<div class="tool">⚙︎ '+data+'…</div>'); }
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
}
|
|
history.push({role:'assistant', content:raw});
|
|
btn.disabled=false; q.focus();
|
|
};
|
|
q.addEventListener('keydown', e=>{ if(e.key==='Enter'&&!e.shiftKey){ e.preventDefault(); f.requestSubmit(); }});
|
|
</script>
|
|
</body>
|
|
</html>
|