mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 08:54:42 +02:00
feat(finlab): console Claude Code finance in-cluster + toolkit d'analyse (#64)
* 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>
This commit is contained in:
parent
35ad1deb64
commit
54f6f7c634
39 changed files with 2115 additions and 2 deletions
78
tools/finlab/webapp/static/index.html
Normal file
78
tools/finlab/webapp/static/index.html
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<!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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue