[EF-10] Le tableau de bord lit la zone or : cinq lectures quittent les fixtures (#179) #182

Merged
lenaic merged 8 commits from marvin/179-tableau-de-bord-zone-or into develop 2026-09-08 10:02:28 +00:00
Member

Ce que ça change

Cinq des huit lectures du tableau de bord quittent fixtures.py et lisent PostgreSQL : les alertes, la traçabilité d'une valeur, le journal de collecte, et les deux courbes. La zone or est peuplée depuis le 07/09, l'écran Qualité affichait encore quatre alertes inventées alors que la base en porte 1 742.

Les trois qui restent — sites, synthese_parc, recommandations_site — portent la prévision et les recommandations, dont les tables sont vides. Elles suivront le #37 et le #39.

Closes #179

Preuve

$ pytest tests/unit/api -q
210 passed

$ pytest tests/unit -q
914 passed, 1 skipped

$ npm run test:unit          # tableau de bord
373 passed

$ ruff check services packages && ruff format --check services packages
All checks passed!

$ mypy --config-file enervision_api/pyproject.toml enervision_api    # strict
Success: no issues found in 22 source files

Ce que la base porte réellement, en préproduction, et que ces lectures servent maintenant :

public.mesure          358 687      public.alerte        1 742
public.mesure_horaire    5 985      public.qualite_jour      7

Trois commits, à relire dans l'ordre

ded567e les fondations : l'énumération à quatre valeurs, la connexion factice qui répond
5d58dd8 les cinq lectures branchées, et leurs cas réécrits
b6ce34a la garde côté front sur la prévision absente

Où regarder en priorité

1. Methode passe de trois à quatre valeurs. C'est le correctif qui bloquait tout le reste. La forme réduite faisait porter à none deux sens distincts — « valeur mesurée » et « trou assumé » — alors que l'ADR 0006 nomme measured le régime de la valeur présente. La contrainte de public.mesure accepte les quatre depuis la 0010, et 89,5 % des lignes réelles portent measured : sans ce correctif, l'endpoint de traçabilité rendait 500 sur la grande majorité des points, pas sur un cas limite.

2. La prévision devient facultative dans le contrat, et c'est une décision. public.prevision est vide : le modèle H+1 est entraîné et promu (#36), rien ne sert encore ses valeurs (#37). L'API rend null, l'écran affiche « — » et la raison. Un chiffre dérivé d'autre chose serait invérifiable, et la première question du jury serait d'où il sort. ecart_reference_pct suit : sans les deux termes, il n'y a pas d'écart, et rendre zéro laisserait croire que la prévision colle à la référence.

3. reference_kw est calculée, pas inventée. C'est la persistance — la valeur observée à la même heure la veille — que le schéma de sortie nomme ainsi depuis le début. Elle se déduit de mesure_horaire seule, sans attendre le service d'inférence. Un historique trop court rend zéro : la courbe reste affichable, seul le repère manque.

4. Les courbes lisent mesure_horaire, jamais une moyenne recalculée — le §12 pose la règle, « les recalculer donnerait deux vérités pour une question ». Mais l'agrégat continu ne porte pas l'imputation, or le front trace ces segments autrement (EF-04). D'où une jointure sur public.mesure qui ne sert qu'à ce drapeau : la valeur affichée reste celle de l'agrégat. C'est le point que je relirais en premier si j'étais relecteur.

5. Le filtre par site est dans la requête SQL, pas appliqué après coup : c'est le contrôle d'accès à la ressource de l'ENF-02. Le faire en Python ramènerait d'abord en mémoire des lignes que l'appelant n'a pas le droit de voir.

Comment les tests tiennent sans PostgreSQL

FakeConn script ses réponses, appariées par FRAGMENT de requête et non par égalité — écrire le SQL complet dans chaque cas en ferait la copie d'un fichier de production que personne ne relirait, et au premier order by déplacé tous les cas rougiraient sans qu'aucun comportement n'ait changé.

Le docstring dit ce qu'elle ne garantit PAS : ce n'est pas un simulateur de PostgreSQL. L'exactitude du SQL relève de l'intégration, la projection des lignes en modèles se prouve ici. Confondre les deux donnerait un faux qui ment sur ce qu'il garantit.

Son constructeur reste sans paramètre, et c'est écrit en commentaire : conftest passe la classe à app.dependency_overrides[get_db], et FastAPI lit la signature de __init__ comme celle d'une dépendance. Un argument de configuration y devient un champ de corps de requête, et toute route POST rend 422 sur un corps valide. Constaté, 84 tests en erreur.

Un faux positif que le branchement a révélé

Le cas test_repartition_methode_couvre_les_releves_attendus affirmait « somme(répartition) + manquants == attendus ». Vrai du jeu figé, faux de la donnée réelle : la zone or calcule les manquants comme « attendus − mesurées − imputées », et la répartition compte les quatre régimes sur TOUTE la série — les additionner comptait none deux fois.

Le vrai invariant est « somme == attendus » et « répartition['none'] == manquants ». Vérifié sur SITE006 le 2026-09-06 : 1317 + 122 + 1 = 1440, manquants 1. C'est exactement le genre de faux positif qu'un jeu figé entretient sans qu'on le sache.

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 que ça change Cinq des huit lectures du tableau de bord quittent `fixtures.py` et lisent PostgreSQL : les alertes, la traçabilité d'une valeur, le journal de collecte, et les deux courbes. La zone or est peuplée depuis le 07/09, l'écran Qualité affichait encore quatre alertes inventées alors que la base en porte 1 742. Les trois qui restent — `sites`, `synthese_parc`, `recommandations_site` — portent la prévision et les recommandations, dont les tables sont vides. Elles suivront le #37 et le #39. Closes #179 ## Preuve ``` $ pytest tests/unit/api -q 210 passed $ pytest tests/unit -q 914 passed, 1 skipped $ npm run test:unit # tableau de bord 373 passed $ ruff check services packages && ruff format --check services packages All checks passed! $ mypy --config-file enervision_api/pyproject.toml enervision_api # strict Success: no issues found in 22 source files ``` Ce que la base porte réellement, en préproduction, et que ces lectures servent maintenant : ``` public.mesure 358 687 public.alerte 1 742 public.mesure_horaire 5 985 public.qualite_jour 7 ``` ## Trois commits, à relire dans l'ordre | | | |---|---| | `ded567e` | les fondations : l'énumération à quatre valeurs, la connexion factice qui répond | | `5d58dd8` | les cinq lectures branchées, et leurs cas réécrits | | `b6ce34a` | la garde côté front sur la prévision absente | ## Où regarder en priorité **1. `Methode` passe de trois à quatre valeurs.** C'est le correctif qui bloquait tout le reste. La forme réduite faisait porter à `none` deux sens distincts — « valeur mesurée » et « trou assumé » — alors que l'ADR 0006 nomme `measured` le régime de la valeur présente. La contrainte de `public.mesure` accepte les quatre depuis la 0010, et **89,5 % des lignes réelles portent `measured`** : sans ce correctif, l'endpoint de traçabilité rendait 500 sur la grande majorité des points, pas sur un cas limite. **2. La prévision devient facultative dans le contrat, et c'est une décision.** `public.prevision` est vide : le modèle H+1 est entraîné et promu (#36), rien ne sert encore ses valeurs (#37). L'API rend `null`, l'écran affiche « — » et la raison. Un chiffre dérivé d'autre chose serait invérifiable, et la première question du jury serait d'où il sort. `ecart_reference_pct` suit : sans les deux termes, il n'y a pas d'écart, et rendre zéro laisserait croire que la prévision colle à la référence. **3. `reference_kw` est calculée, pas inventée.** C'est la persistance — la valeur observée à la même heure la veille — que le schéma de sortie nomme ainsi depuis le début. Elle se déduit de `mesure_horaire` seule, sans attendre le service d'inférence. Un historique trop court rend zéro : la courbe reste affichable, seul le repère manque. **4. Les courbes lisent `mesure_horaire`, jamais une moyenne recalculée** — le §12 pose la règle, « les recalculer donnerait deux vérités pour une question ». Mais l'agrégat continu ne porte pas l'imputation, or le front trace ces segments autrement (EF-04). D'où une jointure sur `public.mesure` **qui ne sert qu'à ce drapeau** : la valeur affichée reste celle de l'agrégat. C'est le point que je relirais en premier si j'étais relecteur. **5. Le filtre par site est dans la requête SQL**, pas appliqué après coup : c'est le contrôle d'accès à la ressource de l'ENF-02. Le faire en Python ramènerait d'abord en mémoire des lignes que l'appelant n'a pas le droit de voir. ## Comment les tests tiennent sans PostgreSQL `FakeConn` script ses réponses, appariées par FRAGMENT de requête et non par égalité — écrire le SQL complet dans chaque cas en ferait la copie d'un fichier de production que personne ne relirait, et au premier `order by` déplacé tous les cas rougiraient sans qu'aucun comportement n'ait changé. Le docstring dit ce qu'elle ne garantit PAS : ce n'est pas un simulateur de PostgreSQL. L'exactitude du SQL relève de l'intégration, la projection des lignes en modèles se prouve ici. Confondre les deux donnerait un faux qui ment sur ce qu'il garantit. Son constructeur reste **sans paramètre**, et c'est écrit en commentaire : `conftest` passe la classe à `app.dependency_overrides[get_db]`, et FastAPI lit la signature de `__init__` comme celle d'une dépendance. Un argument de configuration y devient un champ de corps de requête, et toute route POST rend 422 sur un corps valide. Constaté, 84 tests en erreur. ## Un faux positif que le branchement a révélé Le cas `test_repartition_methode_couvre_les_releves_attendus` affirmait « somme(répartition) + manquants == attendus ». Vrai du jeu figé, **faux de la donnée réelle** : la zone or calcule les manquants comme « attendus − mesurées − imputées », et la répartition compte les quatre régimes sur TOUTE la série — les additionner comptait `none` deux fois. Le vrai invariant est « somme == attendus » **et** « répartition['none'] == manquants ». Vérifié sur SITE006 le 2026-09-06 : 1317 + 122 + 1 = 1440, manquants 1. C'est exactement le genre de faux positif qu'un jeu figé entretient sans qu'on le sache. ## 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
marvin self-assigned this 2026-09-08 08:13:12 +00:00
Deux fondations, sans lesquelles le branchement du tableau de bord sur la zone
or ne tient pas.

`Methode` passe de trois à QUATRE valeurs. La forme réduite faisait porter à
`none` deux sens distincts — « valeur mesurée » et « trou assumé » — alors que
l'ADR 0006 et le §9 du document data nomment `measured` le régime de la valeur
présente et valide. La contrainte de `public.mesure` accepte les quatre depuis
la migration 0010, et la zone argent les écrit : sur une journée réelle,
**89,5 % des lignes portent `measured`**. Tant que le dépôt servait des
fixtures, l'énumération n'a jamais vu de ligne réelle ; le jour où `repository`
lit la base, une valeur hors énumération fait échouer la validation Pydantic et
l'endpoint rend 500 — sur la grande majorité des lignes, pas sur un cas limite.

`FakeConn` sait désormais répondre à des requêtes. Elle n'avait que `commit` et
`rollback`, ce qui suffisait tant que `repository` ignorait son paramètre `conn`.
Les réponses sont appariées par FRAGMENT de requête et non par égalité : écrire
le SQL complet dans chaque cas en ferait la copie d'un fichier de production que
personne ne relirait, et au premier `order by` déplacé tous les cas rougiraient
sans qu'aucun comportement n'ait changé.

Ce n'est pas un simulateur de PostgreSQL et le docstring le dit : l'exactitude
du SQL se prouve en intégration, la projection des lignes en modèles se prouve
en unitaire. Confondre les deux donnerait un faux qui ment sur ce qu'il garantit.

SON CONSTRUCTEUR RESTE SANS PARAMÈTRE, et c'est écrit en commentaire. `conftest`
passe la classe à `app.dependency_overrides[get_db]`, et FastAPI lit la
signature de `__init__` comme celle d'une dépendance : un argument de
configuration y devient un champ de corps de requête, et toute route POST rend
422 sur un corps parfaitement valide. Constaté, 84 tests en erreur. L'instance
est donc partagée par une fixture et scriptée après coup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
`alertes`, `tracabilite_site`, `journal_collecte_site`, `serie_site` et
`serie_parc` lisent PostgreSQL. Les trois qui restent — `sites`,
`synthese_parc`, `recommandations_site` — portent la prévision et les
recommandations, dont les tables sont vides : elles suivront le #37 et le #39.

LE FILTRE PAR SITE EST DANS LA REQUÊTE, pas appliqué après coup. C'est le
contrôle d'accès à la ressource de l'ENF-02 : le faire en Python ramènerait
d'abord en mémoire des lignes que l'appelant n'a pas le droit de voir.

Les valeurs des courbes viennent de `mesure_horaire`, l'agrégat continu de la
migration 0016, et JAMAIS d'une moyenne recalculée ici — le §12 du document data
pose la règle, « les recalculer donnerait deux vérités pour une question ».
Mais l'agrégat ne porte pas l'imputation, or le front trace ces segments
autrement (EF-04, « la méthode reste lisible à côté de la valeur ») : d'où une
jointure sur `public.mesure` qui ne sert QU'À CE DRAPEAU.

`reference_kw` est la PERSISTANCE — la valeur observée à la même heure la
veille. Ce n'est pas le modèle du #36, c'est le repère naïf contre lequel il se
mesure, et c'est ce que le schéma de sortie nomme depuis le début. Elle se
calcule depuis `mesure_horaire` seule, sans attendre le service d'inférence. Un
historique trop court rend zéro plutôt qu'une erreur : la courbe reste
affichable, seul le repère manque.

`prevision` devient FACULTATIVE dans le contrat. `public.prevision` est vide :
le modèle H+1 est entraîné et promu (#36), rien ne sert encore ses valeurs
(#37). Un champ absent dit « pas encore de modèle » ; un chiffre dérivé d'autre
chose serait invérifiable, et la première question du jury serait d'où il sort.
`ecart_reference_pct` suit : sans les deux termes il n'y a pas d'écart, et
rendre zéro laisserait croire que la prévision colle à la référence.

Deux absences ont deux sens, et les confondre coûterait cher en exploitation.
En traçabilité : aucune ligne rend 404 (« cet instant n'est pas dans la
fenêtre »), une ligne sans valeur retenue rend 503 (« la minute existe, rien n'y
a été mesuré » — le trou assumé du §9). En série : un site sans point rend 503
et non une courbe vide, qui se lirait « ce site ne consomme rien » alors qu'on
ne sait pas.

UN FAUX POSITIF DÉCOUVERT EN CHEMIN. Le cas
`test_repartition_methode_couvre_les_releves_attendus` affirmait
« somme(répartition) + manquants == attendus ». C'était vrai du jeu figé, FAUX
de la donnée réelle : la zone or calcule les manquants comme
« attendus − mesurées − imputées », et la répartition compte les quatre régimes
sur TOUTE la série — les additionner comptait `none` deux fois. Le vrai
invariant est « somme == attendus » ET « répartition['none'] == manquants »,
vérifié sur SITE006 le 2026-09-06 : 1317 + 122 + 1 = 1440, manquants 1.

Et un piège de projection qu'un cas garde nommément : `qualite_jour.
taux_disponibilite` est stocké en FRACTION — 0,9993 — pour la même raison que le
taux de charge dans la 0012. Oublier le facteur cent aurait affiché « 1,0 % de
disponibilité » sur une journée parfaite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
dashboard : l'écran Site dit qu'il n'a pas de prévision (#179)
Some checks failed
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 8s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 22s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 37s
Intégration / Python — qualité, tests et dépendances (pull_request) Failing after 1m16s
b6ce34a462
`store.serie.prevision.valeur_kw` déréférençait sans garde un champ que l'API
rend désormais `null` tant que le service d'inférence ne sert pas les valeurs du
modèle promu (#36, #37).

L'écran affiche « — » et la raison, plutôt que de planter ou d'afficher un
chiffre dont personne ne pourrait dire d'où il sort. `ecart_reference_pct` est
gardé de la même façon : sans prévision, il n'y a pas d'écart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
marvin changed title from [EF-10] Le tableau de bord lit la zone or : cinq lectures quittent les fixtures (#179) to WIP: [EF-10] Le tableau de bord lit la zone or : cinq lectures quittent les fixtures (#179) 2026-09-08 08:16:08 +00:00
api: ruff format sur le dictionnaire des fenêtres (#179)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 38s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 17s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m14s
9cb280d33d
`_FENETRES` tenait sur deux lignes là où le formateur en veut cinq. Sans
conséquence sur le comportement, mais `ruff format --check services packages`
est une tâche BLOQUANTE de la chaîne : elle aurait arrêté la demande de fusion.

La faute est d'avoir relancé `ruff check` après les dernières modifications sans
relancer `ruff format`. Les deux sont bloquants, ils se vérifient ensemble.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
marvin changed title from WIP: [EF-10] Le tableau de bord lit la zone or : cinq lectures quittent les fixtures (#179) to [EF-10] Le tableau de bord lit la zone or : cinq lectures quittent les fixtures (#179) 2026-09-08 08:28:11 +00:00
marvin requested review from olivier 2026-09-08 08:28:20 +00:00
Member

Relu à J-1, donc uniquement ce qui bloque ou ce qui se verra à la démonstration. Le reste — l'énumération à quatre valeurs, la prévision facultative assumée jusqu'au front, la traçabilité lue à la clé (site_id, horodatage), le journal de collecte, le faux positif corrigé sur la répartition — tient, et les justifications sont au bon endroit.

Un bloquant, deux points critiques.

1. Bloquant — /v1/parc/mesures n'agrège pas les sites

Les deux requêtes de la courbe rendent une courbe fausse dès qu'il y a plus d'un site, c'est-à-dire dans le cas nominal (sept sites visibles, _sites_visibles les rend tous).

Fenêtre 24 h. mesure_horaire porte une ligne par (site_id, heure) (migration 0016, group by site_id, heure). SQL_SERIE_HEURE regroupe sur group by h.heure, h.moyenne_kw : le couple est distinct par site, donc chaque heure ressort autant de fois qu'il y a de sites, chacune à l'échelle d'un site.

Reproduit sur trois sites et deux heures :

group by h.heure, h.moyenne_kw          une courbe de parc
  ('12h', 101.0)                          ('12h', 603.0)
  ('12h', 201.0)                          ('13h', 606.0)
  ('12h', 301.0)
  ('13h', 102.0)
  ('13h', 202.0)
  ('13h', 302.0)

ProfilHoraireParc.vue trace store.profilParc.points en abscisse par index, une étiquette toutes les quatre heures : il recevra 168 points au lieu de 24, chaque heure répétée sept fois. Le critère « profil horaire » du #24 devient une dent de scie.

Fenêtres 7 j et 30 j. SQL_SERIE_JOUR fait avg(h.moyenne_kw) sur toutes les lignes de tous les sites : le résultat est la consommation moyenne d'un site, pas celle du parc. À côté, capacite_totale_kw est une somme et SQL_REFERENCE fait sum(moyenne_kw) — la courbe et ses deux repères ne sont pas à la même échelle, d'un facteur voisin du nombre de sites. Le cas test_la_courbe_reste_sous_la_capacite_du_parc, supprimé par la branche, gardait exactement cet invariant.

Le correctif est le même des deux côtés : sommer sur les sites avant de regrouper. Pour l'heure, sum(h.moyenne_kw) avec group by h.heure seul. Pour le jour, agréger en deux temps — la somme du parc par heure, puis la moyenne de ces heures sur la journée.

Pourquoi rien ne l'a vu : FakeConn scripte les réponses, ce qui est le bon choix et c'est bien argumenté, mais son docstring renvoie à tests/integration/ pour l'exactitude du SQL — or aucun test d'intégration n'exécute ces cinq requêtes. tests/integration/gold/test_chargement_postgres.py est un banc de temps de réponse, et sa SQL_PARC_HORAIRE ne passe pas par le dépôt. La promesse du docstring n'est pas tenue aujourd'hui ; un seul cas d'intégration sur serie_parc avec deux sites suffirait à fermer la classe entière.

2. Critique — le front ignore measured, et garde l'ancien sens de none

services/dashboard/src/api/glossaire.js porte trois entrées :

none: { libelle: 'mesure directe', description: 'mesure directe, aucune reconstitution' },
interpolated: ...
forward_fill: ...

La branche fait exactement l'inverse en base : measured est la valeur mesurée, none est le trou assumé. Donc, à l'écran :

  • journal de collecte — libelleMethode retombe sur la clé brute : « 1317 en measured », en anglais, à côté de « 1 en mesure directe » pour le trou. Le contresens est affiché sur l'écran Qualité, celui de la qualité de données ;
  • traçabilité (EF-04) — descriptionMethode rend « measured » tel quel pour 89,5 % des points.

Deux lignes dans glossaire.js. Tant qu'on y est, le docstring de QualiteJour (models.py:398) dit encore « NONE couvrant les mesures directes » — c'est la phrase que le reste de la branche corrige.

3. Critique — /v1/alertes et la synthèse du parc chargent les 1 742 lignes

SQL_ALERTES n'a ni limite ni filtre sur l'état. alertes_ouvertes appelle alertes() puis filtre en Python, et synthese_parc s'en sert pour un compte et une ventilation par sévérité : l'écran d'accueil ramène donc 1 742 lignes pour en afficher un entier. ListeAlertes.vue les rend toutes, sans pagination — et comme la 0018 pose etat not null default 'ouverte' et que rien ne résout d'alerte, les 1 742 sont ouvertes.

Le pavé du parc mérite un count(*) ... group by severite, et la liste une borne (ou le filtre where etat = 'ouverte', que l'index alerte_ouvertes de la 0018 attend déjà).

Ce que je note sans bloquer

  • La jointure sur public.mesure multiplie les lignes par 60 avant l'agrégat : sur 30 j × 7 sites, ~300 000 lignes jointes par appel. Conséquence de fond dans SQL_SERIE_JOURavg(h.moyenne_kw) porte sur les lignes jointes, donc chaque heure y pèse son nombre de minutes présentes. Une sous-requête agrégée par heure, jointe ensuite, évite les deux.
  • Le drapeau imputee risque de saturer : la note de fin de la 0010 recense 347 574 lignes historiques portant none avec une valeur non nulle, non normalisées. bool_or(methode_imputation <> 'measured') les compte toutes comme imputées. À regarder sur la préproduction avant de conclure que la courbe dit vrai.
  • Deux descriptions OpenAPI ont vieilli dans le même commit : /alertes promet toujours qu'« une alerte sans site_id sort toujours » (le filtre est maintenant dans le SQL, et la colonne est not null), et /qualite/collecte annonce « trois derniers jours » alors que SQL_JOURNAL_COLLECTE n'a plus de limite.
  • _POINT_ABSENT : le commentaire dit « il faut que l'appel échoue plutôt que de rendre un objet à moitié faux », le code rend un point à l'epoch. L'un des deux a raison.
  • test_dashboard_parc.py a été reformaté à 88 colonnes, hors du périmètre ruff format services packages annoncé dans la description — d'où des retours à la ligne qui n'apportent rien au diff.

Le point 1 est le seul qui m'empêche d'approuver : c'est l'écran d'accueil, et la courbe est fausse dans les trois fenêtres. Les points 2 et 3 sont courts et se voient en démonstration.

Relu à J-1, donc uniquement ce qui bloque ou ce qui se verra à la démonstration. Le reste — l'énumération à quatre valeurs, la prévision facultative assumée jusqu'au front, la traçabilité lue à la clé `(site_id, horodatage)`, le journal de collecte, le faux positif corrigé sur la répartition — tient, et les justifications sont au bon endroit. **Un bloquant, deux points critiques.** ## 1. Bloquant — `/v1/parc/mesures` n'agrège pas les sites Les deux requêtes de la courbe rendent une courbe fausse dès qu'il y a plus d'un site, c'est-à-dire dans le cas nominal (sept sites visibles, `_sites_visibles` les rend tous). **Fenêtre 24 h.** `mesure_horaire` porte une ligne par `(site_id, heure)` (migration 0016, `group by site_id, heure`). `SQL_SERIE_HEURE` regroupe sur `group by h.heure, h.moyenne_kw` : le couple est distinct par site, donc chaque heure ressort autant de fois qu'il y a de sites, chacune à l'échelle d'**un** site. Reproduit sur trois sites et deux heures : ``` group by h.heure, h.moyenne_kw une courbe de parc ('12h', 101.0) ('12h', 603.0) ('12h', 201.0) ('13h', 606.0) ('12h', 301.0) ('13h', 102.0) ('13h', 202.0) ('13h', 302.0) ``` `ProfilHoraireParc.vue` trace `store.profilParc.points` en abscisse par index, une étiquette toutes les quatre heures : il recevra 168 points au lieu de 24, chaque heure répétée sept fois. Le critère « profil horaire » du #24 devient une dent de scie. **Fenêtres 7 j et 30 j.** `SQL_SERIE_JOUR` fait `avg(h.moyenne_kw)` sur toutes les lignes de tous les sites : le résultat est la consommation **moyenne d'un site**, pas celle du parc. À côté, `capacite_totale_kw` est une somme et `SQL_REFERENCE` fait `sum(moyenne_kw)` — la courbe et ses deux repères ne sont pas à la même échelle, d'un facteur voisin du nombre de sites. Le cas `test_la_courbe_reste_sous_la_capacite_du_parc`, supprimé par la branche, gardait exactement cet invariant. Le correctif est le même des deux côtés : sommer sur les sites avant de regrouper. Pour l'heure, `sum(h.moyenne_kw)` avec `group by h.heure` seul. Pour le jour, agréger en deux temps — la somme du parc par heure, puis la moyenne de ces heures sur la journée. Pourquoi rien ne l'a vu : `FakeConn` scripte les réponses, ce qui est le bon choix et c'est bien argumenté, mais son docstring renvoie à `tests/integration/` pour l'exactitude du SQL — or aucun test d'intégration n'exécute ces cinq requêtes. `tests/integration/gold/test_chargement_postgres.py` est un banc de temps de réponse, et sa `SQL_PARC_HORAIRE` ne passe pas par le dépôt. La promesse du docstring n'est pas tenue aujourd'hui ; un seul cas d'intégration sur `serie_parc` avec deux sites suffirait à fermer la classe entière. ## 2. Critique — le front ignore `measured`, et garde l'ancien sens de `none` `services/dashboard/src/api/glossaire.js` porte trois entrées : ```js none: { libelle: 'mesure directe', description: 'mesure directe, aucune reconstitution' }, interpolated: ... forward_fill: ... ``` La branche fait exactement l'inverse en base : `measured` est la valeur mesurée, `none` est le trou assumé. Donc, à l'écran : - journal de collecte — `libelleMethode` retombe sur la clé brute : « 1317 en **measured** », en anglais, à côté de « 1 en **mesure directe** » pour le trou. Le contresens est affiché sur l'écran Qualité, celui de la qualité de données ; - traçabilité (EF-04) — `descriptionMethode` rend « measured » tel quel pour 89,5 % des points. Deux lignes dans `glossaire.js`. Tant qu'on y est, le docstring de `QualiteJour` (`models.py:398`) dit encore « `NONE` couvrant les mesures directes » — c'est la phrase que le reste de la branche corrige. ## 3. Critique — `/v1/alertes` et la synthèse du parc chargent les 1 742 lignes `SQL_ALERTES` n'a ni limite ni filtre sur l'état. `alertes_ouvertes` appelle `alertes()` puis filtre en Python, et `synthese_parc` s'en sert pour un compte et une ventilation par sévérité : l'écran d'accueil ramène donc 1 742 lignes pour en afficher un entier. `ListeAlertes.vue` les rend toutes, sans pagination — et comme la 0018 pose `etat not null default 'ouverte'` et que rien ne résout d'alerte, les 1 742 sont ouvertes. Le pavé du parc mérite un `count(*) ... group by severite`, et la liste une borne (ou le filtre `where etat = 'ouverte'`, que l'index `alerte_ouvertes` de la 0018 attend déjà). ## Ce que je note sans bloquer - La jointure sur `public.mesure` multiplie les lignes par 60 avant l'agrégat : sur 30 j × 7 sites, ~300 000 lignes jointes par appel. Conséquence de fond dans `SQL_SERIE_JOUR` — `avg(h.moyenne_kw)` porte sur les lignes **jointes**, donc chaque heure y pèse son nombre de minutes présentes. Une sous-requête agrégée par heure, jointe ensuite, évite les deux. - Le drapeau `imputee` risque de saturer : la note de fin de la 0010 recense 347 574 lignes historiques portant `none` avec une valeur non nulle, non normalisées. `bool_or(methode_imputation <> 'measured')` les compte toutes comme imputées. À regarder sur la préproduction avant de conclure que la courbe dit vrai. - Deux descriptions OpenAPI ont vieilli dans le même commit : `/alertes` promet toujours qu'« une alerte sans `site_id` sort toujours » (le filtre est maintenant dans le SQL, et la colonne est `not null`), et `/qualite/collecte` annonce « trois derniers jours » alors que `SQL_JOURNAL_COLLECTE` n'a plus de limite. - `_POINT_ABSENT` : le commentaire dit « il faut que l'appel échoue plutôt que de rendre un objet à moitié faux », le code rend un point à l'epoch. L'un des deux a raison. - `test_dashboard_parc.py` a été reformaté à 88 colonnes, hors du périmètre `ruff format services packages` annoncé dans la description — d'où des retours à la ligne qui n'apportent rien au diff. Le point 1 est le seul qui m'empêche d'approuver : c'est l'écran d'accueil, et la courbe est fausse dans les trois fenêtres. Les points 2 et 3 sont courts et se voient en démonstration.
api: la courbe du parc somme les sites — relecture de la #182
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Failing after 36s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 19s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m34s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m26s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
40a68f23d5
Trois défauts relevés en relecture, dont un bloquant, plus le cas d'intégration
qui manquait pour que la classe entière ne puisse plus repasser.

1. BLOQUANT — /v1/parc/mesures n'agrégeait pas les sites.

`mesure_horaire` porte une ligne par (site_id, heure). Un
`group by heure, moyenne_kw` ne regroupe donc RIEN : le couple est distinct par
site, et la courbe du parc sortait avec autant de points par heure qu'il y a de
sites, chacun à l'échelle d'un seul. Vérifié sur la préproduction, fenêtre 24 h,
sept sites : 28 points au lieu de 4, chaque heure répétée sept fois.

La fenêtre journalière était fausse autrement : `avg` sur toutes les lignes de
tous les sites rendait la consommation d'un site, quand `capacite_totale_kw` et
la référence sont des sommes. La courbe et ses deux repères n'étaient pas à la
même échelle, d'un facteur voisin du nombre de sites.

Les deux requêtes agrègent maintenant en deux temps : somme sur les sites, puis
regroupement. Et la jointure d'imputation sort de l'agrégat — jointe, elle
multipliait chaque ligne horaire par ses soixante minutes AVANT le regroupement,
ce qui pondérait chaque heure par son nombre de minutes présentes et faisait
300 000 lignes jointes par appel sur 30 jours.

2. Le front ignorait `measured` et gardait l'ancien sens de `none`.

`glossaire.js` traduisait `none` par « mesure directe ». La base dit l'inverse
depuis la 0010. L'écran Qualité — celui qui parle de la qualité de la donnée —
affichait donc « 1317 en measured », en anglais, à côté de « 1 en mesure
directe » pour le trou. Les deux entrées sont posées, et le docstring de
`QualiteJour` qui portait encore l'ancien sens est corrigé.

3. /v1/alertes ramenait 1 742 lignes pour en afficher un compte.

La liste est bornée à 200, et la ventilation du pavé du parc passe par un
`count(*) ... group by severite` au lieu de filtrer la liste en Python. C'est
l'index partiel `alerte_ouvertes` de la 0018 qui sert, posé exactement pour ça.

LE CAS D'INTÉGRATION QUI MANQUAIT. Le docstring de `FakeConn` renvoyait à
`tests/integration/` pour l'exactitude du SQL, et aucun test d'intégration
n'exécutait ces requêtes : la promesse n'était tenue par personne, et c'est
pourquoi rien n'a vu le défaut — un double rend ce qu'on lui donne.
`tests/integration/test_lectures_tableau_de_bord.py` pose deux sites sur deux
heures avec des valeurs qui distinguent d'un coup d'oeil une somme (300, 330),
une moyenne (150, 165) et une absence de regroupement (quatre points). Cinq cas,
joués contre la préproduction.

CE QUE LA VÉRIFICATION A RÉVÉLÉ EN PLUS. Le drapeau d'imputation disait
`<> 'measured'` ; la préproduction porte **347 574 lignes `none` avec une
valeur**, antérieures à la normalisation de la 0010 et jamais reprises. Sur 24 h
le drapeau restait juste, sur 30 jours chaque journée serait ressortie imputée.
Le prédicat nomme désormais les deux régimes d'imputation — ce qui est aussi
plus juste : un trou n'est pas une reconstitution.

Deux descriptions OpenAPI avaient vieilli dans le même commit, le commentaire de
`_POINT_ABSENT` contredisait son code — c'était le commentaire qui avait tort —
et trois réenroulements à 88 colonnes, hors du périmètre annoncé, sont annulés.

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

Les trois points sont traités, et le bloquant est vérifié sur la préproduction

Commit 40a68f2. La relecture avait raison sur les trois, et le point 1 était bien un défaut de fond — pas une approximation.

1. La courbe du parc n'agrégeait pas les sites

Diagnostic exact. mesure_horaire porte une ligne par (site_id, heure), et group by heure, moyenne_kw ne regroupe rien puisque le couple est distinct par site.

Mesuré sur la préproduction, fenêtre 24 h, sept sites visibles :

ancienne requete :   28 points        nouvelle requete :    4 points
  2026-09-07 10:00   489.2              2026-09-07 10:00  3565.8
  2026-09-07 10:00   906.1              2026-09-07 11:00  3619.3
  2026-09-07 10:00   167.7              2026-09-07 12:00  3592.9

Sept points par heure, chacun à l'échelle d'un site. Sur ProfilHoraireParc.vue, qui trace en abscisse par index, c'était bien la dent de scie annoncée.

Les deux requêtes agrègent maintenant en deux temps : somme sur les sites, puis regroupement — et pour la fenêtre journalière, somme du parc par heure puis moyenne de ces heures.

La remarque sur la jointure était juste aussi, et elle allait plus loin qu'une question de volume. Jointe dans l'agrégat, elle multipliait chaque ligne horaire par ses soixante minutes avant le regroupement : l'avg journalier pondérait donc chaque heure par son nombre de minutes présentes. La sous-requête agrégée à part corrige la justesse en même temps que les 300 000 lignes.

Le cas d'intégration qui manquait

Vous avez mis le doigt sur la vraie cause : le docstring de FakeConn renvoyait à tests/integration/ pour l'exactitude du SQL, et rien ne l'y exécutait. La promesse n'était tenue par personne, et aucun double ne pouvait voir ce défaut — il rend ce qu'on lui donne.

tests/integration/test_lectures_tableau_de_bord.py : deux sites, deux heures, des valeurs choisies pour que les trois cas se distinguent d'un coup d'oeil — une somme correcte donne 300 et 330, une moyenne donnerait 150 et 165, une absence de regroupement rendrait quatre points.

$ ENERVISION_ETL_DATABASE_URL='...enervision_preprod' \
    pytest tests/integration/test_lectures_tableau_de_bord.py -q
5 passed in 5.86s

2. Le front ignorait measured

Corrigé dans glossaire.js, avec les deux entrées : measured pour la valeur mesurée, none pour le trou assumé. Et le docstring de QualiteJour qui portait encore l'ancien sens — merci, c'était exactement la phrase que le reste de la branche corrigeait.

3. Les 1 742 lignes chargées pour un compte

La liste est bornée à 200, et la ventilation du pavé passe par un count(*) ... group by severite. C'est bien l'index partiel alerte_ouvertes de la 0018 qui sert, posé pour ça.

Une borne plutôt qu'une pagination : la pagination est un contrat — curseur, total, « page suivante » côté front — et l'écran n'en a pas besoin pour montrer ce qui vient d'arriver.

Ce que la vérification a révélé en plus

Votre note sur la saturation du drapeau n'était pas seulement un risque, c'est un défaut sur la fenêtre 30 j. La préproduction porte 347 574 lignes none avec une valeur, antérieures à la normalisation de la 0010 :

none            349463   dont  347574 avec valeur
measured          4674   interpolated  4505   forward_fill  45

Sur 24 h le drapeau restait juste — 1 385 mesurées, 556 interpolées — mais sur 30 jours chaque journée serait ressortie imputée. Le prédicat nomme désormais les deux régimes d'imputation au lieu de dire « tout sauf mesuré », ce qui est aussi plus juste sémantiquement : un trou n'est pas une reconstitution, il n'a pas de valeur retenue et ne pèse sur aucun agrégat.

Les autres remarques

Les deux descriptions OpenAPI sont réécrites. Le commentaire de _POINT_ABSENT contredisait son code — c'était le commentaire qui avait tort, une courbe de parc vide reste affichable, seuls ses deux repères n'ont pas de sens. Les trois réenroulements à 88 colonnes hors périmètre sont annulés.

915 unitaires · 5 d'intégration · ruff check et ruff format · mypy strict

### Les trois points sont traités, et le bloquant est vérifié sur la préproduction Commit `40a68f2`. La relecture avait raison sur les trois, et le point 1 était bien un défaut de fond — pas une approximation. ### 1. La courbe du parc n'agrégeait pas les sites Diagnostic exact. `mesure_horaire` porte une ligne par `(site_id, heure)`, et `group by heure, moyenne_kw` ne regroupe rien puisque le couple est distinct par site. Mesuré sur la préproduction, fenêtre 24 h, sept sites visibles : ``` ancienne requete : 28 points nouvelle requete : 4 points 2026-09-07 10:00 489.2 2026-09-07 10:00 3565.8 2026-09-07 10:00 906.1 2026-09-07 11:00 3619.3 2026-09-07 10:00 167.7 2026-09-07 12:00 3592.9 ``` Sept points par heure, chacun à l'échelle d'un site. Sur `ProfilHoraireParc.vue`, qui trace en abscisse par index, c'était bien la dent de scie annoncée. Les deux requêtes agrègent maintenant en deux temps : somme sur les sites, puis regroupement — et pour la fenêtre journalière, somme du parc par heure puis moyenne de ces heures. **La remarque sur la jointure était juste aussi, et elle allait plus loin qu'une question de volume.** Jointe dans l'agrégat, elle multipliait chaque ligne horaire par ses soixante minutes **avant** le regroupement : l'`avg` journalier pondérait donc chaque heure par son nombre de minutes présentes. La sous-requête agrégée à part corrige la justesse en même temps que les 300 000 lignes. ### Le cas d'intégration qui manquait Vous avez mis le doigt sur la vraie cause : le docstring de `FakeConn` renvoyait à `tests/integration/` pour l'exactitude du SQL, et rien ne l'y exécutait. La promesse n'était tenue par personne, et aucun double ne pouvait voir ce défaut — il rend ce qu'on lui donne. `tests/integration/test_lectures_tableau_de_bord.py` : deux sites, deux heures, des valeurs choisies pour que les trois cas se distinguent d'un coup d'oeil — une somme correcte donne 300 et 330, une moyenne donnerait 150 et 165, une absence de regroupement rendrait quatre points. ``` $ ENERVISION_ETL_DATABASE_URL='...enervision_preprod' \ pytest tests/integration/test_lectures_tableau_de_bord.py -q 5 passed in 5.86s ``` ### 2. Le front ignorait `measured` Corrigé dans `glossaire.js`, avec les deux entrées : `measured` pour la valeur mesurée, `none` pour le trou assumé. Et le docstring de `QualiteJour` qui portait encore l'ancien sens — merci, c'était exactement la phrase que le reste de la branche corrigeait. ### 3. Les 1 742 lignes chargées pour un compte La liste est bornée à 200, et la ventilation du pavé passe par un `count(*) ... group by severite`. C'est bien l'index partiel `alerte_ouvertes` de la 0018 qui sert, posé pour ça. Une borne plutôt qu'une pagination : la pagination est un contrat — curseur, total, « page suivante » côté front — et l'écran n'en a pas besoin pour montrer ce qui vient d'arriver. ### Ce que la vérification a révélé en plus **Votre note sur la saturation du drapeau n'était pas seulement un risque, c'est un défaut sur la fenêtre 30 j.** La préproduction porte 347 574 lignes `none` **avec une valeur**, antérieures à la normalisation de la 0010 : ``` none 349463 dont 347574 avec valeur measured 4674 interpolated 4505 forward_fill 45 ``` Sur 24 h le drapeau restait juste — 1 385 mesurées, 556 interpolées — mais sur 30 jours chaque journée serait ressortie imputée. Le prédicat nomme désormais les deux régimes d'imputation au lieu de dire « tout sauf mesuré », ce qui est aussi plus juste sémantiquement : un trou n'est pas une reconstitution, il n'a pas de valeur retenue et ne pèse sur aucun agrégat. ### Les autres remarques Les deux descriptions OpenAPI sont réécrites. Le commentaire de `_POINT_ABSENT` contredisait son code — c'était le commentaire qui avait tort, une courbe de parc vide reste affichable, seuls ses deux repères n'ont pas de sens. Les trois réenroulements à 88 colonnes hors périmètre sont annulés. `915 unitaires · 5 d'intégration · ruff check et ruff format · mypy strict`
Member

Les trois corrections sont bonnes. Je ne fusionne pas encore : le job « Tableau de bord » est rouge, et pour une raison qui tient au point 2.

Ce que j'ai vérifié de la correction

Le bloquant est traité au bon endroit. Les deux requêtes agrègent en deux temps, la sous-requête d'imputation est sortie de l'agrégat — ce qui corrige la pondération de l'avg journalier en même temps que le volume — et le prédicat qui nomme interpolated et forward_fill au lieu de « tout sauf mesuré » est plus juste que ce que j'avais suggéré : un trou n'est pas une reconstitution. Le cas d'intégration choisit ses valeurs pour que somme, moyenne et absence de regroupement se distinguent d'un coup d'œil, et il nettoie derrière lui.

La ventilation par count(*) ... group by severite fait mieux que corriger le volume : alertes_ouvertes devient une propriété dérivée de la carte, donc l'invariant « total == somme de la ventilation » ne peut plus se défaire.

Ce qui reste — npm run test:unit, quatre cas

Le changement de glossaire.js n'a pas été porté dans les cas qui l'exercent. Reproduit en local :

FAIL tests/unit/glossaire.test.js > libelleMethode
     expected 'trou assumé' to be 'mesure directe'
FAIL tests/unit/glossaire.test.js > descriptionMethode
     expected 'au-delà des bornes d'imputation, …' to be 'mesure directe, aucune reconstitution'
FAIL tests/unit/TracabiliteDetail.test.js > affiche la valeur retenue, la valeur brute, la méthode et la qualité
     expected 'Traçabilité…' to contain 'mesure directe'
FAIL tests/unit/JournalCollecte.test.js > ventile les relevés par méthode, la plus nombreuse d abord
     expected [ '1050 en trou assumé', … ] to deeply equal [ '1050 en mesure directe', … ]

Tests  5 failed | 327 passed (332)

(Le cinquième, paquet-de-production, échoue aussi sur 9cb280d dans mon environnement — il n'est pas de votre fait.)

Deux de ces quatre ne sont pas un renommage de chaîne. TracabiliteDetail.test.js monte une trace methode: 'none' avec valeur_retenue_kw: 104 et valeur_brute_kw: 104, et site.test.js:106 fait de même. C'est un cas que l'API ne peut plus produire : none signifie qu'aucune valeur n'a été retenue, et tracabilite_site lève SourceIndisponible — 503 — dès que valeur_kw est nul. Ces deux jeux d'essai décrivent donc une ligne qui n'existe pas. Ils voulaient dire « mesure directe » : c'est measured qu'ils doivent porter, et le cas retrouve alors son libellé sans qu'on touche à l'assertion. Même chose pour la fixture du journal, dont les 1 050 relevés sont des mesures.

« Contrôles statiques » : rien à corriger

Il était rouge au premier passage, sur un contrôle sans rapport avec la branche — listen_ip=127.0.0.1 de cadvisor, dans infra/compose/supervision/docker-compose.yml. J'ai rejoué tests/ci/test-supervision.sh sur le merge exact que la chaîne a testé (94e8f9b + 40a68f2) : 42 contrôles, aucun échec. J'ai relancé le job, il est vert. Un aléa de l'exécuteur, pas un défaut de la branche — je le signale pour que personne n'aille chercher.

Une remarque, non bloquante

Le plafond de 200 et le compte en base ne parlent plus de la même chose. Le pavé du parc affiche le total des ouvertes — 1 742 aujourd'hui — pendant que ListeAlertes.vue écrit « 200 ouvertes sur 200 », store.alertes.length étant désormais borné. Deux écrans qui se contredisent devant le jury. Une mention du genre « 200 plus récentes » dans l'en-tête de la liste suffirait.

Corrigez les quatre cas et je fusionne dès que la chaîne est verte.

Les trois corrections sont bonnes. Je ne fusionne pas encore : **le job « Tableau de bord » est rouge**, et pour une raison qui tient au point 2. ## Ce que j'ai vérifié de la correction Le bloquant est traité au bon endroit. Les deux requêtes agrègent en deux temps, la sous-requête d'imputation est sortie de l'agrégat — ce qui corrige la pondération de l'`avg` journalier en même temps que le volume — et le prédicat qui nomme `interpolated` et `forward_fill` au lieu de « tout sauf mesuré » est plus juste que ce que j'avais suggéré : un trou n'est pas une reconstitution. Le cas d'intégration choisit ses valeurs pour que somme, moyenne et absence de regroupement se distinguent d'un coup d'œil, et il nettoie derrière lui. La ventilation par `count(*) ... group by severite` fait mieux que corriger le volume : `alertes_ouvertes` devient une propriété dérivée de la carte, donc l'invariant « total == somme de la ventilation » ne peut plus se défaire. ## Ce qui reste — `npm run test:unit`, quatre cas Le changement de `glossaire.js` n'a pas été porté dans les cas qui l'exercent. Reproduit en local : ``` FAIL tests/unit/glossaire.test.js > libelleMethode expected 'trou assumé' to be 'mesure directe' FAIL tests/unit/glossaire.test.js > descriptionMethode expected 'au-delà des bornes d'imputation, …' to be 'mesure directe, aucune reconstitution' FAIL tests/unit/TracabiliteDetail.test.js > affiche la valeur retenue, la valeur brute, la méthode et la qualité expected 'Traçabilité…' to contain 'mesure directe' FAIL tests/unit/JournalCollecte.test.js > ventile les relevés par méthode, la plus nombreuse d abord expected [ '1050 en trou assumé', … ] to deeply equal [ '1050 en mesure directe', … ] Tests 5 failed | 327 passed (332) ``` (Le cinquième, `paquet-de-production`, échoue aussi sur `9cb280d` dans mon environnement — il n'est pas de votre fait.) **Deux de ces quatre ne sont pas un renommage de chaîne.** `TracabiliteDetail.test.js` monte une trace `methode: 'none'` **avec** `valeur_retenue_kw: 104` et `valeur_brute_kw: 104`, et `site.test.js:106` fait de même. C'est un cas que l'API ne peut plus produire : `none` signifie qu'aucune valeur n'a été retenue, et `tracabilite_site` lève `SourceIndisponible` — 503 — dès que `valeur_kw` est nul. Ces deux jeux d'essai décrivent donc une ligne qui n'existe pas. Ils voulaient dire « mesure directe » : c'est `measured` qu'ils doivent porter, et le cas retrouve alors son libellé sans qu'on touche à l'assertion. Même chose pour la fixture du journal, dont les 1 050 relevés sont des mesures. ## « Contrôles statiques » : rien à corriger Il était rouge au premier passage, sur un contrôle sans rapport avec la branche — `listen_ip=127.0.0.1` de cadvisor, dans `infra/compose/supervision/docker-compose.yml`. J'ai rejoué `tests/ci/test-supervision.sh` sur le merge exact que la chaîne a testé (`94e8f9b` + `40a68f2`) : 42 contrôles, aucun échec. J'ai relancé le job, il est vert. Un aléa de l'exécuteur, pas un défaut de la branche — je le signale pour que personne n'aille chercher. ## Une remarque, non bloquante Le plafond de 200 et le compte en base ne parlent plus de la même chose. Le pavé du parc affiche le total des ouvertes — 1 742 aujourd'hui — pendant que `ListeAlertes.vue` écrit « 200 ouvertes sur 200 », `store.alertes.length` étant désormais borné. Deux écrans qui se contredisent devant le jury. Une mention du genre « 200 plus récentes » dans l'en-tête de la liste suffirait. Corrigez les quatre cas et je fusionne dès que la chaîne est verte.
dashboard : les quatre régimes, jusque dans les jeux d'essai (#179)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 47s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m5s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m40s
32097f3661
La chaîne a rougi sur quatre cas du tableau de bord : `glossaire.js` a gagné
`measured` et rendu à `none` son sens de trou assumé, mais les cas qui
l'exercent affirmaient encore l'ancien. Faute de ma part — j'ai modifié le
glossaire sans rejouer la suite du tableau de bord derrière.

DEUX JEUX D'ESSAI SE CONTREDISAIENT, et c'est plus intéressant qu'un simple
renommage.

`JournalCollecte` posait `repartition_methode: { none: 1050, interpolated: 147 }`
avec `releves_manquants: 243` et `releves_attendus: 1440`. Sous le sens de la
migration 0010, `none` est le trou assumé : la somme aurait dû faire les
attendus, et `none` valoir les manquants. La forme juste est donc
`{ measured: 1050, interpolated: 147, none: 243 }` — 1050 + 147 + 243 = 1440,
et `none` = 243 = les manquants. C'est exactement l'invariant relevé sur
SITE006 en production, qui est justement le site de ce jeu.

`TracabiliteDetail` posait `methode: 'none'` avec `valeur_brute_kw: 104`. Un
trou assumé n'a par définition aucune valeur : la fixture décrivait une ligne
qui ne peut pas exister. C'est `measured`.

Les deux ne « passaient » que parce que le front traduisait `none` par « mesure
directe ». Corriger le glossaire les a mis à nu, ce qui est le comportement
attendu d'un jeu d'essai qui dit la vérité de la donnée.

`descriptionMethode('measured')` garde la formulation que l'équipe avait
écrite — « mesure directe, aucune reconstitution » — déplacée sur la bonne clé
plutôt que réécrite : elle s'accorde avec le libellé court.

373 cas du tableau de bord, 915 unitaires Python, ruff check et format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fusionne develop : la prévision du #37 rencontre les lectures de la zone or
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 22s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 40s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m42s
6f43828ecc
La #185 a livré le service de prévision et rempli `public.prevision`, la table
vide sur laquelle cette branche avait fondé sa décision de rendre le champ
facultatif. Huit fichiers en conflit, tous ceux que les deux lots touchaient.

CONVERGENCE, PAS DÉSACCORD. Justine a rendu `prevision`, `prevision_kw` et
`reference_kw` facultatifs de son côté, exactement comme ici — deux fois la même
décision, prise séparément. Le conflit était donc mécanique sur le contrat.

CE QUE SA VERSION APPORTE ET QUE JE PRENDS. `reference_kw` vient désormais de
`public.prevision` : le job la publie AVEC la prévision qu'elle sert à juger
(persistance naïve, EF-07). Mon `SQL_REFERENCE` la recalculait depuis
`mesure_horaire`, faute de table remplie — c'était honnête tant qu'aucune
n'existait, ça donnerait maintenant un second chiffre pour la même grandeur, et
les deux divergeraient à la première correction du job. La requête et sa
fonction sont supprimées, ainsi que les deux cas qui les gardaient.

Son `_ecart_pct` factorisé remplace mes deux propriétés dupliquées, et sa
formulation de l'écran — « la passe horaire n'a rien écrit pour ce site » — est
plus juste que la mienne, qui disait « pas encore servi » alors que le service
existe depuis sa demande. Son `!= null` attrape aussi `undefined`.

CE QUE CETTE BRANCHE GARDE. Les points des deux courbes viennent de la base et
non des fixtures, avec l'agrégation en deux temps et la sous-requête
d'imputation. `points_imputes` se compte sur les points rendus plutôt que sur
`fixtures.SITE_HEURES_IMPUTEES`. `pointe` et `creux` gardent leur garde de
séquence vide.

LES DEUX DOUBLES DE CONNEXION COEXISTENT, parce que le dépôt emploie deux
protocoles : `cursor()` pour les prévisions (#37), `execute()` pour les lectures
scriptées (#179). `FakeConn` porte les deux et garde la RÉFÉRENCE de la liste de
prévisions au lieu de la copier — sans quoi la fixture `passe_horaire`, qui
l'étend après le montage, n'aurait aucun effet sur une instance partagée.
`previsions if previsions is not None else []` et non `or []` : une liste vide
est fausse, et `or` en fabriquerait une neuve dans le cas le plus courant.

La connexion porte désormais une courbe de vingt-quatre heures PAR DÉFAUT : une
base qui a des mesures est l'état normal, et un cas qui parle de la prévision
n'a pas à scripter des points pour y arriver. Elle est ancrée sur
`fixtures.INSTANT_REFERENCE`, dont `INSTANT_PREVU` dérive aussi — deux dates
indépendantes faisaient échouer « le point H+1 tombe après le dernier mesuré »
sans que le cas ait tort.

Six cas retirés : les deux qui gardaient mon calcul de persistance, et quatre
qui lisaient un profil de fixtures que la base remplace. Deux doublons de nom,
artefacts de la fusion, masquaient silencieusement les miens — dont un portait
un `fiche` jamais défini.

1 060 unitaires Python, 380 du tableau de bord, 5 d'intégration contre la
préproduction, ruff check et format, mypy strict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merge branch 'develop' into marvin/179-tableau-de-bord-zone-or
Some checks failed
Intégration / Python — qualité, tests et dépendances (pull_request) Has been cancelled
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Has been cancelled
Intégration / Contrôles statiques du dépôt (pull_request) Has been cancelled
Intégration / Workflows — lint et audit de sécurité (pull_request) Has been cancelled
12eb16a488
lenaic approved these changes 2026-09-08 10:02:05 +00:00
lenaic merged commit d753bebcbd into develop 2026-09-08 10:02:28 +00:00
lenaic deleted branch marvin/179-tableau-de-bord-zone-or 2026-09-08 10:02:28 +00:00
olivier approved these changes 2026-09-08 10:02:36 +00:00
Sign in to join this conversation.
No reviewers
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!182
No description provided.