infra : la pile app — l'API et le tableau de bord servis par le serveur #160

Merged
gabriel merged 10 commits from lenaic/113-pile-app into develop 2026-09-07 12:54:43 +00:00
Owner

infra : la pile app, pour que l'application existe ailleurs que sur un poste

Rien dans le dépôt ne déploie l'API ni le tableau de bord. Vérifié sur le
serveur le 06/09 : rien n'écoute sur 127.0.0.1:8000, aucun conteneur api, et
app.g2.enervision rend HTTP 404. Le Caddyfile attend /api depuis le #32.

Ce que ça ajoute

  • infra/compose/app/docker-compose.yml : un service api (image épinglée par
    empreinte, réseau hôte, uvicorn --root-path /api, env_file, mem_limit) et
    un service dashboard-build à exécution unique, sur le modèle de minio-init.
  • roles/app/templates/api.env.j2 : JWT_SECRET depuis le coffre,
    DATABASE_URL de production, TRUST_PROXY, COOKIE_SECURE,
    RUN_MIGRATIONS_ON_STARTUP=false — le rôle les applique déjà.
  • db/creer-compte.py : ouvre le premier compte sans passer par l'API. Il
    réutilise enervision_api.auth.passwords, il ne réimplémente pas le hachage.
    Avec ses tests.
  • docs/runbooks/application.md : premier compte, relance, journaux.

Une dérive de déploiement, pas un trou de conception

Le dépôt déclare bien ./www:/srv/www:ro sur le service proxy, et la page
d'attente est versionnée. Mais le conteneur g2-forge-proxy qui tourne n'a que
trois volumes
: le Caddyfile et les deux répertoires de données. Pas de
/srv/www. La pile forge n'a jamais été recréée depuis l'ajout du volume — d'où
le 404.

Il faudra donc recréer la pile forge au déploiement, pas seulement jouer le
rôle app.

La décision que cette PR tranche

infra/compose/forge/www/ est versionné. Y déposer un dist/ construit
mettrait de la sortie de build sous git. La sortie va donc dans
/opt/g2-forge/www/app sur l'hôte, qui est le déploiement de la pile forge
et non une copie de travail. Un .gitignore commenté garde la page d'attente et
explique pourquoi.

Ce que cette PR ne fait pas

  • Aucune route de l'API n'est branchée sur la base. C'est le #38, c'est
    Marvin, et c'est ce qu'il défendra.
  • Aucun secret n'est généré ni posé. La variable et l'exemple de coffre sont
    préparés ; ansible-vault edit reste un geste humain.
  • Aucun déploiement n'a été joué.

⚠️ Ordre de fusion

Cette branche contient les commits de la #159 (les deux touchent
group_vars/all/vars.yml et l'index des runbooks). Fusionner #159 d'abord.
Elle ne se rebase pas proprement sur develop seule.

Vérifiable après déploiement

curl -k https://app.g2.enervision/api/health   # {"status":"ok"}
curl -k https://app.g2.enervision/             # le <title> du build
curl -k https://app.g2.enervision/api/docs     # la documentation FastAPI

Exigences : ENF-01, ENF-09, ENF-14. Suite du #113 ; sert le déploiement de #38 et #24.

# infra : la pile app, pour que l'application existe ailleurs que sur un poste **Rien dans le dépôt ne déploie l'API ni le tableau de bord.** Vérifié sur le serveur le 06/09 : rien n'écoute sur `127.0.0.1:8000`, aucun conteneur `api`, et `app.g2.enervision` rend **HTTP 404**. Le Caddyfile attend `/api` depuis le #32. ## Ce que ça ajoute - `infra/compose/app/docker-compose.yml` : un service `api` (image épinglée par empreinte, réseau hôte, `uvicorn --root-path /api`, `env_file`, `mem_limit`) et un service `dashboard-build` à exécution unique, sur le modèle de `minio-init`. - `roles/app/templates/api.env.j2` : `JWT_SECRET` depuis le coffre, `DATABASE_URL` de production, `TRUST_PROXY`, `COOKIE_SECURE`, `RUN_MIGRATIONS_ON_STARTUP=false` — le rôle les applique déjà. - `db/creer-compte.py` : ouvre le premier compte sans passer par l'API. Il réutilise `enervision_api.auth.passwords`, il ne réimplémente pas le hachage. Avec ses tests. - `docs/runbooks/application.md` : premier compte, relance, journaux. ## Une dérive de déploiement, pas un trou de conception Le dépôt déclare bien `./www:/srv/www:ro` sur le service proxy, et la page d'attente est versionnée. Mais **le conteneur `g2-forge-proxy` qui tourne n'a que trois volumes** : le Caddyfile et les deux répertoires de données. Pas de `/srv/www`. La pile forge n'a jamais été recréée depuis l'ajout du volume — d'où le 404. **Il faudra donc recréer la pile forge au déploiement**, pas seulement jouer le rôle `app`. ## La décision que cette PR tranche `infra/compose/forge/www/` est **versionné**. Y déposer un `dist/` construit mettrait de la sortie de build sous git. La sortie va donc dans `/opt/g2-forge/www/app` **sur l'hôte**, qui est le déploiement de la pile forge et non une copie de travail. Un `.gitignore` commenté garde la page d'attente et explique pourquoi. ## Ce que cette PR ne fait pas - **Aucune route de l'API n'est branchée sur la base.** C'est le #38, c'est Marvin, et c'est ce qu'il défendra. - **Aucun secret n'est généré ni posé.** La variable et l'exemple de coffre sont préparés ; `ansible-vault edit` reste un geste humain. - **Aucun déploiement n'a été joué.** ## ⚠️ Ordre de fusion Cette branche **contient les commits de la #159** (les deux touchent `group_vars/all/vars.yml` et l'index des runbooks). **Fusionner #159 d'abord.** Elle ne se rebase pas proprement sur `develop` seule. ## Vérifiable après déploiement ```sh curl -k https://app.g2.enervision/api/health # {"status":"ok"} curl -k https://app.g2.enervision/ # le <title> du build curl -k https://app.g2.enervision/api/docs # la documentation FastAPI ``` Exigences : ENF-01, ENF-09, ENF-14. Suite du #113 ; sert le déploiement de #38 et #24.
Rien dans le dépôt ne déployait l'API ni le front : `app.g2.enervision` rend
404 (relevé le 06/09 sur 10.105.200.41), et l'application ne se démontre que
sur un poste. Le Caddyfile de la forge attend les deux depuis le #32 — `/api/*`
vers 127.0.0.1:8000, le reste servi depuis /srv/www/app — et il est déjà juste :
il n'est pas touché.

Deux services, tous deux en réseau hôte parce qu'aucun réseau bridge ne démarre
sur ce LXC :

  - `api`, python:3.12-slim épinglée par empreinte, uvicorn sur 127.0.0.1:8000,
    secrets dans /etc/enervision/api.env, mémoire bornée à 512 Mo. Les
    dépendances vivent dans un volume nommé et ne sont réinstallées que si
    l'empreinte de requirements.txt change : réinstaller à chaque démarrage
    rendrait l'API dépendante de PyPI pour redémarrer, ne jamais réinstaller
    ferait tourner l'ancien environnement après un déploiement, en silence ;
  - `dashboard-build`, node:22 épinglée, à exécution unique sur le modèle de
    `minio-init`. `npm ci` puis `vite build` avec VITE_API_BASE=/api — relative
    et non absolue, pour ne pas graver le nom d'hôte dans le paquet livré.

Images épinglées par empreinte et non par tag : le contrôle d'images de la
chaîne accepterait un tag, mais le groupe n'a pas de registre où publier une
image maison, et l'empreinte donne la même reproductibilité sans registre.
Relevées le 06/09 : Python 3.12.14, Node 22.23.2.

OÙ VA LA SORTIE DU BUILD, et c'est la décision de cette PR. Le montage de Caddy
est `./www:/srv/www:ro` et `www/app/index.html` est versionné : déposer un
`dist/` dans un répertoire suivi par Git serait un choix, pas un détail. Le
build écrit dans /opt/g2-forge/www/app SUR L'HÔTE, qui est le déploiement de la
pile forge et non une copie de travail — le répertoire versionné n'est donc
jamais écrasé. Le .gitignore ajouté est la ceinture par-dessus les bretelles,
pour le jour où quelqu'un déploiera la forge par un `git clone`.

La mise en ligne se fait par renommage de répertoire, après un build réussi, et
laisse `app.ancien` derrière elle : un `npm ci` en échec ne peut pas emporter ce
qui était servi, et le retour arrière ne demande pas de reconstruire.
La pile est déclarée sous le nom `api` et le chemin `app`, et cet écart est
délibéré : le rôle construit le nom du fichier de secrets depuis `name`, donc
la pile cherche /etc/enervision/api.env — le fichier qui porte le secret de
signature et le DSN. Le répertoire, lui, porte l'API ET la construction du
front, qui ne sont pas la même chose que « l'API ».

Elle est placée après postgres, dont l'API lit la base, et avant supervision,
dont la place en dernier tient à Grafana qui teste sa source au démarrage.

Conséquence utile de ce nommage : sans api.env, la pile est SAUTÉE bruyamment
par le dispositif déjà en place, au lieu de démarrer une API qui refuserait de
servir — `Settings.jwt_secret` n'a pas de valeur par défaut.

api.env.j2 pose ce que le Caddyfile attend depuis le #32, et rien de plus :
ROOT_PATH=/api pour que les URL émises retrouvent le préfixe que `handle_path`
retire, TRUST_PROXY=true sans quoi la limite de cinq échecs par IP compte l'IP
de Caddy et un seul poste peut verrouiller tout le monde, COOKIE_SECURE=true,
ALLOWED_ORIGIN vide parce que le front et l'API sont sur la même origine.
RUN_MIGRATIONS_ON_STARTUP=false : le rôle applique déjà les migrations, sous le
rôle applicatif et avant que les piles démarrent ; deux applicateurs pour une
base finissent par se croiser.

vault_api_jwt_secret entre dans la garde du rôle, avec un seuil de 32
caractères et non « non vide » : un secret de signature court se retrouve hors
ligne à partir d'un seul jeton capté, et un jeton forgé vaut une session admin.
La valeur appartient au coffre, elle n'est ni générée ni posée ici.

Le rôle crée /opt/g2-forge/www/app côté hôte. Ce répertoire appartient à la
pile app, qui le PRODUIT ; la pile forge ne fait que le SERVIR, en lecture
seule. Il est créé par Ansible et non par le démon Docker, qui le poserait en
root:root 0755 à un moment que personne ne relit. `deploy` n'écrit pas dans
/opt/g2-forge (drwxr-x--- root root, relevé le 06/09) et n'a pas à y écrire :
ce sont les conteneurs qui écrivent, par le démon.

L'attente de bonne santé passe de 180 à 600 s : au premier passage, l'API
construit son environnement Python et le front subit un `npm ci` puis un
`vite build`. Les passages suivants restent courts.
La base démarre vide et toutes les routes du tableau de bord exigent une
session ; `POST /auth/users` réclame déjà un administrateur. Sur une base
neuve, il n'existe donc aucun chemin par l'API pour créer le premier compte, et
la réponse était jusqu'ici « en SQL » (README de l'API). Un `insert` à la main
est exactement ce qu'on ne veut pas faire en démonstration : il faut produire
soi-même un condensé argon2id, et une adresse en majuscules passe la contrainte
d'unicité tout en restant introuvable au login.

Le script réutilise, il ne réimplémente pas : le hachage vient de
`enervision_api.auth.passwords`, l'insertion de `enervision_api.auth.repository`.
Un second hachage écrit ici dériverait des paramètres argon2 de l'API le jour où
ceux-ci changeraient, et les comptes créés cesseraient de se connecter sans le
moindre message — la vérification rendant simplement False.

Le mot de passe n'a pas d'option : une ligne de commande est lisible par `ps` le
temps de son exécution et reste dans l'historique du shell. Il est demandé au
clavier et confirmé, ou lu sur l'entrée standard pour un usage scripté. Le DSN
suit la règle de db/migrate.py.

Le seuil est celui de l'API, huit caractères, et pas un de plus : plus strict
ici refuserait un mot de passe que `POST /auth/users` accepte, et le suivant en
conclurait que la base est cassée. En dessous de quatorze on le dit, on ne
l'interdit pas. Le rôle par défaut est `user` : un rôle d'administrateur se
demande, il ne s'obtient pas par distraction.

Vingt cas d'essai, sans PostgreSQL, sur un double de connexion minuscule — un
double qui imiterait psycopg finirait par mentir sur psycopg. Ils portent sur
ce qui ne se voit pas à la relecture : l'adresse part en minuscules, c'est un
condensé argon2id vérifiable qui est inséré et jamais le mot de passe, le sel
est tiré à chaque fois, la transaction est validée, et rien n'est écrit quand la
saisie est refusée.
docs: publie le manuel d'exploitation de l'application
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 19s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 4s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m4s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m42s
05bedeff4e
Créer le premier compte, relancer la pile, reconstruire le front et revenir en
arrière, lire les journaux. Plus la séquence de première mise en service, dans
l'ordre où elle doit être jouée : le secret dans le coffre d'abord, sans quoi le
rôle refuse de déployer quoi que ce soit ; le déploiement ensuite, qui rattrape
au passage les 52 commits de retard du serveur ; le premier compte ; et
seulement à la fin la recréation de la pile forge.

Cette dernière est isolée parce que c'est le seul geste risqué : elle touche la
forge de six personnes. Le manuel dit comment constater qu'elle est nécessaire
avant de la faire — `docker inspect` sur g2-forge-proxy — parce qu'elle ne l'est
que si le montage /srv/www manque. C'était le cas le 06/09 : la pile en service
est plus ancienne que le dépôt, le volume a été ajouté au compose et la pile n'a
jamais été recréée. Un conteneur ne gagne pas un volume à chaud.

Le manuel dit aussi ce qu'il n'a pas encore prouvé. Rien de ce qui y est écrit
n'a tourné sur le serveur : les vérifications sont marquées « à éprouver », et
le tableau des messages nomme les cas qu'on rencontrera d'abord — le conteneur
qui boucle sur un jwt_secret absent, la pile sautée faute de secret, /api/docs
sans style quand ROOT_PATH manque, et le cgroup mémoire qui pourrait ne pas être
exposé dans ce LXC.
lenaic requested review from florian 2026-09-07 07:06:37 +00:00
lenaic self-assigned this 2026-09-07 07:25:12 +00:00
infra: la clé de signature de l'API ne prend plus les quatre piles en otage (#113)
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 19s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 58s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m31s
e6a6b77ead
vault_api_jwt_secret était dans l'assertion d'entrée du rôle app. Sans clé au
coffre, ce n'était pas l'API qui échouait mais le rôle ENTIER : PostgreSQL,
MinIO, MLflow, le certificat TLS, l'environnement Python, la crontab et les
migrations. Et comme deploy.yml joue --tags app,proxy, une fusion vers main
cassait tout le déploiement continu, sur une machine qui collecte depuis
quatre jours.

C'est le défaut que j'ai formellement bloqué chez Gabriel sur la #152, dans le
même fichier et à la même ligne. Je le corrige avant de le faire relire.

Le rôle savait déjà faire mieux, et la déclaration de la pile api le dit
noir sur blanc dans group_vars/all/vars.yml : « sans api.env, la pile est
SAUTÉE bruyamment au lieu de démarrer une API qui refuserait de servir ». Le
rendu de api.env est donc conditionné à une clé d'au moins 32 caractères, et
une tâche dit à voix haute pourquoi le fichier n'a pas été écrit.

Et surtout PAS un default('') comme pour le jeton d'alerte de la #42 : un
jeton d'alerte vide fait taire une notification, une clé de signature vide
ferait accepter des jetons forgés. Ici le bon comportement est de ne rien
écrire du tout.

Deux contrôles ajoutés au banc du rôle pour que ça ne revienne pas, l'un sur
l'assertion d'entrée, l'autre sur le rendu conditionné. Ils lisent les lignes
exécutées, commentaires retirés : le nom du secret figure dans l'explication
juste en dessous, et un contrôle qui lirait le bloc brut resterait vert.

Éprouvé en rouge sur les deux défauts réintroduits un par un. ansible-lint
profil production, yamllint propre, trois playbooks à la syntaxe valide, banc
12 ok.
dashboard: connexion du back avec le front de la page connxion fait
All checks were successful
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m5s
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 / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 25s
02a0d16be0
Écran Parc du ticket #24 : sept sites, consommation agrégée et son profil
horaire, disponibilité moyenne contre sa cible, alertes ouvertes par
sévérité (SyntheseParc, GET /parc/synthese + /parc/mesures) — au-dessus
de la comparaison multi-sites déjà en place (un appel /sites/{id}/mesures
par site coché, ligne de moyenne, infobulle, source indisponible sans
bloquer les autres, défilement horizontal sur téléphone).

Colonne fraîcheur (dernière relève) ajoutée au tableau des sites.

Reprise de session au chargement (App.vue) : session.reprendre() existait
dans le dépôt sans être jamais appelée — un rechargement de page perdait
une session pourtant valide côté cookie.

Menu du compte dans l'en-tête (EnteteApplication, MenuProfil) : profil,
panneau admin si le rôle l'autorise, déconnexion — qui redirige maintenant
vers /connexion, comme une connexion réussie redirige vers /graphiques.
/profil et /administration sont des pages minimales, sans contenu inventé :
le changement de mot de passe (#21) et la gestion des comptes (#16) restent
à construire, aucun lien mort en attendant.

42 tests neufs (154 au total), palette catégorielle validée par le skill
dataviz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnUAobdPeQynk8EnCeMLdW
Un 401 reçu par un appel du tableau de bord (api/sites.js — le seul module
qui parle à des routes gatées) signale l'expiration au dépôt de session
(signalerExpiration()), qui vide le profil et ouvre ModalReconnexion.

La modale se pose par-dessus la page dans son état incomplet : ni Échap ni
clic sur le fond ne la ferment, piège à focus minimal (Tab/Maj+Tab bouclent
dedans). Deux issues : une reconnexion réussie (le dépôt referme expiree
lui-même, symétrique à connecter()), ou « Retourner à la connexion » qui
abandonne franchement la page plutôt que de laisser croire que tout va
bien. Montée une fois dans EnteteApplication, donc présente sur toutes les
pages authentifiées.

Distinct des 401 que /auth gère déjà (identifiants refusés, rafraîchissement
épuisé au démarrage) : ceux-là restent traités par connecter() et
reprendre(), signalerExpiration() ne couvre que l'expiration en cours de
route.

13 tests neufs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Supprime la route /administration, la page AdministrationView.vue (stub du
ticket #16, sans contenu réel) et son lien conditionnel au rôle dans le
menu du compte. #16 reste ouvert sur la forge, non touché.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La coquille commune (EnteteApplication) passe de bandeau horizontal
permanent à colonne latérale au delà de 900 px — marque, puis le compte,
puis la navigation. Un seul lien aujourd'hui, « Parc » : le seul écran qui
existe, en ajouter vers des écrans absents inventerait une arborescence que
personne n'a validée.

Fond clair (--ev-paper, celui de la page) plutôt que sombre, sans bordure :
la colonne se fond dans le fond plutôt que de trancher dessus. Le
déclencheur du menu du compte devient une ligne pleine largeur — avatar,
adresse, chevron — au delà du même point de rupture, au lieu du rond
compact du bandeau mobile.

GraphiquesView et ProfilView passent en rangée au même point de rupture
pour que le contenu se place à droite de la colonne plutôt que dessous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Padding, taille du chiffre principal et espacements resserrés — le bloc
prenait plus de la moitié de l'écran pour quatre chiffres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dashboard : les appels du parc visent /v1 et passent par VITE_API_BASE
All checks were successful
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m5s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 1m5s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 4s
Intégration / Aucun secret commité (pull_request) Successful in 3s
eeea397e94
sites.js écrivait « /api/v1/… » en dur et ignorait VITE_API_BASE, alors
qu'authentification.js, lui, le lit depuis toujours. Deux conventions dans le
même dossier, et une seule correspond à ce que Caddy attend.

Caddy relaie en « handle_path /api/* », qui RETIRE le préfixe : « /api/v1/sites »
arrivait à l'API en « /v1/sites », qu'elle ne servait pas — 404. Le proxy de
Vite, lui, conservait « /api », donc le développement marchait et la faute ne
serait apparue qu'une fois déployée.

Le routeur du tableau de bord sert désormais « /v1 » (commit précédent), ce
module vise « /v1/… » et préfixe par VITE_API_BASE comme /auth, et le proxy de
Vite relaie « /v1 » au lieu de « /api » pour que les deux environnements
tapent le même chemin.

Au passage, .env.example annonçait qu'une base vide était « le bon réglage »,
y compris en production. C'est faux et silencieux : Caddy ne relaie que
« /api/* », donc « /v1/sites » tombe sur le bloc qui sert la SPA et rend
index.html en 200, que le front lit en JSON. La variable est obligatoire à la
construction de production — dit explicitement, avec la valeur attendue.

Deux tests gardent la régression : l'un vérifie que la base configurée
préfixe bien l'appel, l'autre qu'elle ne préfixe rien par défaut. Aucun test
ne regardait l'origine jusqu'ici, seulement le chemin — c'est pour ça que
personne ne l'a vu.

Refs #24

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6BwMkMyRBYJJYaHheLwGY
dashboard : écran Site — fiche, courbe, recommandations, traçabilité (ticket #24)
All checks were successful
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 1m28s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 3s
Intégration / Aucun secret commité (pull_request) Successful in 2s
c49b00821f
Nouvel écran sur /sites/:id : FicheSite (référentiel et état courant),
GraphiqueSite (une série, sa référence en pointillé, prévision H+1 et son
écart), RecommandationsSite, TracabiliteDetail. Quatre widgets, quatre
statuts indépendants (stores/site.js) — une fiche en échec ne bloque pas
la courbe ni les recommandations.

Chaque barre de GraphiqueSite est cliquable : elle charge la traçabilité
de ce point (EF-04, « une valeur jusqu'à la lecture bronze »), affichée
par TracabiliteDetail à côté — pas une route à deviner, le point qu'on
regarde déjà. Les points imputés se distinguent en permanence sur la
courbe (teinte allégée + légende), pas seulement au survol : le manque
laissé sur l'écran Parc est corrigé ici.

Intégration : le nom d'un site dans le tableau du Parc mène désormais à
sa fiche.

Les trois routes ajoutées ici (fiche, recommandations, traçabilité) visent
« /v1/… » comme les précédentes : le préfixe « /api » appartient à Caddy, qui
le retire avant de relayer, et c'est VITE_API_BASE qui le pose.

43 tests neufs (207 au total pour le front), build de production propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6BwMkMyRBYJJYaHheLwGY
Merge branch 'develop' into marvin/25-connexion-session
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 3s
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 2m54s
0ae30ba74f
Merge branch 'marvin/25-connexion-session' into marvin/24-ecran-parc
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 5s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m57s
2340d69974
UNE MÉTHODE QUE SEULS SES TESTS APPELLENT EST MORTE. « reprendre() » était
écrit, exporté, couvert par quatre cas — et appelé de nulle part. Relecture
de Justine sur la #138 : recharger la page redemandait les identifiants
d'une session parfaitement valide, cookies encore bons. La PR annonçait la
reprise ; le fil n'était pas branché.

L'appel vit dans « App.vue » et non dans la page de connexion : les cookies
de session ne regardent aucun écran en particulier, et la reprise doit valoir
pour les écrans à venir (#23, #24). Il ne bloque pas le montage — attendre
l'API avant d'afficher quoi que ce soit ferait une page blanche le temps de
l'aller-retour, et sans fin si l'API se tait.

« tests/unit/App.test.js » couvre les trois chemins que « reprendre() »
distingue : cookie encore bon, accès expiré qui déclenche la rotation une
seule fois, et rien à reprendre. Les deux premiers rougissent sur l'App.vue
d'avant, vérifié.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dashboard: la démonstration ne poste plus sur /auth/logout (#25)
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 22s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m14s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m56s
fc7342a6c3
« fermerSession() » était la seule des quatre routes à ne pas porter la garde
« MOCK_ACTIF » — relecture de Justine sur la #138. Sous « VITE_AUTH_MOCK=1 »,
la console annonçait qu'aucun appel ne partait vers l'API pendant que
« Changer de compte » postait sur /auth/logout. Sans API en face, l'appel
échoue ; « deconnecter() » avale l'échec et repose la page, donc rien ne se
voyait — un mode de démonstration qui n'en est plus tout à fait un.

La garde rend le « null » du 204, soit une fermeture réussie : la
démonstration ne pose aucun cookie, il n'y a rien à révoquer, et
l'utilisateur repose sur la page de connexion, ce qu'il a demandé.

Le cas ajouté exerce les trois routes gardées d'un coup et vérifie que
« fetch » n'est jamais appelé. Il rougit sans la garde, vérifié.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
infra: publie le plan de migration cloud chiffré et le plafond
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 3s
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) Successful in 3m24s
21b64e88d9
Le plan compare le coût technique local (11,73 €/mois, hypothèses déclarées)
aux deux scénarios hébergés chiffrés au catalogue public Azure du 04/09 :
reprise à l'identique 127,55 €/mois, services managés 119,58 €/mois. Un ordre
de grandeur d'écart, donc une bascule qui ne se justifie pas par le prix : les
quatre conditions qui la déclencheraient sont posées et vérifiables.

Trois droits manquent au rôle « Devops-cours-projet-eadl », mesurés à la portée
du groupe avec des corps volontairement invalides pour ne rien créer :
Microsoft.Consumption (budget), managementPolicies/write (cycle de vie), et un
second compte de stockage que « storageaccountnumber = 1 » refuse. Le conteneur
blob, lui, passe.

Le plafond est donc déclaré et non provisionné — 5 €/mois, posé en métadonnée
du conteneur pour être lisible dans l'état Terraform comme le ticket le demande.
Rien ne se déclenchera seul, et l'ADR 0012 le dit en clair plutôt que de laisser
le mot « alerte » le suggérer.

Closes #43
docs: journal J5, réaligne PRD et BACKLOG sur l'état réel de la forge (#48)
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 19s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m14s
e078d8af15
Merge branch 'marvin/24-ecran-parc' into marvin/24-ecran-site
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 28s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m54s
6fbf3e5c3a
Merge pull request 'dashboard : la page de connexion parle à l'API réelle (#25)' (#138) from marvin/25-connexion-session into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 5s
Intégration / Aucun secret commité (push) Successful in 3s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 20s
Intégration / Python — qualité, tests et dépendances (push) Successful in 3m23s
f21a619209
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/138
Merge branch 'develop' into gabriel/48-journal-j5-realignement
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 3s
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) Has been cancelled
ac1a1f8183
Merge pull request 'docs: journal J5, réaligne PRD et BACKLOG sur l'état réel de la forge (#48)' (#157) from gabriel/48-journal-j5-realignement into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 6s
Intégration / Aucun secret commité (push) Successful in 3s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 19s
Intégration / Python — qualité, tests et dépendances (push) Successful in 3m25s
58623944e8
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/157
Reviewed-by: lenaic <lenaic@noreply.10.105.200.41>
Merge branch 'develop' into florian/43-plan-migration-cloud
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 20s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m24s
15db2139aa
.env.example documentait la variable, mais rien ne l'appliquait : la CI
construisait sans, et un paquet livré sans base d'API appelle « /v1/… » en
relatif. Caddy ne relaie que « /api/* » ; le reste tombe sur try_files et rend
index.html en 200, que le front essaie de lire en JSON. La panne est totale,
silencieuse, et ne se voit qu'une fois déployée.

vite.config.js refuse donc la construction de production sans elle. Une
assertion et non une valeur par défaut : un défaut plausible (« /api »)
marcherait sur la pile actuelle et masquerait le jour où elle change.

Le fichier exporte désormais une fonction, pour connaître la commande et le
mode ; vitest.config.js l'appelle avec le contexte des tests, mergeConfig ne
sachant pas fusionner une fonction. Le job du tableau de bord passe la
variable, sinon l'assertion le mettrait au rouge sans rien apprendre à
personne. Le paquet réellement déployé reste construit par le rôle app (#94).

Trois tests gardent l'assertion : elle refuse une production sans base, laisse
passer le développement et les tests, et laisse passer une production réglée.

Relecture de la #139.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFGp2MvuHVuoJEss2G5nn1
dashboard: le profil horaire du parc se lit, et les corrections de relecture (#24)
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 30s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m57s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 2m59s
d05ca020f0
Le profil horaire était le dernier critère du #24 en défaut : une polyligne de
20 px, aria-hidden, au fond du pavé « consommation ». mesuresParc était appelé
mais son résultat ne servait qu'à décorer — une courbe sans axe montre une
forme, jamais une valeur ni une heure, et aria-hidden la retirait de l'arbre
d'accessibilité sans rien mettre à la place.

Il a donc sa propre carte : deux axes gradués, l'un en heures UTC, l'autre en
kW depuis zéro, un résumé lu à la place du dessin (bornes et heure des
extrêmes) et un tableau hors écran qui porte les vingt-quatre valeurs. En SVG
et non avec Chart.js : une polyligne à deux axes ne vaut pas la dépendance, et
le SVG reste dans le DOM, donc inspectable.

Le reste de la relecture :

- glossaire.js rendait PALIER[palier].libelle sans repli, quand libelleType et
  libelleSeverite ont le leur : un palier hors référentiel faisait tomber
  TableauSites sur un TypeError.
- chargerSites() n'avait pas de catch et part d'un onMounted. Au-delà du rejet
  non géré, afficherTousLesSites() n'était jamais atteint : la liste restait
  vide, sans état d'erreur ni reprise, là où chargerSynthese fait déjà les
  deux. Elle les fait maintenant aussi.
- le titre et le pied d'infobulle du graphique annonçaient des kWh sur des
  données en kW (valeur_kw).
- TEINTES (variables CSS) et TEINTES_SERIE (hex) décrivaient la même palette
  dans deux fichiers, que jetons.css pouvait désynchroniser sans bruit. Une
  seule source, et un test qui relit jetons.css : la désynchronisation fait
  désormais échouer un test au lieu de peindre deux graphiques discordants.
  La classe .carte, elle aussi, devient commune plutôt que recopiée.
- ecartTexte(null) laissait « par rapport à hier » seul dans le pavé, ce qui
  se lit comme un écart nul. Une absence de mesure n'est pas une stabilité.
- la disponibilité sous cible ne se signalait que par la couleur (RGAA 1.4.1) :
  elle est écrite, la teinte ne fait que la redoubler.
- l'en-tête de GraphiquesView disait encore la coquille de navigation « hors
  périmètre », montée depuis 76e295d.

207 tests vitest passent, la construction de production est propre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFGp2MvuHVuoJEss2G5nn1
Merge branch 'marvin/25-connexion-session' into marvin/24-ecran-parc
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 28s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m2s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m59s
da5b4d25ae
App.vue et App.test.js avaient été écrits deux fois : une première ici, le
03/09, parce que le menu du compte avait besoin de la reprise de session ; une
seconde sur la branche de la #138, le 04/09, quand justine y a signalé que
« reprendre() » n'était appelée de nulle part. Deux créations indépendantes
depuis un ancêtre où le fichier n'existait pas — d'où un conflit de contenu et
un conflit add/add, et non deux intentions qui divergent.

La version de la branche de base est retenue pour les deux fichiers. Les
App.vue sont fonctionnellement identiques (même import, même
onMounted(session.reprendre)), seul le commentaire diffère ; son App.test.js,
lui, couvre un cas de plus, la rotation de l'accès sur 401.

209 tests vitest passent, la construction de production est propre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFGp2MvuHVuoJEss2G5nn1
Merge branch 'develop' into marvin/24-ecran-parc
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 26s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m18s
225db5186e
dashboard: la traçabilité s'atteint au clavier, et se distingue à l'œil (#24)
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 34s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 59s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 2m58s
3bbc9d28b6
Relecture de la #140.

Accès clavier. La traçabilité se prenait au clic seul. Le conteneur portait
déjà « tabindex="0" » — l'intention était là — mais aucune touche n'était
écoutée : au clavier, la traçabilité était hors d'atteinte. Les flèches
parcourent les barres, Home et Fin mènent aux bouts, Entrée ouvre la
traçabilité de la barre désignée. Un anneau la marque sur le canevas, et une
ligne sous le graphique dit ce qu'elle vaut — l'anneau dit laquelle, jamais
combien.

Valeur reconstituée. Elle se distinguait par la même teinte à 40 %, que la
légende répétait à l'identique. Deux nuances d'une même teinte se distinguent
mal sous faible contraste, et l'aplat clair ne tenait pas les 3:1 attendus
d'un élément graphique. La barre porte maintenant des hachures et un contour :
la forme dit la différence, la couleur ne fait que la redoubler. La pastille
de légende reprend les mêmes hachures.

Traçabilité hors 24 h. L'API ne trace qu'un point de la fenêtre 24 h ; au-delà
le pas est journalier et une barre agrège vingt-quatre relevés. Le clic
partait quand même, et rendait 404 sur toutes les barres sauf la dernière —
son début de journée tombant encore dans les vingt-quatre dernières heures.
On ne le propose plus, et on dit pourquoi, au lieu de le faire découvrir par
une erreur. La condition porte sur le pas rendu par l'API, pas sur la valeur
du sélecteur.

Traductions. LIBELLE_METHODE et LIBELLE_QUALITE vivaient dans
TracabiliteDetail.vue, alors que glossaire.js dit porter la traduction des
valeurs d'énumération « et nulle part ailleurs dans le front ». Elles y
rejoignent les autres, avec le repli des voisines.

Unité. Le titre annonçait des kWh sur des valeur_kw. GraphiqueConsommation.vue
porte la même faute mais n'est pas touché ici : elle est déjà corrigée sur la
branche de la #139, et la corriger deux fois est exactement ce qui a produit
les conflits sur App.vue.

224 tests vitest passent, la construction est propre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFGp2MvuHVuoJEss2G5nn1
Merge pull request 'dashboard : écran Parc — synthèse, comparaison des sites, navigation (#24)' (#139) from marvin/24-ecran-parc into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 5s
Intégration / Aucun secret commité (push) Successful in 3s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 25s
Intégration / Python — qualité, tests et dépendances (push) Successful in 3m33s
2ed259ef87
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/139
Merge branch 'develop' into marvin/24-ecran-site
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 33s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 4s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m33s
82de219c22
Merge pull request 'dashboard : écran Site — fiche, courbe, recommandations, traçabilité (#24)' (#140) from marvin/24-ecran-site into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 6s
Intégration / Aucun secret commité (push) Successful in 3s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 30s
Intégration / Python — qualité, tests et dépendances (push) Successful in 3m22s
ce58c07e36
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/140
Reviewed-by: justine <justine@noreply.10.105.200.41>
Merge branch 'develop' into florian/43-plan-migration-cloud
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 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 29s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m23s
c4e1e61fe5
docs: traite la relecture de Justine sur le plan de migration
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 33s
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) Successful in 3m26s
93ebc76046
Deux corrections de fond. Le §6 chiffrait l'archive sur « 30 j de rétention »
alors que le §5 et l'ADR 0012 disent la politique de cycle de vie refusée :
l'hypothèse de calcul contredisait la conclusion de la même page. La ligne dit
désormais « croissance non purgée », 1,2 Gio par an, un centime de plus par mois
chaque année — le total ne bouge pas d'un ordre de grandeur. Et les 18 Gio/an de
la zone bronze sont extrapolés sur deux jours dont un partiel : ils se
présentaient comme mesurés dans un document bâti sur cette distinction. Ils sont
marqués, et la condition 2 devient un ordre de grandeur, pas une échéance.

Le reste lève des ambiguïtés. Le B2ms portait trois valeurs selon l'endroit —
59,20 € de calcul, 68,27 € avec son disque, et 118 € qui était en fait le B4ms :
le SKU et le périmètre sont nommés partout. Le tarif Blob est rejouable, le
catalogue portant trois compteurs « Cool LRS Data Stored » dont un qui n'est pas
du blob. « plafond_lu_sur_azure » ne coïncide par construction qu'après un
apply : sa description exige maintenant « plan -refresh-only » plutôt que de
laisser croire à un contrôle continu. Et prevent_destroy bloque aussi tout
renommage du conteneur, en erreur dure au plan — le runbook porte la manœuvre.

Le backlog suit enfin le PRD : #43 n'est plus « pas démarré ».
Merge pull request 'infra: publie le plan de migration cloud chiffré et le plafond' (#163) from florian/43-plan-migration-cloud into develop
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 31s
Intégration / Contrôles statiques du dépôt (push) Successful in 6s
Intégration / Aucun secret commité (push) Successful in 3s
Intégration / Python — qualité, tests et dépendances (push) Successful in 4m10s
56ff4c4952
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/163
Reviewed-by: justine <justine@noreply.10.105.200.41>
Merge develop dans la branche de la pile app (#113)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 34s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 4s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m21s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m48s
71dd68319b
florian changed target branch from lenaic/136-zone-or-en-cron to develop 2026-09-07 12:07:49 +00:00
gabriel requested reviews from gabriel and removed review requests for florian 2026-09-07 12:24:21 +00:00
Merge remote-tracking branch 'origin/develop' into lenaic/113-pile-app
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 33s
Intégration / Python — qualité, tests et dépendances (pull_request) Failing after 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 1m27s
139ba2597a
# Conflicts:
#	tests/ci/test-role-app.yml
fix: retire les noqa E402 inutiles dans creer-compte.py (ruff)
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 35s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Python — qualité, tests et dépendances (pull_request) Failing after 48s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 23s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m32s
1cdb01c337
E402 n'est pas activé dans la configuration ruff applicable à db/ et
tests/unit/db/, le noqa est donc superflu et bloquait la chaîne (RUF100).
gabriel approved these changes 2026-09-07 12:45:04 +00:00
Dismissed
Merge branch 'develop' into lenaic/113-pile-app
Some checks failed
Intégration / Python — qualité, tests et dépendances (pull_request) Failing after 36s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 32s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 5s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 23s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m13s
5112e33d1b
fix: applique ruff format sur creer-compte.py et son test
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 8s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 32s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 23s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m23s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m55s
329f6b9be9
gabriel dismissed gabriel's review 2026-09-07 12:50:37 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

gabriel merged commit 0f60b0b894 into develop 2026-09-07 12:54:43 +00:00
gabriel deleted branch lenaic/113-pile-app 2026-09-07 12:54:43 +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!160
No description provided.