Funk-lab/tools/finlab/webapp/static/index.html
ALI YESILKAYA 54f6f7c634
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>
2026-06-29 23:26:09 +02:00

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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// 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>