mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 16:24: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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import yaml
|
import yaml
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, File, Request, UploadFile
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
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)})
|
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 ──────────────────────────────────────────────────────────────────
|
# ── Statique ──────────────────────────────────────────────────────────────────
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def index():
|
def index():
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,10 @@
|
||||||
<!-- Colonne gauche : portefeuille + watchlists -->
|
<!-- Colonne gauche : portefeuille + watchlists -->
|
||||||
<div>
|
<div>
|
||||||
<div class="panel">
|
<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 class="acct-tabs" id="acctTabs"></div>
|
||||||
<div id="pfList"><div class="loading">Chargement…</div></div>
|
<div id="pfList"><div class="loading">Chargement…</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -190,6 +193,17 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
let CHART, S = {}, SEL=null, PERIOD='6mo', CCY='$';
|
let CHART, S = {}, SEL=null, PERIOD='6mo', CCY='$';
|
||||||
const fmt = (n,d=0)=> n==null? '—' : Number(n).toLocaleString('fr-FR',{minimumFractionDigits:d,maximumFractionDigits:d});
|
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; }
|
}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) ──────────────────────────
|
// ── Chaîne de valeur IA (couches) ──────────────────────────
|
||||||
const LAYER_META = {
|
const LAYER_META = {
|
||||||
energy_power: {ic:'⚡', lb:'Énergie', ds:'alimente les datacenters'},
|
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 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**.
|
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
|
## Méthode de réponse
|
||||||
|
|
||||||
1. Question chiffrée → appelle l'outil pertinent **avant** de conclure.
|
1. Question chiffrée → appelle l'outil pertinent **avant** de conclure.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue