feat(finlab): import de relevé par image (upload dashboard → scan Console IA) (#69)

Process dédié pour actualiser le portefeuille à partir d'une capture de relevé :

- Dashboard : bouton « 📥 Importer un relevé » (panneau Mes comptes) → upload de
  l'image vers le workspace (imports/) via POST /api/import ; une modale donne la
  phrase exacte à dire à la console + un lien direct
- Backend : endpoints /api/import (UploadFile, refuse les non-images) et /api/imports
  (listing), dossier imports/ sous FINLAB_HOME (workspace, persistant)
- Persona console (CLAUDE.md) : workflow « Import d'un relevé » — lire l'image,
  extraire compte/positions, mapper vers tickers Yahoo en VÉRIFIANT les cours,
  confirmer, puis mettre à jour portfolio.yaml

Le scan vision se fait dans la Console IA (abonnement, pas de clé API). Le dashboard
n'est que le pont d'upload. Testé : POST image→200 + listing OK, non-image→400 ;
JS node --check OK.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-30 11:00:29 +02:00 committed by GitHub
parent 2228613050
commit 8bbfe88a30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 2 deletions

View file

@ -101,7 +101,10 @@
<!-- Colonne gauche : portefeuille + watchlists -->
<div>
<div class="panel">
<h3>Mes comptes <span class="muted" id="pfCount"></span></h3>
<h3>Mes comptes <span class="muted" id="pfCount"></span>
<button class="btn perbtn" style="font-size:11px" title="Importer une capture de relevé (la Console IA la scanne)" onclick="document.getElementById('importFile').click()">📥 Importer un relevé</button>
</h3>
<input type="file" id="importFile" accept="image/*" style="display:none" onchange="uploadImport(this.files[0])">
<div class="acct-tabs" id="acctTabs"></div>
<div id="pfList"><div class="loading">Chargement…</div></div>
</div>
@ -190,6 +193,17 @@
</div>
</div>
<!-- Modal import de relevé -->
<div class="modal" id="importModal" onclick="if(event.target===this)this.classList.remove('on')">
<div class="box">
<h3 style="margin-bottom:10px">Importer un relevé (image)</h3>
<div id="importOut" class="muted"></div>
<div class="controls" style="margin-top:12px">
<button class="btn" onclick="document.getElementById('importModal').classList.remove('on')">Fermer</button>
</div>
</div>
</div>
<script>
let CHART, S = {}, SEL=null, PERIOD='6mo', CCY='$';
const fmt = (n,d=0)=> n==null? '—' : Number(n).toLocaleString('fr-FR',{minimumFractionDigits:d,maximumFractionDigits:d});
@ -384,6 +398,26 @@ async function runPlan(){
}catch(e){ out.textContent='Erreur: '+e.message; }
}
// ── Import de relevé (image → scannée par la Console IA) ────
async function uploadImport(f){
const inp=document.getElementById('importFile');
if(!f){ return; }
const m=document.getElementById('importModal'), out=document.getElementById('importOut');
m.classList.add('on'); out.innerHTML='<div class="loading">Envoi de '+f.name+'…</div>';
try{
const fd=new FormData(); fd.append('file', f);
const r=await fetch('/api/import',{method:'POST',body:fd});
const j=await r.json();
if(j.error) throw new Error(j.error);
out.innerHTML = `<p style="color:var(--up)">✅ Relevé enregistré : <code>${j.rel}</code></p>
<p>Ouvre la <b>Console IA</b> et demande&nbsp;:</p>
<pre>importe le relevé ${j.rel} et mets à jour mon portefeuille</pre>
<p class="muted" style="font-size:12px">Je lirai l'image, je mapperai chaque ligne vers son ticker Yahoo (en vérifiant le cours), puis je mettrai à jour <code>portfolio.yaml</code> <b>après ta confirmation</b>.</p>
<a class="btn acc" href="/console/" target="_blank">Ouvrir la Console IA ↗</a>`;
}catch(e){ out.innerHTML='<div class="err">Erreur: '+e.message+'</div>'; }
finally{ inp.value=''; }
}
// ── Chaîne de valeur IA (couches) ──────────────────────────
const LAYER_META = {
energy_power: {ic:'⚡', lb:'Énergie', ds:'alimente les datacenters'},