# 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.