mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-09 01:04:43 +02:00
feat(finlab): import de relevé par image (upload dashboard → scan Console IA)
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:
parent
2228613050
commit
72f22a7063
3 changed files with 89 additions and 2 deletions
|
|
@ -8,12 +8,13 @@ Lancement : uvicorn dashboard.server:app --host 0.0.0.0 --port 8800
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import yaml
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi import FastAPI, File, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
|
|
@ -167,6 +168,34 @@ def api_plan(ticker: str, capital: float = 1427.0):
|
|||
return _clean({"plan": p, "render": plan_mod.render(p)})
|
||||
|
||||
|
||||
# ── Import de relevés (image → analysée par la Console IA) ─────────────────────
|
||||
IMPORTS_DIR = data.ROOT / "imports"
|
||||
_IMG_EXT = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
||||
|
||||
|
||||
@app.post("/api/import")
|
||||
async def api_import(file: UploadFile = File(...)):
|
||||
"""Enregistre une capture de relevé dans le workspace (imports/) ; la Console IA la scanne
|
||||
ensuite pour mettre à jour portfolio.yaml (vision côté abonnement, pas de clé API ici)."""
|
||||
ext = Path(file.filename or "").suffix.lower()
|
||||
if ext not in _IMG_EXT:
|
||||
return JSONResponse(status_code=400, content={"error": f"format image non supporté: {ext or '?'}"})
|
||||
IMPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ts = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
name = f"releve-{ts}{ext}"
|
||||
(IMPORTS_DIR / name).write_bytes(await file.read())
|
||||
return {"saved": name, "rel": f"imports/{name}"}
|
||||
|
||||
|
||||
@app.get("/api/imports")
|
||||
def api_imports():
|
||||
if not IMPORTS_DIR.exists():
|
||||
return {"imports": []}
|
||||
files = sorted((f for f in IMPORTS_DIR.iterdir() if f.suffix.lower() in _IMG_EXT),
|
||||
key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
return {"imports": [f.name for f in files]}
|
||||
|
||||
|
||||
# ── Statique ──────────────────────────────────────────────────────────────────
|
||||
@app.get("/")
|
||||
def index():
|
||||
|
|
|
|||
|
|
@ -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 :</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'},
|
||||
|
|
|
|||
|
|
@ -56,6 +56,30 @@ courtier + BNP Paribas PEA), **`watchlists.yaml`** (thèmes), **`alerts.yaml`**
|
|||
les modifier à la demande (ex. « ajoute 5 actions Micron sur Revolut »), puis relancer les outils.
|
||||
Les agrégats (`portfolio_summary`, alertes `portfolio`, scan) couvrent **tous les comptes**.
|
||||
|
||||
## Import d'un relevé (image → portefeuille)
|
||||
|
||||
L'utilisateur peut uploader une **capture de relevé** depuis le dashboard (bouton « Importer un
|
||||
relevé ») ; l'image atterrit dans **`imports/`** du workspace. Quand on te demande d'importer un
|
||||
relevé (« importe le relevé imports/… et mets à jour mon portefeuille ») :
|
||||
|
||||
1. **Lis l'image** (`Read imports/<fichier>`). S'il n'y a pas de chemin, prends la plus récente
|
||||
de `imports/` (`ls -t imports/`).
|
||||
2. **Extrais** : le **nom du compte** (banque/courtier visible sur la capture) et son **type**
|
||||
(`banque`/`courtier`/`pea`/`crypto`), les **liquidités**, et chaque ligne : libellé + **ISIN**
|
||||
ou code si présent, **quantité**, **PRU** (prix de revient), devise.
|
||||
3. **Mappe chaque ligne vers son ticker Yahoo Finance** (actions FR/EU → suffixe `.PA`/`.AS` ;
|
||||
ETF par leur mnémonique, ex. `PAEEM.PA`). **VÉRIFIE** chaque ticker avec
|
||||
`python -m finlab.cli` ou l'outil `price` : le **cours doit coller au relevé** (sinon mauvais
|
||||
ticker, cherche la bonne variante).
|
||||
4. **Montre le compte reconstruit** (tableau ticker/qté/PRU + total) et **demande confirmation
|
||||
avant d'écrire**.
|
||||
5. Mets à jour **`portfolio.yaml`** (workspace) : **ajoute** le compte, ou **remplace**-le s'il
|
||||
existe déjà (même nom), au format `accounts:`. Respecte les nombres exactement.
|
||||
6. Relance `portfolio_summary` pour valider que les totaux collent au relevé. Rappelle que
|
||||
c'est analyse only (aucun ordre réel).
|
||||
|
||||
> C'est `portfolio.yaml` du **workspace** (PVC) qui fait foi au runtime — c'est lui que tu édites.
|
||||
|
||||
## Méthode de réponse
|
||||
|
||||
1. Question chiffrée → appelle l'outil pertinent **avant** de conclure.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue