api : le tableau de bord sur données fictives - routes L1 à L4 (#94) #137

Merged
justine merged 12 commits from marvin/94-api-tableau-de-bord into develop 2026-09-04 13:33:54 +00:00
Member

Ce que ça change

L'API du tableau de bord servie sur le jeu de démonstration figé : référentiel des sites, vue Parc, vue Site (série, prévision, recommandations, traçabilité) et vue Qualité (disponibilité, journal de collecte, alertes). Les routes, les schémas de réponse, le gating par session et les codes d'erreur sont définitifs ; seule la source des valeurs est provisoire, et le jour où la zone or arrive, seuls les corps de dashboard/repository.py changent.

Ce lot était mêlé au front dans #127 ; il en est extrait pour être relu seul. Rien ne le consomme encore — le tableau de bord qui l'appelle est porté par #24 (PR séparées).

Closes #94

Preuve

211 tests pytest passés
ruff : All checks passed
mypy : Success, no issues found in 7 source files

Relecture

  • Un pair a relu et laissé un commentaire, même court
  • Ses remarques sont traitées, ou une réponse explique pourquoi elles ne le sont pas

Ce qui suit le code

(rien de coché : aucun runbook, ADR, incident ni variable d'environnement nouvelle de ce côté)

Où regarder en priorité

Le préfixe des routes a changé, et c'est le point à valider. Le routeur portait prefix="/api/v1". Or le Caddyfile relaie en handle_path /api/*, qui retire /api avant de transmettre : un appel navigateur /api/v1/sites arrivait à l'API en /v1/sites, que rien ne servait — 404 en production. Invisible en développement, où le proxy Vite conserve le préfixe.

Le routeur sert donc /v1, même convention que /auth et /health, déjà à la racine pour cette raison. Le préfixe /api redevient une affaire de Caddy seule. Le correctif côté front est dans la PR de l'écran Parc.

Autres points :

  • dashboard/autorisation.py rend encore les sept sites à tout utilisateur authentifié — la table ops.acces_site existe en base (migration 0015) mais rien ne la lit encore.
  • Un f sans interpolation que ruff refusait traînait dans les tests de traçabilité ; corrigé au passage, sinon la chaîne recalait la PR.
## Ce que ça change L'API du tableau de bord servie sur le jeu de démonstration figé : référentiel des sites, vue Parc, vue Site (série, prévision, recommandations, traçabilité) et vue Qualité (disponibilité, journal de collecte, alertes). Les routes, les schémas de réponse, le gating par session et les codes d'erreur sont définitifs ; seule la source des valeurs est provisoire, et le jour où la zone or arrive, seuls les corps de `dashboard/repository.py` changent. Ce lot était mêlé au front dans #127 ; il en est extrait pour être relu seul. **Rien ne le consomme encore** — le tableau de bord qui l'appelle est porté par #24 (PR séparées). Closes #94 ## Preuve ``` 211 tests pytest passés ruff : All checks passed mypy : Success, no issues found in 7 source files ``` ## Relecture - [ ] Un pair a relu et laissé un commentaire, même court - [ ] Ses remarques sont traitées, ou une réponse explique pourquoi elles ne le sont pas ## Ce qui suit le code (rien de coché : aucun runbook, ADR, incident ni variable d'environnement nouvelle de ce côté) ## Où regarder en priorité **Le préfixe des routes a changé, et c'est le point à valider.** Le routeur portait `prefix="/api/v1"`. Or le Caddyfile relaie en `handle_path /api/*`, qui **retire** `/api` avant de transmettre : un appel navigateur `/api/v1/sites` arrivait à l'API en `/v1/sites`, que rien ne servait — 404 en production. Invisible en développement, où le proxy Vite conserve le préfixe. Le routeur sert donc `/v1`, même convention que `/auth` et `/health`, déjà à la racine pour cette raison. Le préfixe `/api` redevient une affaire de Caddy seule. Le correctif côté front est dans la PR de l'écran Parc. Autres points : - `dashboard/autorisation.py` rend encore les sept sites à tout utilisateur authentifié — la table `ops.acces_site` existe en base (migration 0015) mais rien ne la lit encore. - Un `f` sans interpolation que ruff refusait traînait dans les tests de traçabilité ; corrigé au passage, sinon la chaîne recalait la PR.
Premier lot du ticket #94. La zone or n'existe pas encore : les routes du
tableau de bord sont servies par un jeu figé, repris des maquettes et observé à
un instant de référence gelé.

- GET /api/v1/sites et GET /api/v1/sites/{site_id}, session obligatoire
- un site inconnu et un site non autorisé rendent tous deux 404 : un 403
  confirmerait son existence
- dashboard/repository.py prend déjà une connexion en premier argument, comme
  auth/repository.py. Le passage à la vraie base ne changera que des corps de
  fonctions, ni les routeurs ni les schémas de réponse
- le filtrage par site (ENF-02) est isolé dans dashboard/autorisation.py, seul
  endroit à reprendre quand la table d'autorisation existera
- les fixtures ne sont pas dans data/, que .gitignore exclut

Les états des sites ne sont pas posés à la main : un test les recalcule depuis
leurs propres chiffres, et une valeur changée sans sa cohérence fait rougir la
chaîne.

Ruff, mypy strict et 95 tests passent. Couverture globale 86,75 % (seuil 70),
zones sensibles 90 % (seuil 85).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZSzujf2cJNhX7gSpqXrNk
Deuxième lot du ticket #94.

- GET /api/v1/parc/synthese : les cinq indicateurs de tête, tous dérivés des
  sites visibles plutôt que posés à la main
- GET /api/v1/parc/mesures?fenetre=24h|7j|30j : la courbe agrégée, le point de
  prévision H+1, la pointe, le creux et le compte de points imputés. Le pas
  découle de la fenêtre, une fenêtre inconnue rend 422
- le filtrage par autorisation passe désormais par un seul helper du routeur,
  les agrégats du parc le respectent comme la liste des sites

Le jeu gagne un profil horaire de parc, les trois alertes ouvertes de la
maquette et l'instant estimé du dépassement de Couëron. Les alertes portent
l'origine « systeme » : nous les produisons, elles ne viennent pas du flux amont
— la maquette les annonçait à tort comme ingérées de la source (EF-05).

La courbe et l'indicateur de tête ne peuvent plus diverger : le profil à 16 h
vaut la consommation instantanée, et un test le vérifie.

Deux écarts avec la maquette, tranchés en faveur du calcul :
- disponibilité moyenne à 98,5 % et non 98,6 % — c'est la moyenne des sept
  sites que la maquette affiche elle-même juste en dessous
- « 1 critique » devient « 1 alerte », pour tenir un seul vocabulaire de
  sévérité entre les sites et les alertes

Ruff, mypy strict et 134 tests passent. Couverture globale 88,91 % (seuil 70),
zones sensibles 90 % (seuil 85).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZSzujf2cJNhX7gSpqXrNk
Le jeu était calé sur les maquettes : sept sites de l'agglomération nantaise,
capacités et vocabulaire inventés. docs/GLOSSAIRE.md est la source unique de
ces valeurs, et la PR #106 vient de poser le schéma réel. Tout divergeait.

Référentiel repris du glossaire : SITE001 à SITE007, leurs libellés, leurs
types en anglais et leurs capacités — 4 130 kW au total, contre 6 200 inventés.
Le schéma suit la migration 0007_zone_or_mesure.sql, d'où « site_id » et
« libelle » plutôt que « id » et « nom ».

L'état d'un site n'est plus une règle maison. C'est le taux de charge et les
quatre paliers du glossaire (70 / 85 / 95 %, bornes incluses à gauche). Mes
seuils SEUIL_IMPUTATION_PCT et RETARD_RELEVE_MAX_S disparaissent : ils n'avaient
aucune source, et le glossaire dit qu'un seuil en dur qui n'y figure pas est un
défaut.

La consommation devient nullable, et un site l'exerce : SITE006 a le capteur
muet, donc pas de taux ni de palier. Jamais zéro — un site muet n'est pas un
site au repos. Les agrégats du parc l'excluent et le disent : sites_mesures 6
sur sites_total 7.

Énumérations alignées : types de site en anglais (glossaire §1), sévérités
low/medium/high/critical (§4), types d'alerte threshold/spike/anomaly/outage/
sensor (§4), méthodes d'imputation interpolated/forward_fill/none (ADR 0006,
trois régimes), qualité good/imputed/suspect/critical (migration 0007).

La synthèse expose désormais deux grandeurs de charge, parce qu'elles ne se
confondent pas : consommation_kw est une somme, taux_de_charge_moyen_pct une
moyenne — la seule grandeur comparable entre sites.

Un test vérifie qu'une alerte de seuil porte exactement la sévérité du palier
de son site, un autre que le référentiel n'a pas dérivé du glossaire.

Reste sans source : disponibilite_cible_pct à 99 %, marqué d'un NOTE. À faire
ajouter au glossaire ou à retirer.

Ruff, mypy strict et 159 tests passent. Couverture globale 89,56 % (seuil 70),
zones sensibles 90 % (seuil 85).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZSzujf2cJNhX7gSpqXrNk
GET /api/v1/sites/{id}/mesures — un appel par site affiché, comme l'annote
la maquette #24. Chaque site est mis à l'échelle du profil du parc plutôt
que de porter sa propre courbe inventée (aucune donnée historique par site
n'existe encore dans le jeu de démonstration).

Un site sans consommation courante (capteur muet, SITE006) lève
SourceIndisponible côté dépôt, traduite en 503 côté route : le front garde
les autres sites affichés, comme l'état « source indisponible » de la
maquette.

Pas de reference_kw par site : la référence de comparaison (EF-07) n'est
publiée qu'au parc, en inventer une par site afficherait un nombre que
personne n'a calculé.

19 tests neufs, couverture dashboard/ à 100 %.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnUAobdPeQynk8EnCeMLdW
L3 de #94 : les trois routes qui manquaient à la vue Site.

- SerieSite.reference_kw : la référence du parc (EF-07, méthode de
  persistance) proratisée à l'échelle du site — la même technique, déjà
  revue, qui construit sa série depuis le profil du parc. Pas un chiffre
  inventé.
- GET /sites/{id}/recommandations : fixtures, comme les prévisions — #94
  met explicitement le moteur de règles hors périmètre, pas la route.
  Liste vide si aucune n'a été émise, ce n'est pas une erreur.
- GET /sites/{id}/tracabilite?horodatage=... : rejoue le générateur de
  points de /mesures, donc ne peut jamais diverger de ce que la courbe
  affiche au même instant. 404 sur un instant hors fenêtre.

_site_visible_ou_404 factorisée : la même vérification était dupliquée
trois fois avant cette série de routes, quatre après.

19 tests neufs, couverture dashboard/ toujours à 100 %.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L4 de #94 — dernier lot de routes de l'écran Qualité :

- GET /qualite/synthese : disponibilité moyenne et sa cible, imputation
  moyenne, sites sous la cible.
- GET /qualite/collecte?site_id=... : trois jours de journal par site
  (migration 0013_zone_or_qualite_jour.sql). Le jour de référence reprend
  exactement disponibilite_pct/imputation_pct de la fiche du site — pas de
  contradiction entre les deux, même principe que la courbe qui rejoint la
  consommation instantanée.
- GET /alertes : ouvertes et résolues, la plus récente d'abord — jusqu'ici
  seules les ouvertes étaient exposées (synthese_parc). alertes_ouvertes()
  devient un sous-ensemble d'alertes(), le filtre n'est plus dupliqué.

Une quatrième alerte fixture (résolue, alr-0004) : sans elle, EtatAlerte.
RESOLUE n'était exercé nulle part dans le jeu. Trois tests existants
ajustés en conséquence (ils verrouillaient « trois alertes, toutes
ouvertes »).

Avec ce lot, #94 (API + fixtures) couvre ses quatre lots (L1 à L4). Reste
sa dernière case : le README des routes.

19 tests neufs (dont 3 ajustés), couverture dashboard/ toujours à 100 %.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
api : le tableau de bord sert /v1, le préfixe /api revient à Caddy
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 27s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 3s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m29s
27ce5f1266
Le routeur portait « /api/v1 ». Or le Caddyfile relaie en
« handle_path /api/* », qui RETIRE « /api » avant de transmettre : un appel
navigateur « /api/v1/sites » arrivait à l'API en « /v1/sites », que rien ne
servait — 404 en production.

Invisible en développement, où le proxy Vite relaie « /api » sans le retirer :
l'API y recevait bien « /api/v1/sites ». Dev et prod divergeaient en silence,
et les tests, qui tapent l'application directement, ne voyaient ni l'un ni
l'autre.

Le préfixe appartient donc à Caddy, pas à l'API — même convention que « /auth »
et « /health », déjà à la racine pour cette raison. Le front vise
VITE_API_BASE=…/api en regard (corrigé côté tableau de bord).

Au passage, un « f » sans interpolation que ruff refusait dans les tests de
traçabilité.

Refs #94, #24

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6BwMkMyRBYJJYaHheLwGY
marvin changed title from api : le tableau de bord sur donn�es fictives � routes L1 � L4 (#94) to api : le tableau de bord sur données fictives — routes L1 à L4 (#94) 2026-09-04 08:04:02 +00:00
marvin self-assigned this 2026-09-04 08:35:42 +00:00
Member

Très bon travail mais il y a 2 points à revoir (et un autre mineur).

Dans les critères du #94 il est demandé que "Le README de l'API liste les routes et explique comment lancer l'API sur le jeu de démonstration." or plusieurs routes ne sont manquantes (il n'y en a que 4/10 on dirait qu'il y a celles du Lot 2 mais pas le reste)

Deuxième point : tu as fix côté entrant mais pas sortant, FastAPI émet aussi des URLs qui pointent vers lui-même. Il les construit en préfixant par root_path (fastapi/applications.py:1124). Donc sans réglage, root_path vaut "", et ça va casser la doc car https://app.g2.enervision/api/docs -> Swagger va demander https://app.g2.enervision/openapi.json sans api/

Dernier point (qui est probablement plus une incohérence dans le ticket qu'autre chose) : bin/api / bin/api.ps1 n'existe pas alors qu'il est mentionné dans "comment on vérifie" et "à savoir" du #94

Très bon travail mais il y a 2 points à revoir (et un autre mineur). Dans les critères du #94 il est demandé que "Le README de l'API liste les routes et explique comment lancer l'API sur le jeu de démonstration." or plusieurs routes ne sont manquantes (il n'y en a que 4/10 on dirait qu'il y a celles du Lot 2 mais pas le reste) Deuxième point : tu as fix côté entrant mais pas sortant, FastAPI émet aussi des URLs qui pointent vers lui-même. Il les construit en préfixant par root_path (fastapi/applications.py:1124). Donc sans réglage, root_path vaut "", et ça va casser la doc car https://app.g2.enervision/api/docs -> Swagger va demander https://app.g2.enervision/openapi.json sans api/ Dernier point (qui est probablement plus une incohérence dans le ticket qu'autre chose) : bin/api / bin/api.ps1 n'existe pas alors qu'il est mentionné dans "comment on vérifie" et "à savoir" du #94
api : root_path, lanceur bin/api et README complet (relecture #137)
Some checks failed
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 3s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m34s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m4s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Failing after 5m15s
c98ac01594
Trois points relevés en relecture de la PR #137.

root_path. Le correctif du préfixe ne portait que sur l'entrée : Caddy retire
« /api » (handle_path), les routeurs sont donc à la racine. Mais FastAPI émet
aussi des URL vers lui-même, préfixées par root_path — dont le /openapi.json
que charge la page /docs. Sans réglage, .../api/docs demandait
.../openapi.json et n'affichait rien. Nouveau réglage ENERVISION_API_ROOT_PATH,
vide par défaut, « /api » en production ; le Caddyfile et le runbook, qui
parlaient de « --root-path » en ligne de commande, sont alignés dessus.

bin/api et bin/api.ps1. Le ticket #94 les cite dans « comment on vérifie » et
« à savoir », ils n'existaient pas. Une seule sonde Python partagée
(bin/_sonde_api.py) : configuration illisible, base joignable ou non. Sans
base l'API démarre quand même — les migrations de démarrage sont coupées —
et le script dit que seul /health répondra.

README. Le tableau des routes du tableau de bord s'arrêtait au lot L2 : 4 des
10 routes. Les six autres sont ajoutées, groupées par écran, et la mise en
route se fait désormais sur le jeu de démonstration via bin/api.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017t9ACoY2UR7Hv6JieW1Uqh
Author
Member

Merci - les trois points sont traités dans c98ac01.

Routes. Le bon compte : le tableau s'arrêtait au lot L2. Les six manquantes sont ajoutées, groupées par écran, avec paramètres et codes d'erreur - plus la mise en route sur le jeu de démonstration, l'autre moitié du critère.

root_path. Corrigé, mais par un réglage plutôt que par --root-path en ligne de commande : ENERVISION_API_ROOT_PATH, vide par défaut, /api en production. Trois tests dans test_app_root_path.py, dont un qui vérifie que /health répond toujours tel quel : root_path corrige la sortie, pas l'entrée. Le Caddyfile et le runbook réclamaient déjà --root-path /api « côté pile API (#94) » ; leurs commentaires sont réalignés dessus.

bin/api. Plutôt un manque qu'une incohérence du ticket : le lanceur n'avait jamais été écrit. Il existe, avec bin/api.ps1, au-dessus d'une sonde partagée - sans base il coupe les migrations et lance quand même, en disant que seul /health répondra.

Merci - les trois points sont traités dans c98ac01. **Routes.** Le bon compte : le tableau s'arrêtait au lot L2. Les six manquantes sont ajoutées, groupées par écran, avec paramètres et codes d'erreur - plus la mise en route sur le jeu de démonstration, l'autre moitié du critère. **`root_path`.** Corrigé, mais par un réglage plutôt que par `--root-path` en ligne de commande : `ENERVISION_API_ROOT_PATH`, vide par défaut, `/api` en production. Trois tests dans `test_app_root_path.py`, dont un qui vérifie que `/health` répond toujours tel quel : root_path corrige la sortie, pas l'entrée. Le `Caddyfile` et le runbook réclamaient déjà `--root-path /api` « côté pile API (#94) » ; leurs commentaires sont réalignés dessus. **`bin/api`.** Plutôt un manque qu'une incohérence du ticket : le lanceur n'avait jamais été écrit. Il existe, avec `bin/api.ps1`, au-dessus d'une sonde partagée - sans base il coupe les migrations et lance quand même, en disant que seul `/health` répondra.
marvin changed title from api : le tableau de bord sur données fictives — routes L1 à L4 (#94) to api : le tableau de bord sur données fictives - routes L1 à L4 (#94) 2026-09-04 09:47:00 +00:00
Merge branch 'develop' into marvin/94-api-tableau-de-bord
Some checks failed
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 4s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Python — qualité, tests et dépendances (pull_request) Failing after 2m18s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Failing after 7m14s
317ad14fc3
Member

@marvin Ok pour moi quand tes tests passeront

@marvin Ok pour moi quand tes tests passeront
Merge branch 'develop' into marvin/94-api-tableau-de-bord
Some checks failed
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Aucun secret commité (pull_request) Successful in 4s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 20s
Intégration / Python — qualité, tests et dépendances (pull_request) Failing after 2m28s
e595684de0
tests: le jeu d'essai de zone argent quitte conftest pour un module nommé
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 5s
Intégration / Aucun secret commité (pull_request) Successful in 4s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 19s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m4s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m21s
f288c87a2b
UN CONFTEST NE S'IMPORTE PAS. Sans `__init__.py`, pytest donne à chaque
`conftest.py` le même nom de module de premier niveau — `conftest` — et un
seul fichier peut l'occuper. `tests/unit/gold/test_fabrique_argent.py`
faisait pourtant `from conftest import ...` en visant `tests/conftest.py` :
depuis que la #94 a ajouté `tests/unit/api/conftest.py`, c'est celui-là que
l'import trouvait, et ses deux cas tombaient en `ImportError`.

Aucune des deux branches n'était fautive seule — develop verte à la #290, la
#94 verte à la #279 — c'est leur réunion qui rougissait, aux exécutions #288
et #293. Un conflit qu'aucun des deux côtés ne pouvait voir chez lui.

Le référentiel et les familles de grandeurs vivent donc dans
`tests/jeu_essai_argent.py`, un module dont le nom ne peut désigner qu'un
fichier. `conftest.py` les importe pour ses fabriques : il fournit des
fixtures, il ne s'importe plus. Le reste du jeu d'essai, que personne ne
relit par son nom, reste où il est.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA13mGth6mJq7yTvgtfuLG
Merge branch 'develop' into marvin/94-api-tableau-de-bord
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Aucun secret commité (pull_request) Successful in 4s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 18s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m20s
ee0adb20a1
justine merged commit 109f0a5171 into develop 2026-09-04 13:33:54 +00:00
justine deleted branch marvin/94-api-tableau-de-bord 2026-09-04 13:33:54 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision!137
No description provided.