diff --git a/docs/architecture/00-vue-ensemble.md b/docs/architecture/00-vue-ensemble.md
index 2f54762..480ef5d 100644
--- a/docs/architecture/00-vue-ensemble.md
+++ b/docs/architecture/00-vue-ensemble.md
@@ -77,15 +77,17 @@ collecteur ne vient le lire.
| Backend | FastAPI, Python 3.14 | `apps/backend` | `En cours` | Factory, configuration, journalisation, 2 sondes de santé, `/metrics`, contrat OpenAPI versionné, routes `sites`, `alerts`, `recommendations`, `stats/summary`, `readings`, `sensors/status` et `predictions` en lecture (endpoints → services → repositories → models) |
| Frontend | Angular 22, Node 24 | `apps/frontend` | `En cours` | Tableau de bord sur route `/dashboard`, authentification complète (garde de route, intercepteur de jeton), cinq services HTTP, graphiques Chart.js. `stats`/`alerts` sur fixtures, `predictions` branché sur l'API réelle |
| Base | PostgreSQL 17 + TimescaleDB | `db` | `Fait` | Bootstrap de l'extension, base de test, chaîne Alembic. Schéma applicatif créé (`site`, `dataset`, `reading` en hypertable, `prediction`, `alert`, `recommendation`) |
-| ML | LightGBM, MLflow | `ml` | `En cours` | Pipeline d'entraînement et de scoring (`enervision_ml.train`/`.score`, features par lags/moyennes glissantes partagées entre les deux, baseline de persistance saisonnière, suivi MLflow local), exposé en lecture via `GET /predictions`. Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../../ML-START.md). Automatisation (Airflow) et surveillance de dérive (EC06, #44/#45) pas encore construites |
+| ML | LightGBM, MLflow | `ml` | `En cours` | Pipeline d'entraînement et de scoring (`enervision_ml.train`/`.score`, features par lags/moyennes glissantes partagées entre les deux, baseline de persistance saisonnière, suivi MLflow local), exposé en lecture via `GET /predictions`. Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../ML-START.md). Automatisation (Airflow) et surveillance de dérive (EC06, #44/#45) pas encore construites |
| Infra | Terraform, k3s single-node | `infra/terraform` | `En cours` | Module d'installation du cluster. Jamais appliqué, aucune ressource Kubernetes déclarée |
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API |
| ETL | Apache Airflow | `etl/airflow` | `Cible` | Rien |
-| CI/CD | GitHub Actions | `.github/workflows` | `Cible` | Rien |
+| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 4 workflows, 14 jobs : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud. Détail dans [50-cicd.md](50-cicd.md). **Aucun job de déploiement** (#21) |
## Flux bout en bout
-Statut : `Cible`. Aucun maillon de cette chaîne n'existe aujourd'hui, à l'exception de la base.
+Statut : `En cours`. Tout le chemin de lecture existe (base, API, frontend), ainsi que l'ingestion
+par import depuis un CSV historique et depuis l'API Mock. **Le seul maillon absent est
+l'orchestration** : Airflow ne tourne pas, l'ingestion et le scoring sont lancés à la main.
```mermaid
sequenceDiagram
@@ -153,7 +155,9 @@ consolidée.
- **TLS, HSTS et CSP** : ils appartiennent au terminateur TLS, qui n'existe pas encore.
- **Limitation de débit au frontal** : celle de l'application protège les identifiants, pas
l'infrastructure.
-- **Analyse de dépendances et de conteneurs** dans la CI, qui relève du chantier CI/CD.
+- **Analyse des images de conteneur** dans la CI. Celle des dépendances, elle, est en place
+ (`pip-audit`, `npm audit`, Dependabot sur 5 écosystèmes), de même que le SAST Bandit. Voir
+ [50-cicd.md](50-cicd.md).
- **Le fichier `environment.ts` de production** pointe encore sur `http://localhost:8000` en HTTP
simple : dans cet état, le cookie `Secure` ne sera pas posé. Voir
[31-contrat-authentification.md](31-contrat-authentification.md).
diff --git a/docs/architecture/50-cicd.md b/docs/architecture/50-cicd.md
new file mode 100644
index 0000000..deadf87
--- /dev/null
+++ b/docs/architecture/50-cicd.md
@@ -0,0 +1,169 @@
+# Intégration et livraison continues
+
+Ce document décrit la chaîne qui s'exécute entre un `git push` et un merge autorisé : ce qui est
+vérifié, ce qui bloque, et ce qui ne l'est pas.
+
+| Étage | Sert à | Statut |
+|---|---|---|
+| Intégration continue | Interdire le merge d'un code qui casse la qualité, les tests ou la sécurité | `Fait` |
+| Livraison continue | Porter un artefact vérifié jusqu'à la machine de déploiement | `Cible` |
+
+Le **D** de CI/CD n'existe pas encore : aucun job de déploiement, aucune construction d'image
+publiée, aucun environnement GitHub. L'issue #21 le porte. C'est la limite principale de cet
+étage, et elle est nommée ici plutôt que découverte en soutenance.
+
+## Vue d'ensemble
+
+```mermaid
+flowchart TB
+ push["push ou pull_request"]
+
+ subgraph back["Backend · .github/workflows/backend.yml"]
+ bv["verification
ruff, mypy, pytest --cov-fail-under=85"]
+ bi["integration
TimescaleDB réel + alembic upgrade head"]
+ bd["security-audit
uv export | pip-audit"]
+ bs["sast
bandit"]
+ end
+
+ subgraph front["Frontend · frontend.yml"]
+ fb["build
npm ci, npm run build"]
+ ft["test
couverture lcov"]
+ fd["security-audit
npm audit --audit-level=high"]
+ end
+
+ subgraph mlw["ML · ml.yml"]
+ mv["verification
ruff, mypy, pytest"]
+ ms["sast
bandit"]
+ end
+
+ subgraph sq["SonarQube · sonarqube.yml"]
+ sb1["build-front / test-front"]
+ sb2["build-back / test-back"]
+ sscan["sonarqube
quality gate SonarCloud"]
+ end
+
+ push --> bv & bi & bd & bs
+ push --> fb --> ft
+ push --> fd
+ push --> mv & ms
+ push --> sb1 & sb2 --> sscan
+ sscan -.-> cd["deploy
issue #21"]
+```
+
+## Déclenchement
+
+Les quatre workflows se déclenchent sur `push` **et** sur `pull_request`, filtrés par **chemin** :
+`backend.yml` sur `apps/backend/**`, `frontend.yml` sur `apps/frontend/**`, `ml.yml` sur `ml/**`,
+chacun incluant son propre fichier de workflow dans le filtre pour qu'une modification du pipeline
+déclenche le pipeline.
+
+**Piège à connaître** : il n'y a **aucun filtre de branche**. Une branche de travail déclenche la
+CI complète à chaque push, et un merge vers n'importe quelle branche la déclenche aussi. C'est
+délibéré pendant le projet (retour au plus tôt, et la CI tournera sur `main` dès la remontée sans
+rien changer), mais ce serait à borner sur un dépôt à forte fréquence de push.
+
+`backend.yml` et `ml.yml` déclarent en plus un groupe de concurrence par référence git avec
+`cancel-in-progress`, ce qui annule un run devenu obsolète par un push plus récent.
+
+## Ce qui bloque un merge
+
+| Gate | Où | Seuil | Effet d'un échec |
+|---|---|---|---|
+| Formatage `ruff format --check` | backend, ml | zéro écart | Bloque |
+| Analyse statique `ruff check` | backend, ml | zéro constat | Bloque |
+| Typage `mypy` | backend (`app`), ml (strict) | zéro erreur | Bloque |
+| Tests unitaires `pytest` | backend, ml | **`--cov-fail-under=85`** côté backend | Bloque |
+| Tests d'intégration | backend | marqueur `integration`, base réelle | Bloque |
+| Audit de dépendances `pip-audit` | backend | sur le **verrou figé** | Bloque |
+| Audit de dépendances `npm audit` | frontend | `--audit-level=high` | Bloque |
+| **SAST `bandit`** | backend (`app`), ml (`enervision_ml`) | **MEDIUM et au-dessus** | Bloque |
+| Quality gate SonarCloud | tout le dépôt | gate par défaut, couverture du **code neuf** | Bloque |
+| Build `npm run build` | frontend | compilation | Bloque |
+
+Deux seuils portent une décision qu'il faut savoir défendre :
+
+- **`npm audit --audit-level=high`** et non `moderate` : une vulnérabilité modérée dans une
+ dépendance de développement ne doit pas immobiliser une livraison. Le corollaire est que les
+ `moderate` sont invisibles en CI, et qu'elles se regardent à la main.
+- **Bandit bloque à partir de MEDIUM**, et une seconde passe sans seuil publie les constats LOW
+ sans bloquer. Sans cette seconde passe, un constat LOW disparaîtrait du journal sans trace. Au
+ 21/09/2026, les deux modules sont à **zéro constat, tous niveaux confondus**, sur 5 904 lignes
+ analysées.
+
+## Le job d'intégration, et pourquoi il ne suffisait pas d'un `postgres`
+
+`backend.yml` monte un service `timescale/timescaledb-ha:pg17`, **la même image que
+`docker-compose.yml`**, et non une image `postgres` nue. La première migration s'arrête
+volontairement si l'extension TimescaleDB manque : un écart d'image entre la CI et le poste
+rendrait ce job vert sur une base qui n'est pas la nôtre.
+
+Sur le poste, c'est `db/init/110-test-database.sql` qui pose l'extension. Ce fichier n'est pas
+monté dans le service GitHub Actions, d'où l'étape `CREATE EXTENSION IF NOT EXISTS timescaledb`
+avant `alembic upgrade head`.
+
+La couverture est **désactivée** sur ce job (`pytest -m integration --no-cov`) : il ne joue qu'une
+partie de la suite, et son taux n'aurait aucun sens face au seuil de 85 %.
+
+## SonarCloud, et l'incident qui a immobilisé trois PR
+
+Le workflow `sonarqube.yml` exécute quatre jobs de préparation (`build-front`, `test-front`,
+`build-back`, `test-back`) qui produisent chacun un rapport de couverture en artefact, puis un
+cinquième job qui les télécharge et lance `SonarSource/sonarqube-scan-action@v8` avec le secret
+`SONAR_TOKEN`. Le périmètre est décrit par `sonar-project.properties` à la racine.
+
+**L'incident, à raconter tel quel.** Les 18 et 19 septembre, trois PR (#103, #105, #107) sont
+restées bloquées sur une quality gate rouge annonçant une couverture du code neuf à 0 %, alors que
+la couverture globale du backend dépassait 87 %. Le diagnostic était **hors du code de ces PR** :
+`sonar.test.inclusions` ne reconnaissait que les fichiers `test_*.py`, si bien que
+`tests/api/acces.py`, `tests/factories.py` et les `__init__.py` du dossier de tests étaient
+comptés comme **code de production non couvert**. Le motif `tests` sans joker ne désignait par
+ailleurs que la racine.
+
+Deux commits ont corrigé la configuration (`9e6a5c0` classe tout `apps/backend/tests` comme test,
+`af2b8cb` déclenche l'analyse quand `sonar-project.properties` change). La gate est verte sur
+toutes les PR depuis. Ce qui compte pour la suite : **la cause a été traitée en configuration, pas
+contournée** en désactivant la gate ou en excluant les fichiers gênants.
+
+## Dependabot
+
+`.github/dependabot.yml` déclare **cinq entrées hebdomadaires groupées** : `npm` sur
+`/apps/frontend`, `uv` sur `/apps/backend`, `github-actions` sur `/`, et `docker` sur les deux
+dossiers d'application. Les mises à jour arrivent en PR, donc elles traversent les mêmes gates que
+n'importe quel changement : une montée de version qui casse les tests ne se merge pas.
+
+## Stratégie de branche et conventions
+
+| Règle | Détail |
+|---|---|
+| Préfixes de branche | `feat/`, `fix/`, `chore/`, `docs/`, `test/` |
+| Messages de commit | Conventional Commits |
+| Branche d'intégration | `dev` ; `main` est la branche par défaut du dépôt public |
+| Revue | Toute PR passe par une revue écrite avant merge |
+| ADR | Toute décision structurante porte son ADR dans la même PR |
+| Vues d'architecture | Toute PR qui change un composant met à jour sa vue **dans la même PR** |
+
+## Secrets
+
+Un seul secret est consommé par la CI : **`SONAR_TOKEN`**, porté par les dépôts GitHub Actions.
+Les identifiants de la base du job d'intégration sont des valeurs de test en clair dans le
+workflow, ce qui est volontaire : elles ne protègent rien, la base est créée et détruite avec le
+run. Aucune clé de déploiement n'existe encore, puisqu'il n'y a pas de déploiement (issue #22).
+
+## Ce qui manque, et pourquoi
+
+| Manque | Issue | Conséquence assumée |
+|---|---|---|
+| Job de déploiement (CD) | #21 | La chaîne s'arrête au merge. Rien ne part vers une machine |
+| DAST (OWASP ZAP) | #41 | Aucune vérification sur l'application en fonctionnement, seulement sur le code et les dépendances |
+| Tests end to end | #46 | Les parcours utilisateur ne sont pas vérifiés en CI |
+| Tests de charge | #47 | Aucun garde-fou de performance |
+| Scan d'image de conteneur | aucune | Les `Dockerfile` sont construits en local, pas analysés |
+
+## Reproduire la CI en local
+
+`make check` enchaîne formatage, analyse statique, typage et tests du backend, c'est à dire le job
+`verification`. `make ml-check` fait la même chose pour le module ML. Les tests d'intégration
+demandent une base : `make db-up` puis `uv run pytest -m integration`.
+
+Le SAST se rejoue à l'identique : `uvx bandit --recursive app --severity-level medium
+--confidence-level medium` depuis `apps/backend`.
diff --git a/docs/architecture/README.md b/docs/architecture/README.md
index 78a82c9..b31b71a 100644
--- a/docs/architecture/README.md
+++ b/docs/architecture/README.md
@@ -15,10 +15,12 @@ contredisent, c'est l'ADR qui fait foi et la vue qui est en retard.
| [31-contrat-authentification.md](31-contrat-authentification.md) | Ce que le frontend doit savoir pour coder la connexion |
| [32-design-systeme-frontend.md](32-design-systeme-frontend.md) | Tokens CSS, composants `ev-*` partagés, règle anti-couleur-en-dur |
| [40-data.md](40-data.md) | Frontières `db/` et `alembic/`, cycle de vie d'une mesure, modèle |
+| [50-cicd.md](50-cicd.md) | Workflows, gates bloquantes, SonarCloud, Dependabot, ce qui manque |
-L'observabilité et la CI/CD n'ont pas de document propre : ce sont des sections des documents
-ci-dessus, tant que `monitoring/` et `etl/airflow/` ne contiennent que des `.gitkeep`. Elles en
-sortiront le jour où elles auront de la matière. Un fichier vide de plus n'aide personne.
+La CI/CD a désormais son document : quatre workflows et quatorze jobs, c'est assez de matière pour
+qu'une section de plus dans une autre vue devienne illisible. L'observabilité, elle, n'en a
+toujours pas : `monitoring/` ne contient que des `.gitkeep`. Elle en sortira le jour où elle aura
+de la matière. Un fichier vide de plus n'aide personne.
La sécurité applicative, elle, a désormais de la matière : la vue consolidée reste dans
[00-vue-ensemble.md](00-vue-ensemble.md), le détail dans [20-backend.md](20-backend.md), la