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

@ -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():