Ajoute CLAUDE.md (guide d'architecture pour Claude Code)

This commit is contained in:
jerem
2026-06-19 00:41:40 +02:00
parent df66d33031
commit 47ec36faf1

130
CLAUDE.md Normal file
View File

@@ -0,0 +1,130 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Vue d'ensemble
Monitorink transforme une **Kobo Libra 2** (e-reader e-ink) en écran de monitoring domestique.
Le backend agrège plusieurs sources (usage **Claude** + **Codex**, météo, NAS, ratios trackers
torrent), rend un dashboard HTML, le capture en PNG niveaux de gris, et le sert à la Kobo qui
l'affiche puis se rendort. La fonctionnalité phare est le **% de tokens restants** des
abonnements Claude/Codex (les API ne renvoient qu'une *utilisation* en %, pas de compteur absolu).
Trois zones :
- **`backend/`** — serveur FastAPI Python qui agrège les données et génère l'image.
- **`kobo/`** — scripts shell client pour la Kobo (overlay sur `usetrmnl/trmnl-kobo`).
- **`dev/`** — utilitaires de dev (`preview.py` rendu hors-ligne, `probe_usage.py` sonde API).
## Commandes
Le venv local est `backend/.venv` (Python 3.9 sur le Mac de dev ; le conteneur, lui, est en 3.x
via l'image Playwright). Toujours utiliser le binaire du venv.
```sh
# Dev backend (depuis backend/)
cd backend
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python -m playwright install chromium # navigateur pour le rendu
cp ../.env.example ../.env # puis compléter
.venv/bin/uvicorn app:app --reload --port 8080
# -> http://localhost:8080/image.png et /debug.html (HTML brut, itération design rapide)
# Aperçu design hors-ligne (données fictives, aucune dépendance réseau) — depuis la racine
backend/.venv/bin/python dev/preview.py docs/preview.png
# Sonde l'endpoint /usage de Claude (debug auth/refresh)
backend/.venv/bin/python dev/probe_usage.py
# Vérif syntaxe rapide (pas de suite de tests dans ce repo)
backend/.venv/bin/python -m py_compile backend/render.py backend/integrations/*.py
sh -n kobo/monitorinkloop.sh # lint shell des scripts Kobo
# Déploiement serveur
docker compose up -d --build
# Valider le rendu de la config compose sans .env réel :
HOME=/tmp MONITORINK_DOMAIN=d.example.com MONITORINK_NETWORK=net docker compose config
```
Il n'y a **pas de suite de tests automatisée** : la validation se fait via `py_compile`,
`dev/preview.py` (rendu visuel), et le test réel sur l'appareil.
## Architecture du rendu
Le flux central (`backend/render.py`) :
1. `build_context()` récupère **toutes les sources en parallèle** (`asyncio.gather`) et assemble
le contexte du template Jinja2.
2. Le template `templates/dashboard.html` est rendu en HTML, **canevas paysage 1680×1264**.
3. Playwright (Chromium) capture le HTML en PNG via `page.set_content()` (pas de base URL →
les polices woff2 sont embarquées en data-URI base64 par `fonts.py`).
4. Le PNG est converti en niveaux de gris (`mode 'L'`) puis **pivoté de 90°** (`config.rotate`)
pour le panneau e-ink physiquement en portrait (1264×1680).
`config.py` charge **toute** la configuration depuis l'environnement / `.env` (dataclass `Config`
gelée, instance singleton `config`). Aucune valeur sensible n'est versionnée. Les trackers sont
configurés dynamiquement via `MONITORINK_TRACKERS=clé1,clé2` + un bloc d'env par clé.
### Endpoints (`backend/app.py`)
- `GET /image.png` — dashboard PNG complet, avec cache TTL (`cache_ttl_seconds`). La Kobo **pousse
sa batterie** ici (`?bat=0-100&chg=0|1`) — voir `integrations/kobo.py`.
- `GET /frame.meta` + `GET /frame.png`**refresh partiel e-ink** (voir ci-dessous).
- `GET /debug.html` — HTML brut sans screenshot, pour itérer sur le design.
- `GET /health` — sonde.
### Refresh partiel e-ink (`backend/frame.py`)
Les full refresh e-ink « flashent » (waveform complète qui efface le ghosting) — visuellement
gênant. `frame.py` calcule **côté serveur** (PIL dispo, pas la Kobo) le diff entre le rendu courant
et le précédent par client, détecte les **bandes horizontales modifiées disjointes**, et renvoie
soit des crops partiels (sans flash), soit un full. Un full n'est forcé qu'au 1er lancement/reset
et toutes les `full_refresh_interval_minutes`. La Kobo parse le bloc texte trivial de `/frame.meta`
(`MODE SEQ NREGIONS` + lignes `i x y w h`) puis fetch chaque région via `/frame.png?region=i`.
## Intégrations (`backend/integrations/`)
Toutes suivent le même contrat : `async def fetch_*()` renvoie une dataclass avec un champ `ok` ;
en erreur transitoire elles **replient sur la dernière valeur connue** plutôt que d'afficher une
erreur à l'écran. Toutes sont **optionnelles sauf Claude** : env vide = section masquée.
- **`claude_usage.py`** — LA pièce délicate. `GET api.anthropic.com/api/oauth/usage` exige le scope
OAuth `user:profile`, que `claude setup-token` n'a PAS (403). On utilise donc un **login Claude
isolé dédié** (`CLAUDE_CONFIG_DIR` séparé, monté sur `/creds/.credentials.json`). Le backend
lit/refresh **ce seul fichier**, jamais le `~/.claude` partagé. Points sensibles : le refresh
token est **rotatif** (on ne re-soumet JAMAIS le même token → reuse-detection), refresh
**proactif** bien avant l'expiration (~8 h de vie), **backoff exponentiel** sur échec (anti-429),
et distinction stricte entre erreur **transitoire** (429/réseau → backoff + repli cache) et
**fatale** (`invalid_grant`/401 → `_RefreshFatal`, alerte « login requis » à l'écran, jamais de
re-soumission en boucle). Écriture des credentials **atomique** (tmp + `os.replace`, mode 0600).
- **`codex.py`** — usage ChatGPT/Codex via `chatgpt.com/backend-api/wham/usage`. Token lu dans un
`auth.json` maintenu frais par un **processus externe** (monté en lecture seule) ; Monitorink ne
refresh pas. Extrait `chatgpt_account_id` du JWT (header requis).
- **`trackers.py`** — ratio des trackers torrent privés. Architecture **multi-fetcher** : chaque
`TrackerSpec.type` choisit une fonction dans `_FETCHERS` (`unit3d_nuxt`, `torr9`, `tr4ker`,
`yggreborn`). Le ratio n'est PAS lisible au token API → **login session** (CSRF + cookies) propre
à chaque techno. Cache long (`tracker_ttl_seconds`, défaut 1 h) **persisté sur disque** (`/data`)
pour survivre aux redéploiements sans reloguer. Pour ajouter un tracker : écrire `_fetch_xxx(spec)`
renvoyant un `TrackerStat` et l'enregistrer dans `_FETCHERS`.
- **`weather.py`** — Open-Meteo (sans clé). **`nas.py`** — endpoint HTTP maison `/api/status`.
- **`kobo.py`** — inverse des autres : la Kobo **pousse** sa batterie (elle seule la connaît), on
stocke la dernière valeur horodatée en mémoire pour le prochain rendu.
## Client Kobo (`kobo/`)
`monitorinkloop.sh` est la boucle d'affichage, **overlay** réutilisant les binaires ARM (`fbink`,
`busybox_kobo`) et helpers WiFi de [`usetrmnl/trmnl-kobo`](https://github.com/usetrmnl/trmnl-kobo)
(à copier dans `bin/`, `scripts/` — non versionnés ici). Cycle : WiFi up → fetch `/frame.meta`
affiche (full/partial via `fbink`) → WiFi down → suspend `rtcwake -m mem` → réveil RTC. Détails
critiques dans `kobo/README.md` :
- **Piège EPDC/VEE** : juste après un refresh e-ink, le pilote EPDC refuse de suspendre (haute
tension VEE pas redescendue) → `suspend_for()` attend ~12 s puis **retente** le suspend (sinon
le CPU tournait 24/24 et vidait la batterie).
- **Reboot** : 3 appuis rapides sur un bouton de page (`reboot_watcher.sh`, evdev `EV_KEY` 193/194).
Sur batterie : un appui power d'abord (réveil) puis les 3 appuis.
## Conventions
- **e-ink = noir & blanc purs, zéro gris** (le gris fantôme au refresh partiel). La hiérarchie passe
par taille/graisse, le « consommé » par des hachures. Polices : Archivo (mots) + JetBrains Mono
(nombres tabulaires), vendorisées en woff2 dans `backend/static/fonts/`.
- **Tout le texte utilisateur et les commentaires sont en français** (accents inclus).
- Modifier `dashboard.html` → vérifier le rendu avec `dev/preview.py` avant de commit.
</content>
</invoke>