- Python 94.6%
- CSS 3.8%
- HCL 0.7%
- Dockerfile 0.4%
- Shell 0.4%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
Build & push Docker image / build (push) Failing after 5m4s
Le 2026-09-02, le coach a ingere les parties, lance ~40 min de Stockfish par joueur, puis decouvert que le CLI n'etait pas authentifie. Un ping haiku de deux secondes avant la boucle rend la panne lisible tout de suite et evite de brasser trois comptes pour rien. preflight_auth() ne leve jamais. En cas d'echec, `_run_all` notifie "Coach non demarre — claude indisponible" avec la cause et sort en 1. Sauter un jour est sans consequence : la fenetre n'avance que sur un run 'done', les parties seront reprises au passage suivant. Fixture autouse dans tests/conftest.py : sans elle, tout test appelant `_run_all` partait en subprocess vers la vraie CLI (suite passee de 9 s a 82 s, et resultat dependant de la machine). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP5xCMda6LKaE4krgnn3F3 |
||
| .forgejo/workflows | ||
| chessia | ||
| docker | ||
| docs/superpowers | ||
| infra/module | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| Dockerfile | ||
| pyproject.toml | ||
| README.md | ||
ChessIA
Pipeline d'analyse pédagogique de parties d'échecs en français, cible FIDE 1600+.
Flux : PGN → Stockfish (multi-PV, seuils adaptatifs) → détection des coups critiques → Claude (routage Opus/Sonnet selon sévérité) → rapport HTML interactif avec graphique d'éval, mini-viewers et explications structurées.
Deux modes d'utilisation :
- CLI (
chessia partie.pgn -o rapport.html) - Web (
chessia-web) — formulaire d'upload + historique + suivi live + notifications push
Installation
python -m venv .venv && source .venv/bin/activate
pip install -e ".[web]" # extras [web] pour le serveur
cp .env.example .env # renseigner STOCKFISH_PATH
Prérequis
- Stockfish UCI avec NNUE (tablebases recommandées).
vendor/stockfish/stockfishpar défaut. - CLI
claude(Claude Code) authentifiée via l'abonnement Max — le pipeline passe parclaude -p(pas l'API Anthropic).
Variables d'environnement (.env)
| Variable | Défaut | Rôle |
|---|---|---|
STOCKFISH_PATH |
— | Chemin du binaire UCI (obligatoire) |
STOCKFISH_HASH_MB |
128 |
Hash table Stockfish en Mo (par process ; l'analyse force Threads=1 pour la reproductibilité, parallélisme par process) |
CLAUDE_CONCURRENCY |
3 |
Appels Claude parallèles |
CLAUDE_MODEL_BLUNDER |
opus |
Modèle pour blunders / occasions manquées |
CLAUDE_MODEL_FORCE |
— | Override tous les modèles (debug) |
CHESSIA_REPORTS_DIR |
~/.chessia-reports |
Dossier des rapports |
CHESSIA_WEB_HOST / CHESSIA_WEB_PORT |
127.0.0.1:8000 |
Écoute du serveur web |
CHESSIA_ACCOUNTS_DB |
/data/coach/accounts.db |
Base des comptes, sessions et consommation de tokens (voir § Comptes) |
CHESSIA_USERS_DIR |
/data/coach/u |
Dossier des bases de coaching, une par joueur (<login>.db) |
CHESSIA_COOKIE_INSECURE |
— | Mettre à 1 pour tester en local sur http:// sans TLS (le cookie de session perd son flag Secure). Ne jamais positionner en production : sans TLS, un cookie sans Secure circule en clair |
NTFY_SERVER / NTFY_TOPIC / NTFY_USER / NTFY_PASSWORD |
— | Notifs push début/fin de diag (voir ntfy.sh) |
Usage
CLI
chessia partie.pgn -o rapport.html # analyse les deux camps
chessia partie.pgn --side white -o rapport.html # limiter au côté Blancs
Serveur web
chessia-web # uvicorn sur 127.0.0.1:8000
Tout le site est derrière une session (middleware à liste blanche dans
chessia/authweb.py) : sans compte, seules /login, /logout, /healthz et
/static/ répondent. Voir § Comptes pour créer le premier compte.
/login,/logout: formulaire de connexion / déconnexion/healthz: sonde de santé, publique, répondoksans authentification/: redirige vers/coach/(rapport du jour), la porte d'entrée du site/analyser: formulaire d'upload (fichier PGN ou coller du texte, drag & drop), onglet « Analyse » de la nav /coach — plus un analyseur public, il faut une session/history: tableau des diagnostics avec filtres et polling live/watch/<id>: suivi live d'un diag en cours (progress bar + timeline + logs colorés)/report/<id>: rapport final avec TOC sticky, coups critiques repliables, export PDF/example: redirige vers un rapport terminé aléatoire du joueur connecté (démo sans uploader son PGN)
Régénérer des rapports existants
Quand l'UI évolue, régénérer les rapports terminés avec le nouveau template (le numéro de version d'origine est préservé dans le rapport) :
python -m chessia.regen # tous les jobs done
python -m chessia.regen --dry-run # aperçu
python -m chessia.regen --job <id> # un seul job
Le script crée un report.html.bak.<timestamp> avant chaque overwrite.
Comptes utilisateurs
Multi-utilisateur depuis la branche multi-utilisateur (02/08/2026) : les comptes sont créés
à la main, il n'y a ni inscription libre ni réinitialisation de mot de passe. Sur une
installation neuve, personne ne peut se connecter tant qu'un compte n'a pas été créé —
c'est la première chose à faire après le déploiement, pas une étape optionnelle.
# Installation vraiment neuve, sans base mono-utilisateur à reprendre — premier
# compte, administrateur :
chessia-admin add timothth TimothTh --admin
# Cas de ce déploiement : reprise de l'ancienne base mono-utilisateur (le compte
# hérite en plus des parties et rapports déjà accumulés). `migrate-legacy` sort
# en erreur (« base introuvable ») s'il n'y a pas de coach.db historique à
# reprendre — ce n'est PAS la commande d'une installation neuve :
chessia-admin migrate-legacy --login timothth --chesscom TimothTh --admin
# Ajouter un ami :
chessia-admin add bob BobChess --ntfy-topic chessia-bob
--ntfy-topic mérite d'être posé dès la création : sans lui, les notifications de coaching
du nouveau joueur retombent sur le topic ntfy global (NTFY_TOPIC), c'est-à-dire le
téléphone du propriétaire. chessia-admin ntfy-topic <login> [topic] corrige après coup.
Révoquer un accès : chessia-admin disable <login> (bloque les nouvelles connexions) puis
chessia-admin logout <login> (coupe les sessions déjà ouvertes). Voir
chessia/admin_cli.py pour l'ensemble des sous-commandes (list, passwd, enable).
Coach
Deuxième pipeline, indépendant du premier : au lieu d'analyser un PGN ponctuel apporté par
l'utilisateur, chessia-coach tourne en tâche de fond (cron), ingère automatiquement les
parties rapides jouées sur Chess.com, les fait analyser par Stockfish, puis produit un
rapport de coaching quotidien et cumulatif via un unique appel claude -p. Les pages sont
montées sous /coach/* dans le même serveur FastAPI (chessia.web:app).
Une base SQLite par joueur (schéma v8), sous CHESSIA_USERS_DIR/<login>.db — le pseudo
Chess.com vient du compte (chessia-admin add <login> <pseudo>), il n'y a plus de variable
d'environnement pour le fixer globalement. accounts.db (§ Comptes) est une base à part,
qui ne contient aucune donnée de partie.
chessia-coach run # ingestion + analyse + rapport, tous les comptes actifs
chessia-coach run --user bob # ne rejouer qu'un seul joueur
chessia-coach ingest --since <epoch> --user bob # étapes séparées, pour debug
chessia-coach analyze --user bob
chessia-coach report --user bob
Sans --user, les sous-commandes manuelles (ingest/analyze/report/condense/
backfill-openings) portent sur le premier compte actif — l'administrateur s'il y en a
un (active_users trie is_admin DESC, login ASC), sinon simplement le premier login actif
par ordre alphabétique ; run sans --user boucle en série sur tous les comptes actifs.
Pages /coach/*
| Route | Contenu |
|---|---|
/coach/ |
Rapport du jour (le plus récent), navigation vers /coach/report/<id> |
/coach/history |
Index chiffré de tous les rapports (date, parties, ΔElo, V/D/N) |
/coach/postit |
Mur de pense-bêtes : les recommandations du coach, par catégorie, cochables |
/coach/progress |
Série temporelle par jour (parties, victoires, Elo) |
/coach/consignes |
Contraintes actives transmises au coach, gérées ligne à ligne (ajout/édition/archivage) |
/coach/admin |
Suggestions des joueurs à traiter, puis consommation de tokens par joueur (24 h / 7 j / 30 j) — réservé à l'administrateur (require_admin) |
/feedback |
Formulaire de suggestion (hors préfixe /coach) : accessible depuis la pastille présente sur toute page passée par layout.page(), y compris /analyser |
Ces pages n'ont pas de contrôle d'accès propre : elles s'appuient entièrement sur la session
du middleware (chessia/authweb.py) — il n'y a plus de périmètre réseau (l'allowlist IP
nginx qui restreignait /coach au LAN/tailnet a été supprimée au déploiement). Chaque joueur
ne voit que sa propre base ; /coach/admin vérifie en plus is_admin.
Toute route d'écriture de ce routeur (et de chessia/feedback.py) porte la dépendance
verifier_origine (chessia/authweb.py, partagée par les deux routeurs), qui vérifie en
outre l'origine de la requête, car l'authentification par session ne protège pas de la
CSRF : c'est le navigateur légitime du joueur déjà connecté qui émettrait la requête à son
insu en visitant un site tiers, cookie de session compris. Le risque n'est pas réseau mais un
vecteur de prompt-injection : le texte des consignes est réinjecté verbatim dans le prompt du
run de 6 h, dans une section présentée au modèle comme des contraintes prioritaires — un site
tiers pourrait ainsi piloter le coach.
Sec-Fetch-Site fait foi (same-origin et none seuls acceptés), avec repli sur Origin
vs Host ; une requête sans aucun des deux en-têtes (curl, script) est acceptée, car un tel
client doit de toute façon déjà présenter un cookie de session valide. Un refus renvoie
403.
Suggestions des joueurs
Une pastille fixe en bas à droite, sur toute page passée par layout.page() (rapport,
historique, /analyser…), ouvre un petit formulaire (texte + envoi) sans quitter la page —
et fonctionne aussi sans JavaScript : la pastille est un vrai lien vers /feedback
(chessia/feedback.py), une page à formulaire plein qui porte le chemin d'origine en query.
Le message est stocké dans la table feedback d'accounts.db (auteur, date, page, texte,
statut), et déclenche une notification ntfy immédiate vers l'administrateur. Celui-ci les lit
et les marque traitées sur /coach/admin, dans une section dédiée au-dessus des tableaux de
consommation — ni la lecture ni le marquage ne sont accessibles à un compte non administrateur.
Citations de coups
Dans le markdown produit par le modèle, un coup ne s'écrit jamais en texte libre : il doit
utiliser le jeton [[m:<game_id>:<ply>]], où game_id (id complet ou ses 8 premiers
caractères) et ply sont recopiés tels quels depuis les données Stockfish fournies dans le
prompt. Le serveur (chessia/coach/render.py) résout chaque jeton en interrogeant la base
et affiche à la place un bloc repliable (<details class="cite">) avec le coup, sa
sévérité, le temps passé, un diagramme SVG (coup joué en rouge, meilleur coup Stockfish en
vert) et un lien vers la partie sur Chess.com. Un jeton dont l'id ou le ply ne correspond à
rien en base s'affiche « coup cité, partie inconnue » plutôt que de faire échouer le rendu.
Les rapports produits avant cette fonctionnalité ne contiennent pas de jetons : ils
continuent de s'afficher en texte brut, sans échiquier.
Recommandations : catégories et priorité
Chaque recommandation appartient à une catégorie parmi une liste fermée —
opening / middlegame / endgame / general — imposée au modèle dans le prompt système ;
toute autre valeur reçue est ramenée à general (normalize_category dans
chessia/coach/store.py). Le modèle peut aussi marquer une recommandation comme
prioritaire (priority: true) : ces recommandations s'affichent en tête du mur
/coach/postit, dans un bandeau à part, mais le serveur ne garde jamais plus de 3
recommandations dans ce bandeau, même si le modèle en marque davantage.
Le prompt du run quotidien transmet toutes les recommandations, cochées comprises, avec
leur status et leur checked_at : sans cela le modèle ne voit plus une recommandation
cochée, ne peut plus la référencer par son id, et la recrée à l'identique au run suivant.
Une recommandation cochée par le propriétaire n'est rouverte que si le modèle la
signale délibérément persistante (still_failing: true à côté de refers_to_reco_id) ;
une simple référence met seulement à jour la trace du dernier rapport et conserve
checked_at et times_repeated (_apply_recommendations dans chessia/coach/pipeline.py).
Consignes utilisateur
Plusieurs consignes actives coexistent : toutes sont transmises au coach à chaque run
de 6 h, sous forme de liste numérotée (queries.current_instructions), pas seulement la
dernière saisie. Elles sont injectées telles quelles dans le prompt
(chessia/coach/prompt.py::build_prompt), sous un en-tête qui précise au modèle qu'il
s'agit de contraintes primant sur ses habitudes de coaching, pas de données à interpréter.
/coach/consignes les gère ligne à ligne (chessia/coach/web.py) :
- Ajouter une nouvelle consigne (
POST /coach/consignes) ; - Modifier le texte en place d'une consigne active (
POST /coach/consignes/<id>/edit) — ce n'est pas un versionnement : le texte précédent n'est pas conservé ; - Archiver une consigne (
POST /coach/consignes/<id>/archive) : elle disparaît de la liste active et n'est donc plus transmise au coach, mais aucune suppression dure — elle reste consultable et réactivable dans la section repliable en bas de page ; - Réactiver une consigne archivée (
POST /coach/consignes/<id>/reactivate).
Chaque consigne porte un status (active / archived) et un updated_at. À
l'initialisation d'une base (ou lors de sa migration vers ce schéma), une consigne
inaugurale est semée automatiquement en active (volume minimal de parties à recommander
par jour).
Pipeline
Phase 1 — Stockfish scan
Analyse chaque demi-coup avec multipv=2 (détecte les coups brillants : meilleur coup dans une position où les candidats sont proches).
Phase 2 — Re-analyse des coups critiques
Pour chaque coup flaggé blunder / mistake / inaccuracy / missed_win, ré-évalue avec multipv=3 pour fournir les 3 candidats à Claude.
Phase 3 — Explications Claude
Pour chaque coup critique, prompt structuré en 5 sections :
- Diagnostic — nom précis du motif tactique/positionnel
- Analyse comparative des 3 candidats Stockfish
- Ligne principale détaillée sur 6 à 10 demi-coups avec éval
[+0.53],[+M3] - Raison profonde (principe échiquéen violé)
- Exercice d'entraînement ciblé
Le prompt est enrichi par phase (ouverture / milieu / finale) et inclut :
- Code ECO + nom d'ouverture (détection offline via
chessia/data/eco.json) - Features positionnelles calculées (pions passés/doublés/isolés, paire de fous, contrôle centre)
- Top 5 moments de tension de la partie
- 15 derniers demi-coups d'historique
Catégories de coups
| Catégorie | Seuil | NAG |
|---|---|---|
blunder |
≥ 200 cp de perte | ?? ($4) |
mistake |
≥ 100 cp | ? ($2) |
inaccuracy |
≥ 50 cp | ?! ($6) |
missed_win |
gain décisif lâché (+300 → <150) | !? ($5) |
brilliant |
meilleur coup dans position contestée (gap top1-top2 ≤ 60 cp) | !! ($3) |
Les seuils sont adaptatifs : ils montent avec la décisivité de la position via une sigmoid win_prob(cp) — moins de faux positifs sur positions déjà gagnées/perdues.
Rapport HTML
- Autonome : styles inlinés, échiquier interactif via CDN
lichess-pgn-viewer - Graphique d'éval SVG de la partie avec marqueurs colorés sur les coups notables
- Coups critiques repliables (
<details>natifs), sommaire sticky à gauche - Navigation clavier
↑ ↓/j kpour sauter de coup en coup - Mini-viewer Lichess par bloc critique avec la variante Stockfish (PV 14 demi-coups)
- Cases mentionnées par Claude mises en surbrillance sur le diagramme
- Export PDF via
window.print()(mise en page impression optimisée) - Light mode (toggle ☾/☀ avec persistance localStorage)
Architecture
chessia/
├── __main__.py # CLI entrypoint
├── web.py # Serveur FastAPI (historique, watch, notifs, regen)
├── authweb.py # Middleware de session, /login, /logout, /healthz, voir § Comptes
├── accounts.py # Comptes, sessions, comptage des tokens (accounts.db)
├── admin_cli.py # CLI chessia-admin (add | list | passwd | disable | enable | logout | ntfy-topic | migrate-legacy)
├── pipeline.py # Orchestration : engine + claude + rapport
├── engine.py # Wrapper Stockfish (python-chess UCI)
├── critical.py # Détection (blunder/mistake/inaccuracy/missed_win/brilliant)
├── explain.py # Appels Claude via subprocess claude -p
├── cache.py # Cache FS des explanations (sha256 fen|ply|model)
├── eco.py # Détection offline ECO + nom d'ouverture
├── position_features.py # Features positionnelles (pions, roi, centre, fous)
├── progress.py # ETA (EMA) + cancel/pause contextvars
├── report_html.py # Rendu rapport (graphique, TOC, mini-viewers, print CSS)
├── board_svg.py # Diagramme SVG avec highlighted squares
├── regen.py # Script CLI regen rapports (avec backup)
├── data/eco.json # Base ECO (3700 entrées, lichess-org/chess-openings, CC0)
├── coach/ # Pipeline coach (ingestion Chess.com + rapport quotidien), voir § Coach
│ ├── run.py # CLI chessia-coach (run | ingest | analyze | report | condense | backfill-openings), --user <login>
│ ├── store.py # Schéma SQLite + migrations + Store (une base par joueur)
│ ├── queries.py # Lecture seule pour les pages /coach
│ ├── prompt.py # System prompt + appel claude -p + parse JSON
│ ├── render.py # Markdown → HTML nettoyé + résolution des citations
│ ├── admin_view.py # Rendu de /coach/admin (consommation de tokens par joueur)
│ └── web.py # Routes FastAPI /coach/*
└── static/ # Design system partagé (styles.css + theme.js)
Voir CLAUDE.md pour les conventions de développement et les règles non-évidentes depuis le code.
Déploiement Docker
Image multi-stage (build Stockfish from source + Python + Node.js pour Claude Code CLI).
Build local
docker build -t chessia:dev .
Run
Cet exemple écoute en clair sur 8000, sans TLS devant. Le cookie de session est
Secure par défaut (chessia/authweb.py) : sur http://, le navigateur le jette
silencieusement et la connexion semble échouer sans aucun message. CHESSIA_COOKIE_INSECURE=1
(voir § Variables d'environnement) désactive ce flag pour ce cas précis — à retirer dès qu'un
reverse-proxy TLS est devant, ce qui est le cas en prod (voir Infra).
docker run -d \
--name chessia \
-p 8000:8000 \
-v chessia-reports:/data/reports \
-v chessia-cache:/data/cache \
-v chessia-coach:/data/coach \
-v chessia-claude:/home/chessia/.claude \
-e CLAUDE_CREDENTIALS_JSON="$(cat ~/.claude/.credentials.json)" \
-e CHESSIA_COOKIE_INSECURE=1 \
-e NTFY_SERVER=https://ntfy.exemple.com \
-e NTFY_TOPIC=chessia-diag \
-e NTFY_USER=chessia \
-e NTFY_PASSWORD=... \
git.greil.fr/chipster/chessia:latest
Il n'y a pas encore de compte à ce stade (§ Comptes utilisateurs) : les commandes
chessia-admin tournent dans le container, par exemple
docker exec chessia chessia-admin add timothth TimothTh --admin.
Volumes à persister
| Mount | Contenu |
|---|---|
/data/reports |
Rapports HTML + meta.json des diagnostics |
/data/cache |
Cache des explications Claude (évite les re-appels) |
/data/coach |
accounts.db (comptes, sessions, consommation de tokens) + une base de coaching par joueur sous u/<login>.db. Sans ce volume nommé, Docker crée un volume anonyme : ça fonctionne, mais rien ne garde le lien avec le container si on le recrée sans y penser — nommer le volume évite de perdre tous les comptes et tout l'historique de coaching par erreur |
/home/chessia/.claude |
OAuth refresh tokens Claude Code (mis à jour automatiquement par le CLI). Persister ce volume évite d'avoir à ré-injecter CLAUDE_CREDENTIALS_JSON à chaque redémarrage tant que le refresh token reste valide |
Secrets de run
CLAUDE_CREDENTIALS_JSON: contenu du fichier~/.claude/.credentials.jsonde l'utilisateur Claude Code Max. L'entrypoint l'écrit dans/home/chessia/.claude/.credentials.jsonau démarrage.NTFY_*: optionnel, pour les push mobiles (voir section Env ci-dessus).
Déploiement via tofu-maison — module non utilisé, non à jour
Ce module n'est pas le chemin de déploiement en production. La prod tourne via un
compose Docker + Ansible dans le dépôt d'infrastructure séparé (Infra), pas via ce module
OpenTofu. infra/module/ n'a pas été touché par le passage au multi-utilisateur (45 commits) :
il ne monte aucun volume nommé sur /data/coach. L'image déclare ce chemin en VOLUME
(Dockerfile), donc Docker y crée quand même un volume anonyme plutôt que d'écrire dans la
couche writable — accounts.db et les bases de coaching de tous les joueurs survivent tant
que le container vit, mais deviennent orphelins (plus aucun nom pour les retrouver) à la
prochaine recréation (nouvelle image, tofu apply, etc.). Son healthcheck sonde aussi
toujours / avec curl -fsS, qui ne considère qu'un code ≥ 400 comme un échec : / répond
désormais 302 (redirection vers /login) pour un visiteur sans session, et curl -fsS
accepte un 3xx — la sonde reste donc « healthy » même si tout ce qui est derrière le login
est cassé, un faux positif strictement pire qu'une sonde qui remonterait unhealthy. Ne pas
l'exécuter en le croyant à jour — voir l'en-tête du fichier.
Ce qui suit décrit le module tel qu'il existait avant le multi-utilisateur, conservé pour mémoire si quelqu'un le remet à niveau un jour.
Côté tofu-maison, dans docker/services.tf :
module "chessia" {
source = "git::ssh://git@git.greil.fr/chipster/chessia.git//infra/module?ref=main"
image = "git.greil.fr/chipster/chessia:latest"
external_port = 8000
claude_credentials_json = var.chessia_claude_credentials_json
ntfy_server = "https://ntfy.exemple.com"
ntfy_topic = "chessia-diag"
ntfy_user = "chessia"
ntfy_password = var.chessia_ntfy_password
}
Puis tofu -chdir=docker init && tofu -chdir=docker apply.
Un workflow .forgejo/workflows/deploy.yml dans ce repo trigger automatiquement un apply de tofu-maison quand :
- le module
infra/module/**change (push main) - le build Docker termine avec succès (nouveau tag à tirer)
- déclenché manuellement depuis l'UI Forgejo
Secret repo supplémentaire requis :
| Secret | Usage |
|---|---|
TOFU_MAISON_TOKEN |
Token Forgejo avec write:repository sur mat/tofu-maison (pour déclencher son workflow apply.yml) |
CI Forgejo Actions
Workflow dans .forgejo/workflows/docker-build.yml :
- Trigger : push sur
main, tagv*, ou déclenchement manuel - Build : image AMD64 (
linux/amd64) - Push :
git.greil.fr/chipster/chessiaavec tagssha-<short>,latest(main),<version>(tags semver) - Cache layers : stockées en registry (
:buildcache) - Runner : self-hosted
Secrets Forgejo à configurer dans les settings du repo :
| Secret | Usage |
|---|---|
REGISTRY_USERNAME |
User pour push vers le registry Forgejo |
REGISTRY_TOKEN |
Token d'accès avec scope write:package |
Licence
Code : usage personnel. data/eco.json dérivé de lichess-org/chess-openings sous CC0.