Files
Monitorink/CLAUDE.md

131 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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>