feat(finlab): dashboard web (graphiques/portefeuille/actions) + fix seed NFS (#65)

Ajoute une interface graphique à finlab, en plus de la console Claude Code (les
deux cohabitent sur finance.lab.local) :

- dashboard/ : backend FastAPI (sans LLM/clé) exposant les données finlab en JSON
  (/api/portfolio, /api/scan, /api/ohlc, /api/plan, /api/alerts) + front statique
  (lightweight-charts) : graphiques chandeliers MM50/200 + volume, portefeuille
  (P&L/poids/secteurs), watchlists thématiques scannées, alertes, plan de trade R:R
- Découplage code/data : finlab lit ses configs depuis FINLAB_HOME si défini
  (workspace persistant) ; repli sur le dossier du package en dév local
- Image double usage (un build, deux conteneurs) : code → /opt/app, seed minimal
  (configs + persona + MCP) → workspace
- Fix seed NFS (cause racine du workspace vide) : WORKDIR n'est plus sur le volume
  (le runtime le créait en root sur NFS) ; seed déplacé dans un initContainer
  robuste (recrée le dossier s'il est non-inscriptible)
- Deployment : initContainer seed + 2 conteneurs (console ttyd -b /console, dashboard
  uvicorn) ; Service 2 ports ; IngressRoute 2 routes (/console + dashboard), basicAuth

Validé en local (podman) : seed OK, dashboard /api/portfolio → 16k€/10 positions
via FINLAB_HOME, endpoints ohlc/scan/plan/alerts OK, ttyd/claude/finlab présents.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-30 00:57:23 +02:00 committed by GitHub
parent 607e5e6c0a
commit 56cb32eacc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 700 additions and 149 deletions

View file

@ -1,32 +1,34 @@
# FinLab — console Claude Code finance (in-cluster)
# FinLab — dashboard + console Claude Code (in-cluster)
Une **vraie session Claude Code** (sur l'**abonnement** d'Alkatrazz, login persisté) servie
dans un **terminal web** (`ttyd`), spécialisée analyse de marché via la persona `CLAUDE.md` et
la boîte à outils **finlab** (Yahoo Finance, exposée en **MCP**). Esprit OpenAlice, mais c'est
Claude Code + le toolkit maison — **aucune facturation API**, **aucun broker**, analyse seule.
Deux usages, **une image**, un workspace partagé :
> ⚠️ La console expose un **shell web** → protégée par **basicAuth Traefik**. Analyse et aide à
> la décision uniquement : **aucun ordre réel** (seul le paper trading Alpaca fictif existe).
- **Dashboard** (`finance.lab.local/`) — interface web : graphiques chandeliers (MM50/200 +
volume), portefeuille (P&L, poids, secteurs), watchlists thématiques scannées, alertes du
jour, actions (plan de trade R:R). Données finlab (Yahoo Finance) — **aucun LLM, aucune clé**.
- **Console** (`finance.lab.local/console/`) — vraie session **Claude Code** dans un terminal
web (`ttyd`), sur ton **abonnement** (login persisté), persona finance + outils finlab en MCP.
## Image
> ⚠️ La console expose un **shell web** → tout est derrière **basicAuth Traefik**. Analyse et
> aide à la décision uniquement : **aucun ordre réel** (paper trading Alpaca fictif seul).
Pas d'image publique : la CI (`.github/workflows/build-finlab.yml`) build depuis
`tools/finlab/` (notre code) → `ghcr.io/alkatrazz24/funk-finlab`, tag `sha-<court>`, bump du
manifest sur `main`.
## Image & code
## Architecture du déploiement
CI (`.github/workflows/build-finlab.yml`) build depuis `tools/finlab/``ghcr.io/alkatrazz24/funk-finlab`.
Le **code** (package `finlab` + backend `dashboard`) vit dans l'image (`/opt/app`) et se met à
jour avec elle. Les **configs éditables** (portefeuille/watchlists/alertes) + la persona + le
login vivent dans le **workspace persistant** (`FINLAB_HOME=/home/finlab/workspace`, sur le PVC).
| Composant | Détail |
## Architecture du pod
| Élément | Rôle |
|---|---|
| Deployment | 1 réplica, `Recreate` (volume RWO), PodSecurity *restricted* (uid 1000, `fsGroup` 1000) |
| PVC `finlab-data` | 5Gi NFS → `/home/finlab` : `~/.claude` (login abonnement) + workspace (code finlab seedé, `portfolio.yaml`/`watchlists.yaml`/`alerts.yaml` éditables, `.cache`) |
| Service `finlab` | `web:7681` (ttyd) |
| Middleware `finlab-auth` | basicAuth Traefik (secret `finlab-auth`, htpasswd) |
| IngressRoute | `finance.lab.local` → 7681 (via `finlab-auth`) |
Le workspace est **seedé depuis l'image au 1er boot** (`cp -rn`, no-clobber) puis vit sur le
PVC : tes edits (portefeuille, etc.) et ton login persistent ; une nouvelle image n'apporte que
les fichiers manquants.
| initContainer `seed` | Peuple le workspace depuis l'image (`cp -rn`), robuste au piège NFS (recrée le dossier si root/non-inscriptible). Tourne **avant** les conteneurs |
| container `console` | `ttyd -b /console``claude` dans le workspace (port 7681) |
| container `dashboard` | `uvicorn dashboard.server:app` (port 8800), lit `FINLAB_HOME` |
| PVC `finlab-data` (5Gi NFS) | `/home/finlab` : `~/.claude` (login) + workspace (configs + `.cache`) |
| Service `finlab` | `web:8800` + `console:7681` |
| Middleware `finlab-auth` | basicAuth Traefik (secret htpasswd) |
| IngressRoute | `/console` → 7681 · reste → 8800 (les deux derrière `finlab-auth`) |
## Pré-requis manuels (une fois)
@ -34,35 +36,21 @@ les fichiers manquants.
```bash
kubectl -n ai create secret docker-registry ghcr-pull \
--docker-server=ghcr.io \
--docker-username=Alkatrazz24 \
--docker-server=ghcr.io --docker-username=Alkatrazz24 \
--docker-password=<GHCR_PAT_read_packages>
```
### 2. Secret basicAuth de la console (`finlab-auth`)
### 2. Secret basicAuth `finlab-auth`
```bash
# htpasswd (paquet apache2-utils). Remplace <user>/<pass>.
htpasswd -nbB <user> '<pass>' > /tmp/finlab.htpasswd
kubectl -n ai create secret generic finlab-auth \
--from-file=users=/tmp/finlab.htpasswd
rm -f /tmp/finlab.htpasswd
--from-literal=users="alkatrazz:$(openssl passwd -apr1 'MOT_DE_PASSE')"
```
### 3. Premier boot — login Claude Code (abonnement)
### 3. Premier boot — login Claude Code (pour la console)
1. Ouvrir **http://finance.lab.local** (basicAuth → identifiants de l'étape 2).
2. Dans le terminal, lancer `claude`, puis `/login` → suivre l'OAuth (ouvrir l'URL sur ta
machine, autoriser, coller le code). Persisté dans `~/.claude` sur le PVC.
3. À l'invite, FinLab a déjà sa persona (`CLAUDE.md`) et les outils finlab (MCP `finlab`,
auto-activé). Essaie : *« résume mon portefeuille »*, *« plan de trade pour Micron »*.
1. Ouvrir **http://finance.lab.local/console/** (basicAuth), lancer `claude`, puis `/login`.
2. Le dashboard est sur **http://finance.lab.local/** (rien à configurer).
> Le workspace `/home/finlab/workspace` contient `portfolio.yaml` etc. — éditables directement
> (par toi ou par Claude à ta demande). Pour une vue rapide hors console :
> `kubectl -n ai exec deploy/finlab -- python -m finlab.cli digest`.
## Accès depuis le terminal (sans navigateur)
```bash
kubectl -n ai exec -it deploy/finlab -- bash -lc 'cd "$WORKSPACE" && claude'
```
> Configs éditables dans le workspace (`portfolio.yaml`…) : depuis la console, ou
> `kubectl -n ai exec deploy/finlab -c console -- python -m finlab.cli digest`.

View file

@ -16,12 +16,9 @@ spec:
labels:
app: finlab
spec:
# Image privée sur GHCR → secret de pull créé manuellement dans le ns ai
# (cf. README : kubectl create secret docker-registry ghcr-pull ...).
imagePullSecrets:
- name: ghcr-pull
# Conformité PodSecurity « restricted » (namespace ai). fsGroup 1000 → le PVC NFS monté
# en /home/finlab (HOME : ~/.claude du login abonnement + workspace) est inscriptible.
# PodSecurity « restricted » (ns ai). fsGroup 1000 → PVC NFS monté en /home/finlab.
securityContext:
runAsNonRoot: true
runAsUser: 1000
@ -29,40 +26,72 @@ spec:
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: finlab
# Tag bumpé par la CI (build-finlab.yml) sur main.
# initContainer : prépare le workspace (seed robuste NFS) AVANT console + dashboard,
# pour qu'il soit peuplé et inscriptible quand les deux conteneurs démarrent.
initContainers:
- name: seed
image: ghcr.io/alkatrazz24/funk-finlab:sha-54f6f7c
command: ["/usr/local/bin/entrypoint-seed.sh"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: home
mountPath: /home/finlab
containers:
# ── CONSOLE : Claude Code dans un terminal web (ttyd), sous /console ──
- name: console
image: ghcr.io/alkatrazz24/funk-finlab:sha-54f6f7c
command: ["/usr/local/bin/entrypoint-console.sh"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
ports:
- name: console
containerPort: 7681
readinessProbe:
tcpSocket: { port: 7681 }
initialDelaySeconds: 10
periodSeconds: 15
livenessProbe:
tcpSocket: { port: 7681 }
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests: { cpu: 150m, memory: 512Mi }
limits: { cpu: "1", memory: 1536Mi }
volumeMounts:
- name: home
mountPath: /home/finlab
# ── DASHBOARD : interface web (FastAPI), graphiques + données finlab ──
- name: dashboard
image: ghcr.io/alkatrazz24/funk-finlab:sha-54f6f7c
command:
- /opt/venv/bin/uvicorn
- dashboard.server:app
- --host=0.0.0.0
- --port=8800
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
ports:
- name: web
containerPort: 7681
env:
- name: TZ
value: "Europe/Paris"
# ttyd écoute en TCP : sonde TCP (pas de endpoint HTTP de santé dédié). Démarrage
# rapide, mais on laisse de la marge (seed du workspace au 1er boot).
containerPort: 8800
# FINLAB_HOME/PYTHONPATH viennent de l'image ; FINLAB_HOME = workspace (configs live).
readinessProbe:
tcpSocket:
port: 7681
initialDelaySeconds: 10
httpGet: { path: /healthz, port: 8800 }
initialDelaySeconds: 8
periodSeconds: 15
livenessProbe:
tcpSocket:
port: 7681
initialDelaySeconds: 30
httpGet: { path: /healthz, port: 8800 }
initialDelaySeconds: 20
periodSeconds: 30
# Node + Claude Code peuvent consommer ; nœuds 8 Go (OOM actif) → limite raisonnable.
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "1"
memory: 1536Mi
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 500m, memory: 768Mi }
volumeMounts:
- name: home
mountPath: /home/finlab

View file

@ -7,11 +7,21 @@ spec:
entryPoints:
- web
routes:
- match: Host(`finance.lab.local`)
# /console → console Claude Code (ttyd sert sous ce préfixe via -b /console).
- match: Host(`finance.lab.local`) && PathPrefix(`/console`)
kind: Rule
priority: 20
middlewares:
# basicAuth : un shell web ne doit pas être ouvert sans auth (cf. middleware.yaml).
- name: finlab-auth
services:
- name: finlab
port: 7681
# Tout le reste → dashboard web (graphiques, portefeuille, watchlists, actions).
- match: Host(`finance.lab.local`)
kind: Rule
priority: 10
middlewares:
- name: finlab-auth
services:
- name: finlab
port: 8800

View file

@ -8,5 +8,8 @@ spec:
app: finlab
ports:
- name: web
port: 8800
targetPort: 8800
- name: console
port: 7681
targetPort: 7681