Mise en production : le tableau de bord quitte les fixtures #198

Merged
lenaic merged 14 commits from develop into main 2026-09-08 12:27:55 +00:00
Owner

Mise en production. Quatorze commits de retard sur main.

Ce que ça change en prod

  • Le tableau de bord ne lit plus aucune fixture. Consommation, dernier relevé, taux de disponibilité, part imputée, recommandations par site : tout vient de enervision_prod. C'est le point le plus visible, derniere_releve affichait le 1er septembre.
  • L'écart à la veille compare bien le même parc des deux côtés, et vaut null quand la veille est absente au lieu d'un pourcentage inventé.
  • Supervision : deux panneaux Grafana portaient un titre qui ne décrivait pas leur mesure, plus un panneau de fraîcheur réelle de la collecte (seuils 180 s / 300 s).
  • Les deux manuels (reprise.md, deploiement.md) ont une section panne et une section retour arrière, et le filet du restore n'écrase plus la sauvegarde qu'il protège.
  • Fenêtre de promotion du modèle alignée à 14 jours dans l'ADR et dans le défaut de --jours-test.

Vérifications

Chaîne verte sur chaque demande fusionnée. 1 068 tests unitaires, ruff, mypy --strict. Les trois requêtes SQL du tableau de bord ont été passées à la main sur enervision_prod.

Après le déploiement

Rien à jouer manuellement. Le déploiement recrée api et supervision.

Mise en production. Quatorze commits de retard sur `main`. ### Ce que ça change en prod - Le tableau de bord ne lit plus aucune fixture. Consommation, dernier relevé, taux de disponibilité, part imputée, recommandations par site : tout vient de `enervision_prod`. C'est le point le plus visible, `derniere_releve` affichait le 1er septembre. - L'écart à la veille compare bien le même parc des deux côtés, et vaut `null` quand la veille est absente au lieu d'un pourcentage inventé. - Supervision : deux panneaux Grafana portaient un titre qui ne décrivait pas leur mesure, plus un panneau de fraîcheur réelle de la collecte (seuils 180 s / 300 s). - Les deux manuels (`reprise.md`, `deploiement.md`) ont une section panne et une section retour arrière, et le filet du `restore` n'écrase plus la sauvegarde qu'il protège. - Fenêtre de promotion du modèle alignée à 14 jours dans l'ADR et dans le défaut de `--jours-test`. ### Vérifications Chaîne verte sur chaque demande fusionnée. 1 068 tests unitaires, `ruff`, `mypy --strict`. Les trois requêtes SQL du tableau de bord ont été passées à la main sur `enervision_prod`. ### Après le déploiement Rien à jouer manuellement. Le déploiement recrée `api` et `supervision`.
lenaic self-assigned this 2026-09-08 12:10:13 +00:00
docs: les deux manuels disent quoi faire quand ça échoue, et le filet ne détruit plus sa source (#44)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 39s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 19s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 2m21s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m49s
c858c27d3a
Demandé par Gabriel : reprise.md n'avait AUCUNE section d'échec, deploiement.md
n'avait qu'un garde-fou préventif. Gabarit repris de mlflow.md §8-§9.

reprise.md — les trois frictions de l'exercice du 02/09 remontent du tableau
de constat vers une procédure : pg_restore sans shared_preload_libraries,
forgejo.db rejoué par-dessus le vidage, l'app.ini qui écoute sur 127.0.0.1.
Plus les quatre cas rencontrés depuis : coffre qui refuse, vidage illisible,
migrations plus récentes que le vidage, pile api sautée faute de clé.

deploiement.md — huit symptômes, dont les quatre demandés : coffre qui refuse,
UFW qui coupe SSH, crontab qui garde une tâche retirée du dépôt, retour arrière
du front. Plus le cas le plus traître, « failed=0 mais rien de neuf », qui est
arrivé deux fois cette semaine.

ET UN DÉFAUT TROUVÉ EN ÉCRIVANT, qui est la raison d'être de l'exercice.

restore.yml prend un « filet » avant d'écraser, en appelant pg-backup.sh. Ce
script nomme ses fichiers <base>-<date du jour>.dump, dans le même répertoire.
Restaurer la sauvegarde DU JOUR faisait donc écraser la source par le filet,
juste avant de la lire : la reprise repartait de l'état cassé, et le dispositif
censé protéger détruisait ce qu'il protégeait.

Le correctif tient en deux lignes. pg-backup.sh accepte PG_BACKUP_DEST, et
restore.yml écrit son filet dans backup_dir/filet/. Le script crée son
répertoire au besoin, sans quoi la redirection échouerait sur un message
inutile.

ansible-lint profil production, yamllint propre, trois playbooks valides,
shellcheck propre, liens markdown vérifiés.
Le critère écrit le 06/09 jugeait le candidat sur les trois derniers jours.
Sur cette fenêtre, la persistance tombe à 10,2 kW contre 22,2 sur quatorze :
trois jours peuvent tomber entièrement sur une plage plate, où la référence
est presque imbattable sans que le modèle y soit pour rien. Le modèle promu
ce matin perdait donc sur le critère qui l'autorisait.

Quatorze jours est la plus courte fenêtre qui contient deux week-ends. Le
motif ne dépend d'aucun des trois résultats mesurés, et c'est la seule raison
pour laquelle il est opposable : une fenêtre choisie parce qu'elle gagne ne
se défend pas.

Le relevé des trois fenêtres est publié en entier, y compris celle où le
modèle perd, et la fiche dit que les trois ont été regardées avant que la
règle ne soit fixée. Elle note aussi que `--jours-test` commande à la fois
le jugement et l'apprentissage — 1 848 exemples de moins entre trois et
quatorze jours — et renvoie la séparation des deux au #118.

Le 2,018 kW du 07/09 est réexpliqué plutôt que retiré : il vient de
`enervision_preprod`, pas de la production. Il lui manquait le nom de sa
base.
adr: les deux mentions résiduelles de trois jours suivent le nouveau critère (#36)
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 24s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 39s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m45s
4923b7317c
inference: le défaut de --jours-test suit le critère de promotion (#36)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 41s
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 26s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m43s
96e8cfcc3e
L'ADR 0013 amendée juge sur quatorze jours, mais `_JOURS_TEST_DEFAUT` valait
toujours trois. Un entraînement lancé sans option appliquait donc une règle
que la fiche ne reconnaît plus, et il aurait fallu se souvenir de
`--jours-test 14` à chaque fois. Le défaut est la règle : il la suit.

Le manuel de réentraînement disait « trois derniers jours » au paragraphe qui
énonce le critère ; il dit quatorze, et ajoute que changer l'option à la main
change le critère et pas seulement le découpage — sur trois jours la
persistance peut tomber à moitié de son erreur habituelle. Pour un essai,
`--sans-registre`.

tests/unit/model : 95 passés, 1 sauté.
Merge pull request '[36] La fenêtre de promotion du modèle passe de 3 à 14 jours' (#194) from gabriel/36-critere-promotion into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 8s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 39s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 28s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m27s
7f4dcd5c1a
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/194
Reviewed-by: lenaic <lenaic@noreply.10.105.200.41>
Merge branch 'develop' into lenaic/44-manuels-echec
All checks were successful
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 22s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 38s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m51s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m34s
d140996bcf
api: le tableau de bord quitte entièrement les fixtures (#179)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 40s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
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 5m50s
67a6c57fdf
repository.py ne lit plus fixtures.py DU TOUT. Il en portait trente et un
renvois hier matin, dix ce midi, zéro maintenant.

Ce qui devient réel, et la mesure côté production :

  consommation_kw      176,220 kW au lieu de 104,0 figé
  derniere_releve      10h59 aujourd'hui au lieu du 1er septembre 16h42
  disponibilite_pct    80,6 % lu dans qualite_jour
  imputation_pct       dérivé de repartition_methode, la colonne n'existe pas
  taux_de_charge, palier   deviennent vrais d'eux-mêmes, ce sont des dérivés
  consommation_veille  3 619,3 kW, le même seau horaire à J-1
  recommandations      2 actives sur SITE003, produites par R1

Le champ le plus visible était derniere_releve : l'écran affichait le 1er
septembre pendant que la relève tournait à la minute et que la base portait
88 440 lignes pour le seul SITE001. Sur un tableau de bord, la première chose
qu'on regarde est l'heure.

DEUX CHAMPS DEVIENNENT FACULTATIFS, de bout en bout.

  derniere_releve      un site jamais mesuré n'en a pas. Lui en inventer une
                       serait pire que de ne rien afficher, et le masquer
                       donnerait un parc plus petit que le parc. Le front rend
                       un tiret : formaterHeure(null) valait 01:00, une heure
                       plausible et fausse.
  consommation_veille  le seau de la veille peut manquer. ecart_veille_pct rend
                       alors None, et aussi quand la veille vaut zéro : un parc
                       à l'arrêt ne donne pas un écart infini, il ne donne pas
                       d'écart du tout.

DISPONIBILITE_CIBLE_PCT reste une constante, et sort de fixtures.py. Ce n'est
pas une donnée, c'est un objectif d'équipe qu'aucune table ne porte : c'était
son voisinage avec des valeurs inventées qui la rendait suspecte.

Trois doubles de test corrigés, et chacun cachait un défaut :

  FakeCurseur    rendait ses lignes QUELLE QUE SOIT la requête. Il rendait donc
                 les prévisions à qui demandait le référentiel.
  les réponses   ignoraient les paramètres : le double rendait les
                 recommandations de tout le parc à qui demandait celles d'un
                 seul site, et le cas d'un site sans recommandation passait au
                 vert sur un dépôt qui n'aurait rien filtré.
  CurseurDouble  exigeait son second argument alors que psycopg le rend
                 facultatif. Le double était plus strict que ce qu'il double.

Les fixtures restent, à leur place : le jeu d'essai. Les sept sites, le site
muet et les quatre paliers sont inchangés pour les cas, et c'est le point,
aucun d'eux n'a eu à bouger.

Les trois requêtes sont éprouvées contre enervision_prod avant d'être écrites.
1 068 tests, ruff propre, mypy --strict propre sur 23 fichiers.
reprise : les commandes du manuel d'échec s'exécutent vraiment (#44)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 40s
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 18s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m13s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m53s
6453f5d31e
Les trois gestes documentés ont été rejoués sur ml-stagiaire-02. Aucun ne
passait.

`PGOPTIONS="-c shared_preload_libraries=timescaledb"` fait refuser la
connexion : le paramètre est de contexte POSTMASTER. Il est de toute façon
déjà posé au niveau du serveur. Ce qui manquait à la restauration du 02/09
n'était pas le chargement de l'extension mais le mode « restoring », d'où
timescaledb_pre_restore() / post_restore() encadrant le pg_restore, le second
à jouer même en cas d'échec du premier.

Le conteneur ne monte pas /var/backups : un chemin de vidage passé en argument
à pg_restore n'existe pas de son point de vue. Le vidage entre par stdin.

`sudo cmd < fichier` ouvre le fichier avec les droits de l'appelant, et le
répertoire est en 0750 root:root : les deux commandes de retour arrière
échouaient en « Permission denied » avant de joindre la base. La redirection
passe dans un sh -c, et le `ls` prend son sudo.

restore.yml : le filet détourne aussi PG_BACKUP_TEXTFILE_DIR. pg-backup.sh
publie sa métrique dans tous les cas, et publier_metrique ne suit pas
PG_BACKUP_DEST : un filet réussi éteignait 26 h l'alerte du #42, alors qu'un
vidage quotidien cassé est souvent la raison même de la restauration.

rôle backup : le premier vidage ne se déclenche plus que si aucun vidage n'est
présent. `.dernier-etat` est un point-fichier, qu'un `cp *.dump` de reprise sur
machine neuve n'emporte pas ; le vidage partait alors sur des bases vides et
écrasait celui qu'on venait de recopier. Un vidage vide passe `pg_restore -l`,
donc ni verifier-sauvegardes.sh ni le contrôle d'entrée de restore.yml ne
l'attrapaient.
supervision: deux panneaux mesuraient juste et se lisaient faux (#112)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 40s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
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 5m38s
6459a8f39d
Signalé en regardant le tableau de bord en production. Les deux chiffres sont
exacts, ce sont leurs titres qui font conclure à une panne.

1. « Fraîcheur par site » affichait 45,9 minutes sur les sept sites.

   Ce n'est pas l'âge de la collecte, c'est celui de la ZONE OR, et la chaîne
   est horaire par construction : argent à :00, or à :17, chargement en base à
   :27. La valeur oscille donc normalement entre 28 et 88 minutes, et un
   chiffre élevé juste avant :27 est attendu.

   Pendant ce temps la collecte est fraîche à 84 secondes, mesurée à l'instant.
   Un lecteur qui ouvre Grafana à 12h25 lit « 88 minutes » sous un panneau
   intitulé « Fraîcheur » et conclut que la collecte est morte.

2. « Volume ingéré par heure » affichait 0, puis 33, puis 302.

   La grille est TOUJOURS complète, 420 lignes par heure pour sept sites. Ce
   qui varie est la part que la source rend exploitable : le 08/09 à 08 h, 416
   relevés sur 420 étaient marqués « critical » par la source simulée. Le
   collecteur, lui, a écrit 7/7 objets avec charge utile à chaque minute, sans
   une seule erreur au journal.

   Un creux n'est donc pas une panne de la chaîne, c'est une heure de source
   dégradée — précisément ce que la règle R3 sert à rendre visible. Le titre
   dit maintenant « relevés exploitables », et la description donne le chiffre.

3. Un panneau de plus : la fraîcheur RÉELLE de la collecte.

   Elle existait déjà dans Prometheus depuis le #42,
   ev_ops_collecte_derniere_reussite_timestamp_seconds, republiée par
   node-exporter et personne ne l'affichait. Elle se compte en secondes, avec
   les seuils de l'alerte du #42 : vert, orange à 3 minutes, rouge à 5.

Aucune requête n'a changé. Ce sont trois titres, deux descriptions et un
panneau. Le banc de supervision reste vert.
api: l'écart à la veille compare deux fois le même parc (#179)
All checks were successful
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 24s
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) Successful in 5m54s
7981c13ae0
`synthese_parc` sommait la veille sur TOUS les sites et le jour même sur les
seuls sites mesurés. L'écart rapportait donc une perte de couverture comme une
baisse de consommation.

Le contrat le disait déjà : `consommation_kw` est « la somme des sites mesurés,
les muets exclus », et `consommation_veille_kw` « la même, à la même heure la
veille ». La même — donc sur les mêmes sites.

Sur le parc de production, les chiffres de la relecture du #179 le montrent
seuls : 176,2 kW mesurés contre 3 619,3 kW la veille, soit −95 % affichés en
tête d'écran le jour où six sondes sur sept se taisent. Un chiffre plausible,
donc invisible.

Un cas le tient : la requête de la veille ne doit porter que sur les six sites
mesurés du jeu d'essai, SITE006 étant muet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge branch 'develop' into lenaic/179-reste-des-fixtures
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 39s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 19s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m51s
149ec66ee3
Merge pull request 'docs : les deux manuels disent quoi faire quand ça échoue (#44)' (#190) from lenaic/44-manuels-echec into develop
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 39s
Intégration / Contrôles statiques du dépôt (push) Successful in 6s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 19s
Infra Ansible / Playbooks Ansible valides (push) Successful in 1m36s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m36s
c79d2de1a8
Merge pull request 'api : le tableau de bord quitte entièrement les fixtures (#179)' (#195) from lenaic/179-reste-des-fixtures into develop
Some checks are pending
Intégration / Python — qualité, tests et dépendances (push) Waiting to run
Intégration / Tableau de bord — dépendances, tests et construction (push) Waiting to run
Intégration / Contrôles statiques du dépôt (push) Waiting to run
Intégration / Workflows — lint et audit de sécurité (push) Waiting to run
66cacb01b9
Merge pull request 'supervision : deux panneaux mesuraient juste et se lisaient faux (#112)' (#197) from lenaic/112-fraicheur-lisible into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 8s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 23s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 36s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 39s
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 1m19s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m39s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m32s
71f9f7ee5c
lenaic requested review from gabriel 2026-09-08 12:10:14 +00:00
gabriel approved these changes 2026-09-08 12:27:31 +00:00
gabriel left a comment

Relu en entier. Approuvé, avec UNE CONDITION : la #200 doit entrer dans develop avant que celle-ci parte sur main. Elle corrige le seul point bloquant que la relecture a trouvé, elle est verte, et sans elle la mise en production emporte le défaut décrit plus bas.

Le point bloquant, et ce que la #200 change

_consommation_veille_kw restreint bien la somme d'hier aux sites mesurés aujourd'hui — c'est l'apport du #179, et test_la_veille_est_sommee_sur_les_seuls_sites_mesures le garde. Mais rien ne vérifiait que la veille les portait TOUS. mesure_horaire.moyenne_kw vaut avg(valeur_kw) filter (where indicateur_qualite <> 'critical') (0016) : un site dont le seau de la veille est vide ou entièrement dégradé rend null, sum() l'ignore en silence, et six sites d'aujourd'hui se comparaient à trois d'hier. C'est le faux delta que le périmètre corrigeait, retourné — une perte de COUVERTURE affichée comme une baisse de CONSOMMATION. Le 08/09 à 08 h, la source marquait 416 relevés sur 420 en critical (#112) : le cas n'a rien de théorique, et il tombe sur le premier chiffre de l'écran.

Signe que le garde-fou était prévu mais pas câblé : count(*) as sites était sélectionné et jamais lu. La #200 le lit, en count(moyenne_kw) — une ligne présente dont la moyenne est nulle n'est pas une couverture — et rend None sur une veille partielle. Le contrat le permettait déjà : consommation_veille_kw et ecart_veille_pct sont nullables depuis cette demande-ci, et SyntheseParc.vue les garde derrière un v-if. Au passage, le double de la veille répondait « sites: 7 » à une requête qui en demandait six : il décrivait une base impossible, il suit maintenant ses paramètres, et un cas rescripte une veille à trois sites.

Le reste de la relecture

La nullabilité de derniere_releve, consommation_veille_kw et ecart_veille_pct est propagée partout : SiteOut, SyntheseParcOut, le v-if de la synthèse, et formaterHeure qui rend « — » là où new Date(null) donnait l'epoch, soit 01:00 — une heure plausible pour un site jamais mesuré. Aucun autre lecteur de ces trois champs dans l'API.

Toutes les colonnes des nouvelles requêtes existent : site (0007), qualite_jour (0013), recommandation (0014 et 0019). horodatage est timestamptz, donc le front reçoit un décalage explicite et n'interprète pas une heure UTC comme locale. statut = 'active' est cohérent avec l'index unique partiel de la 0019.

Côté sauvegarde, PG_BACKUP_DEST isole vraiment le filet : .dernier-etat suit $DEST, et la rétention est en -maxdepth 1, donc le sous-répertoire n'est pas purgé. publier_metrique sort en 0 quand le répertoire est absent, donc le PG_BACKUP_TEXTFILE_DIR: /nonexistent de restore.yml fait ce qu'il annonce et le filet ne repose plus la métrique de réussite. Le find *.dump du rôle backup ferme bien le trou du .dernier-etat caché.

Deux réserves, pas bloquantes, à garder en tête

L'écart compare un instantané (la dernière relève par site) à une moyenne horaire. Le corriger demanderait de lire aussi le seau du jour dans mesure_horaire, qui exclut l'heure en cours (end_offset => 1 hour) : le chiffre de tête aurait jusqu'à deux heures de retard. C'est un moins bon compromis la veille du jury, donc rien ne bouge, mais l'asymétrie est réelle.

Et qualite_jour est lue par order by jour desc limit 1, sans borne : si le job de qualité s'arrête, l'écran continue d'afficher la disponibilité du dernier jour calculé sans dire qu'elle est vieille. À traiter avec la fraîcheur du reste, pas ici.

Relu en entier. Approuvé, avec UNE CONDITION : la #200 doit entrer dans `develop` avant que celle-ci parte sur `main`. Elle corrige le seul point bloquant que la relecture a trouvé, elle est verte, et sans elle la mise en production emporte le défaut décrit plus bas. ## Le point bloquant, et ce que la #200 change `_consommation_veille_kw` restreint bien la somme d'hier aux sites mesurés aujourd'hui — c'est l'apport du #179, et `test_la_veille_est_sommee_sur_les_seuls_sites_mesures` le garde. Mais rien ne vérifiait que la veille les portait TOUS. `mesure_horaire.moyenne_kw` vaut `avg(valeur_kw) filter (where indicateur_qualite <> 'critical')` (0016) : un site dont le seau de la veille est vide ou entièrement dégradé rend `null`, `sum()` l'ignore en silence, et six sites d'aujourd'hui se comparaient à trois d'hier. C'est le faux delta que le périmètre corrigeait, retourné — une perte de COUVERTURE affichée comme une baisse de CONSOMMATION. Le 08/09 à 08 h, la source marquait 416 relevés sur 420 en `critical` (#112) : le cas n'a rien de théorique, et il tombe sur le premier chiffre de l'écran. Signe que le garde-fou était prévu mais pas câblé : `count(*) as sites` était sélectionné et jamais lu. La #200 le lit, en `count(moyenne_kw)` — une ligne présente dont la moyenne est nulle n'est pas une couverture — et rend `None` sur une veille partielle. Le contrat le permettait déjà : `consommation_veille_kw` et `ecart_veille_pct` sont nullables depuis cette demande-ci, et `SyntheseParc.vue` les garde derrière un `v-if`. Au passage, le double de la veille répondait « sites: 7 » à une requête qui en demandait six : il décrivait une base impossible, il suit maintenant ses paramètres, et un cas rescripte une veille à trois sites. ## Le reste de la relecture La nullabilité de `derniere_releve`, `consommation_veille_kw` et `ecart_veille_pct` est propagée partout : `SiteOut`, `SyntheseParcOut`, le `v-if` de la synthèse, et `formaterHeure` qui rend « — » là où `new Date(null)` donnait l'epoch, soit `01:00` — une heure plausible pour un site jamais mesuré. Aucun autre lecteur de ces trois champs dans l'API. Toutes les colonnes des nouvelles requêtes existent : `site` (0007), `qualite_jour` (0013), `recommandation` (0014 et 0019). `horodatage` est `timestamptz`, donc le front reçoit un décalage explicite et n'interprète pas une heure UTC comme locale. `statut = 'active'` est cohérent avec l'index unique partiel de la 0019. Côté sauvegarde, `PG_BACKUP_DEST` isole vraiment le filet : `.dernier-etat` suit `$DEST`, et la rétention est en `-maxdepth 1`, donc le sous-répertoire n'est pas purgé. `publier_metrique` sort en 0 quand le répertoire est absent, donc le `PG_BACKUP_TEXTFILE_DIR: /nonexistent` de `restore.yml` fait ce qu'il annonce et le filet ne repose plus la métrique de réussite. Le `find *.dump` du rôle backup ferme bien le trou du `.dernier-etat` caché. ## Deux réserves, pas bloquantes, à garder en tête L'écart compare un instantané (la dernière relève par site) à une moyenne horaire. Le corriger demanderait de lire aussi le seau du jour dans `mesure_horaire`, qui exclut l'heure en cours (`end_offset => 1 hour`) : le chiffre de tête aurait jusqu'à deux heures de retard. C'est un moins bon compromis la veille du jury, donc rien ne bouge, mais l'asymétrie est réelle. Et `qualite_jour` est lue par `order by jour desc limit 1`, sans borne : si le job de qualité s'arrête, l'écran continue d'afficher la disponibilité du dernier jour calculé sans dire qu'elle est vieille. À traiter avec la fraîcheur du reste, pas ici.
lenaic merged commit 9e1896255e into main 2026-09-08 12:27:55 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 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!198
No description provided.