diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e269238 --- /dev/null +++ b/CLAUDE.md @@ -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. + +