diff --git a/README.md b/README.md
index 3302c64..20181cc 100644
--- a/README.md
+++ b/README.md
@@ -3,21 +3,36 @@
Monorepo de la plateforme EnerVision : collecte, stockage, analyse et restitution de
series temporelles energetiques, deployee sur une machine on-premise.
+## Jalons
+
+| Jalon | Intitulé |
+|-------|----------------------------------------------------------|
+| J1 | Valider la préparation de l'environnement et du repo |
+| J2 | Valider le périmètre retenu et les choix technologiques |
+| J3 | Valider l'architecture et la gestion de la sécurité |
+| J4 | Valider la robustesse et assurer les livrables |
+
+Ce que la documentation apporte à chacun : [docs/architecture/00-vue-ensemble.md](docs/architecture/00-vue-ensemble.md).
+
## Stack cible
| Domaine | Technologie | Emplacement | Etat |
|------------|-------------------------------------|---------------------|---------------|
| Backend | FastAPI, Python 3.14 | `apps/backend` | Initialise |
-| Frontend | Angular, Node 24 LTS | `apps/frontend` | A initialiser |
+| Frontend | Angular 22, Node 24 LTS | `apps/frontend` | Squelette |
| Base | PostgreSQL 17 + TimescaleDB | `db` | Initialise |
| ETL | Apache Airflow | `etl/airflow` | A initialiser |
| Infra | Terraform (k3s single-node) | `infra/terraform` | Initialise |
| CI/CD | GitHub Actions | `.github/workflows` | A initialiser |
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | A initialiser |
-Le backend, la base et l'infrastructure (Terraform/k3s) sont initialises a ce stade. Les autres dossiers
+Le backend, la base et l'infrastructure (Terraform/k3s) sont initialises a ce stade. Le frontend
+porte le squelette Angular, sans code metier : aucune route, aucun appel d'API. Les autres dossiers
portent l'arborescence et un README de cadrage, leur contenu fait l'objet d'un ticket dedie.
+L'etat detaille de chaque brique et les vues d'architecture sont dans
+[docs/architecture](docs/architecture/README.md).
+
## Arborescence
```
@@ -82,3 +97,4 @@ curl -s localhost:8000/api/v1/health/ready
- Branches : `feat/`, `fix/`, `chore/`, `docs/`, `test/` suivi d'un libelle court.
- Commits : Conventional Commits, portee = dossier de premier niveau concerne.
- Toute decision structurante donne lieu a un ADR dans `docs/adr`.
+- Toute PR qui change un composant met a jour sa vue dans `docs/architecture`, dans la meme PR.
diff --git a/apps/frontend/README.md b/apps/frontend/README.md
index ce9c73c..aeaf788 100644
--- a/apps/frontend/README.md
+++ b/apps/frontend/README.md
@@ -72,7 +72,8 @@ Points à vérifier après toute regénération :
1. Pointer l'API dans `src/environments/` sur `http://localhost:8000/api/v1`.
2. Ajouter le proxy de développement (`proxy.conf.json`) vers le backend.
-3. Vérifier que `npm start` sert bien sur le port 4200 attendu par `docker-compose.yml`.
+3. Vérifier que `npm start` sert bien sur le port 4200, valeur par défaut d'`APP_CORS_ORIGINS`
+ côté backend. Le `docker-compose.yml` n'a aucun service frontend.
4. Ajouter le `Dockerfile` multi-stage (build Angular puis service statique nginx).
## Additional Resources
diff --git a/apps/frontend/TESTING.md b/apps/frontend/TESTING.md
index e23ed7a..d4e92bf 100644
--- a/apps/frontend/TESTING.md
+++ b/apps/frontend/TESTING.md
@@ -27,6 +27,10 @@ it('devrait faire X quand Y', () => {
- Composants avec logique (formulaires, conditions d'affichage) — pas nécessaire pour
un composant 100% template, sans logique
+`core/services/`, `core/guards/` et `core/interceptors/` n'existent pas encore : c'est
+l'arborescence cible, décrite dans
+[docs/architecture/30-frontend.md](../../docs/architecture/30-frontend.md).
+
## Gabarit — tester un service avec appel HTTP
```typescript
import { TestBed } from '@angular/core/testing';
diff --git a/docs/README.md b/docs/README.md
index b4ad74d..938a78a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,4 +1,4 @@
# Documentation
- `adr` : decisions d'architecture, une par fichier, numerotees et immuables.
-- `architecture` : schemas et vues d'ensemble.
+- `architecture` : les vues du systeme. Point d'entree : [architecture/README.md](architecture/README.md).
diff --git a/docs/architecture/.gitkeep b/docs/architecture/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/docs/architecture/00-vue-ensemble.md b/docs/architecture/00-vue-ensemble.md
new file mode 100644
index 0000000..8da3f0f
--- /dev/null
+++ b/docs/architecture/00-vue-ensemble.md
@@ -0,0 +1,136 @@
+# Vue d'ensemble
+
+EnerVision collecte, stocke, analyse et restitue des séries temporelles énergétiques, sur une
+machine on-premise.
+
+## Cadre du projet
+
+Quatre jalons ont été posés à l'ouverture du projet. Ils ont disparu du `README.md` lors de la
+réécriture de l'arborescence (`2670483`) et ne subsistaient que sur `main`. Ils sont repris ici
+parce qu'ils disent ce que le projet doit prouver, et donc à quoi sert chaque décision technique.
+
+| Jalon | Intitulé | Ce que la documentation apporte |
+|---|---|---|
+| J1 | Valider la préparation de l'environnement et du repo | `10-infra.md` décrit la stack du poste de développement et la commande qui la démarre |
+| J2 | Valider le périmètre retenu et les choix technologiques | Les ADR (`../adr/`) portent les choix ; `40-data.md` liste les questions de périmètre encore ouvertes |
+| J3 | Valider l'architecture et la gestion de la sécurité | Les cinq vues, et la section « Sécurité » ci-dessous qui consolide les surfaces exposées |
+| J4 | Valider la robustesse et assurer les livrables | `20-backend.md` et `30-frontend.md` renvoient aux conventions de tests de chaque application |
+
+## Contexte
+
+Statut : `Cible`. Les acteurs et les sources de mesures ne sont pas arrêtés, c'est l'objet du
+jalon J2.
+
+```mermaid
+flowchart LR
+ exploitant["Exploitant
consulte les courbes"]
+ admin["Administrateur
exploite la plateforme"]
+ sources["Sources de mesures
à définir en J2"]
+
+ subgraph systeme["EnerVision"]
+ plateforme["Collecte, stockage,
analyse et restitution
de séries temporelles"]
+ end
+
+ sources -.-> plateforme
+ exploitant -.-> plateforme
+ admin -.-> plateforme
+```
+
+## Conteneurs
+
+Trait plein pour ce qui tourne, pointillé pour ce qui est cible.
+
+```mermaid
+flowchart TB
+ navigateur["Navigateur"]
+
+ subgraph machine["Machine on-premise"]
+ front["Frontend Angular 22
apps/frontend"]
+ api["API FastAPI
apps/backend"]
+ db[("PostgreSQL 17
TimescaleDB")]
+ airflow["Airflow
etl/airflow"]
+ prom["Prometheus"]
+ grafana["Grafana"]
+ end
+
+ navigateur --> front
+ front -.-> api
+ api --> db
+ airflow -.-> db
+ prom -.-> api
+ grafana -.-> db
+ grafana -.-> prom
+```
+
+Le lien `front -.-> api` est en pointillé à dessein : le frontend n'appelle aujourd'hui aucune
+API, `provideHttpClient` n'est pas encore installé. Voir [30-frontend.md](30-frontend.md).
+
+Le lien `prom -.-> api` de même : l'API expose bien `/metrics` au format Prometheus, mais aucun
+collecteur ne vient le lire.
+
+## État de la stack
+
+| Domaine | Technologie | Emplacement | Statut | Ce qui existe réellement |
+|---|---|---|---|---|
+| Backend | FastAPI, Python 3.14 | `apps/backend` | `En cours` | Factory, configuration, journalisation, 2 sondes de santé, `/metrics`. Aucune couche métier |
+| Frontend | Angular 22, Node 24 | `apps/frontend` | `En cours` | Squelette `ng new` standalone, routes vides, aucun service HTTP |
+| Base | PostgreSQL 17 + TimescaleDB | `db` | `Fait` | Bootstrap de l'extension, base de test, chaîne Alembic. Aucune table applicative |
+| 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 |
+
+## Flux bout en bout
+
+Statut : `Cible`. Aucun maillon de cette chaîne n'existe aujourd'hui, à l'exception de la base.
+
+```mermaid
+sequenceDiagram
+ participant S as Source de mesures
+ participant A as Airflow
+ participant T as TimescaleDB
+ participant API as FastAPI
+ participant U as Angular
+
+ S->>A: mesures horodatées
+ A->>T: insertion dans l'hypertable
+ T->>T: rafraîchissement de l'agrégat continu
+ U->>API: GET /api/v1/...
+ API->>T: agrégation sur la fenêtre demandée
+ T-->>API: lignes
+ API-->>U: JSON
+```
+
+## Sécurité
+
+Section rattachée au jalon J3. Le détail par brique est dans chaque document ; voici la vue
+consolidée.
+
+### En place
+
+- **Les secrets n'ont pas de valeur par défaut.** `APP_SECRET_KEY` et `DATABASE_URL` sont requis
+ sans repli : l'application refuse de démarrer si l'un manque, plutôt que de tourner avec une
+ valeur de démonstration. `.env` reste hors dépôt, `.env.example` est versionné.
+- **CORS conditionnel** : le middleware n'est ajouté que si `APP_CORS_ORIGINS` est renseigné.
+- **Documentation interactive fermée en production** : `/docs`, `/redoc` et `/openapi.json` sont
+ désactivés dès que `APP_ENV=prod`.
+- **Conteneur backend non-root**, déclaré dans `apps/backend/Dockerfile`.
+- **Côté infrastructure** : la clé SSH est marquée `sensitive`, le kubeconfig reste en `600/root`
+ sur la machine cible et n'est lu que par `sudo`, `*.tfvars` est ignoré par git sauf les
+ `.example`.
+
+### Absent
+
+- **Aucune authentification ni autorisation.** Les deux endpoints exposés sont publics. Rien
+ n'est encore décidé sur ce point.
+- Pas de TLS, pas de limitation de débit, pas de journalisation des accès, pas de rotation des
+ secrets.
+- Aucune analyse de dépendances ni de conteneur, faute de CI.
+
+## Décisions structurantes
+
+Elles vivent dans `../adr/`, pas ici.
+
+| ADR | Objet |
+|---|---|
+| [0001](../adr/0001-postgresql-timescaledb.md) | PostgreSQL 17 avec l'extension TimescaleDB, et la frontière `db/` vs `alembic/` |
diff --git a/docs/architecture/10-infra.md b/docs/architecture/10-infra.md
new file mode 100644
index 0000000..4e82445
--- /dev/null
+++ b/docs/architecture/10-infra.md
@@ -0,0 +1,132 @@
+# Infrastructure
+
+Deux topologies coexistent et ne servent pas la même chose. Ce document dit laquelle vaut dans
+quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre les deux.
+
+| Topologie | Sert à | Statut |
+|---|---|---|
+| Docker Compose | Développer et recetter sur le poste | `Fait` |
+| k3s single-node | Déployer sur le serveur on-premise | `En cours` |
+
+## Poste de développement
+
+Statut : `Fait`. Défini par `docker-compose.yml`, projet `enervision`.
+
+```mermaid
+flowchart TB
+ subgraph poste["Poste de développement"]
+ ng["ng serve
:4200"]
+ api["uvicorn --reload
:8000"]
+ end
+
+ subgraph compose["docker compose"]
+ back["service backend
image construite depuis apps/backend"]
+ db[("service db
timescale/timescaledb-ha:pg17")]
+ end
+
+ ng -.->|"proxy /api"| api
+ api -->|"hôte :5433 vers conteneur :5432"| db
+ back -->|"réseau interne, db:5432"| db
+```
+
+| Service | Image | Points notables |
+|---|---|---|
+| `db` | `timescale/timescaledb-ha:pg17` | Publié sur **5433** côté hôte, 5432 souvent déjà pris. `healthcheck` `pg_isready`, 12 tentatives, `start_period` 40s |
+| `backend` | Construite depuis `apps/backend` | `depends_on: db, condition: service_healthy`. **N'embarque pas le source** : toute modification impose `docker compose up -d --build backend` |
+
+**La boucle de développement n'utilise pas le service `backend`.** `make db-up` puis `make dev` :
+seule la base tourne en conteneur, l'API tourne sur le poste avec le rechargement à chaud. Le
+service `backend` sert la stack complète et la recette. Les deux occupent le port 8000, ils ne se
+lancent donc pas ensemble.
+
+Deux pièges sont documentés en tête du `docker-compose.yml`, ils ne se devinent pas :
+
+- `PGDATA` vaut `/home/postgres/pgdata/data` pour l'image `-ha`, et non le chemin habituel de
+ l'image `postgres`. Monté ailleurs, le volume ne retient rien, sans le moindre message.
+- `db/init` est monté **fichier par fichier**. Monter le dossier masquerait les scripts d'init de
+ l'image, dont `timescaledb-tune`. Ajouter un fichier dans `db/init/` impose donc une ligne dans
+ le compose. Voir [`db/README.md`](../../db/README.md).
+
+## Cible de déploiement
+
+Statut : `En cours`. Le module `infra/terraform/modules/k3s/` installe le cluster. Il n'a jamais
+été appliqué.
+
+```mermaid
+flowchart LR
+ poste["Poste
terraform apply"]
+ kube["kubeconfig local"]
+
+ subgraph serveur["Serveur on-premise"]
+ k3s["k3s server single-node
Traefik désactivé"]
+ charges["Charges de travail
aucune déclarée"]
+ end
+
+ poste -->|"SSH, get.k3s.io"| k3s
+ k3s -->|"cat /etc/rancher/k3s/k3s.yaml"| kube
+ k3s -.-> charges
+```
+
+### Ce que le Terraform fait
+
+```mermaid
+sequenceDiagram
+ participant TF as terraform apply
+ participant SRV as Serveur on-premise
+ participant L as Poste local
+
+ TF->>SRV: SSH, curl get.k3s.io puis install server
+ TF->>SRV: attend /etc/rancher/k3s/k3s.yaml
+ TF->>SRV: ssh cat k3s.yaml
+ SRV-->>L: kubeconfig, 127.0.0.1 réécrit en ssh_host
+```
+
+### Ce que le Terraform ne fait pas
+
+Il déclare le provider `null` et **lui seul** : ni `kubernetes`, ni `helm`. Aucun namespace,
+aucun déploiement, aucun service, aucun ingress. À l'issue d'un `apply`, on dispose d'un cluster
+vide et d'un kubeconfig, rien de plus.
+
+## Décisions figées
+
+Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de code et des
+`description` de variables, c'est-à-dire qu'ils ne survivaient pas au premier remaniement.
+
+| Décision | Raison | Où elle est appliquée |
+|---|---|---|
+| k3s single-node plutôt que Kubernetes complet | Une seule machine on-premise, pas de plan de contrôle à répartir | `modules/k3s/main.tf` |
+| `k3s_version` obligatoire, valeur vide refusée | Sans épinglage, `get.k3s.io` installe la dernière version à chaque exécution : le déploiement cesse d'être reproductible | `validation` dans `modules/k3s/variables.tf` |
+| Traefik désactivé | Le choix d'ingress reste ouvert, on ne veut pas en subir un par défaut | `k3s_disable_components`, défaut `["traefik"]` |
+| Kubeconfig laissé en `600/root`, lu par `sudo` | `--write-kubeconfig-mode 644` exposerait `cluster-admin` à tout utilisateur local de la machine | Commentaire et `fetch_kubeconfig` dans `modules/k3s/main.tf` |
+| State Terraform en backend `local` | Un seul opérateur, pas d'exécution concurrente, pas de dépendance à un stockage distant | `environments/dev/versions.tf` |
+| `.terraform.lock.hcl` versionné | Fige les versions de provider entre contributeurs et future CI | Commentaire dans `.gitignore` |
+| `*.tfvars` ignoré, `*.tfvars.example` versionné | Les tfvars portent l'adresse du serveur et le chemin de la clé | `.gitignore` |
+| Désinstallation gérée au `destroy` | `k3s-uninstall.sh` en `on_failure = continue` : un serveur injoignable ne bloque pas le `destroy` | `modules/k3s/main.tf` |
+| Deux racines, `dev` et `prod` | Séparation des états et des variables par environnement | `environments/` |
+
+## Ports et noms
+
+| Quoi | Valeur | Remarque |
+|---|---|---|
+| PostgreSQL, côté hôte | `5433` | Redirigé vers 5432 dans le conteneur. 5432 est souvent déjà pris |
+| PostgreSQL, côté réseau Compose | `db:5432` | Nom de service, utilisé par `DATABASE_URL` du service `backend` |
+| API | `8000` | Identique en conteneur et hors conteneur |
+| Frontend, `ng serve` | `4200` | Valeur par défaut d'`APP_CORS_ORIGINS`. Le compose n'a aucun service frontend |
+| SSH du serveur | `22` par défaut | `ssh_port`, redéfinissable |
+| Base applicative | `enervision` | Variable `POSTGRES_DB` |
+| Base de test | `enervision_test` | Créée par `db/init/110-test-database.sql`, nom attendu en dur par `apps/backend/tests/conftest.py` |
+
+## Le trou entre les deux topologies
+
+Rien ne relie aujourd'hui ce qui est construit par Compose et ce qui tournerait sur k3s. Compose
+construit une image backend localement ; k3s ne saurait pas où la trouver. C'est la première
+question à trancher, avant toute ressource Kubernetes.
+
+## Questions ouvertes
+
+- **Quel ingress** remplace Traefik, et qui termine le TLS.
+- **Quel registre d'images**, et comment il est alimenté sans CI.
+- **Quel stockage persistant** côté Kubernetes pour PostgreSQL, et si la base tourne dans le
+ cluster ou à côté.
+- **Quelle stratégie de sauvegarde et de restauration** des données de mesure.
+- **Que devient `environments/prod/`**, aujourd'hui réduit à un `.gitkeep`.
diff --git a/docs/architecture/20-backend.md b/docs/architecture/20-backend.md
new file mode 100644
index 0000000..d975da6
--- /dev/null
+++ b/docs/architecture/20-backend.md
@@ -0,0 +1,163 @@
+# Backend
+
+API FastAPI, Python 3.14, SQLAlchemy asynchrone sur `asyncpg`. Source dans `apps/backend`.
+
+## Couches
+
+La doctrine est posée dans [`apps/backend/README.md`](../../apps/backend/README.md) et
+[`TESTING.md`](../../apps/backend/TESTING.md) : `endpoints` appelle `services`, qui appelle
+`repositories`, qui seuls touchent les `models`. Le sens de dépendance ne s'inverse jamais.
+
+Dans les faits, trois de ces couches sont des dossiers vides.
+
+```mermaid
+flowchart TB
+ ep["endpoints
2 routes"]
+ sc["schemas
2 modèles Pydantic"]
+ sv["services
vide"]
+ rp["repositories
vide"]
+ md["models
vide"]
+ db[("PostgreSQL")]
+
+ ep --> sc
+ ep -.-> sv
+ sv -.-> rp
+ rp -.-> md
+ ep -->|"SQL brut, état actuel"| db
+ rp -.-> db
+```
+
+Le trait plein de `endpoints` vers la base n'est pas une erreur de dessin : `/health/ready`
+exécute aujourd'hui son `SELECT` directement, sans repository. C'est acceptable pour une sonde
+d'infrastructure, qui vérifie la base elle-même et non une donnée métier. Ce raccourci ne doit
+pas servir de modèle au premier endpoint métier.
+
+`app/models/__init__.py` ne contient qu'un avertissement, qui mérite d'être connu avant la
+première migration : tout modèle absent de ce module reste invisible d'un
+`alembic revision --autogenerate`, qui produirait alors un `drop` de sa table.
+
+## Démarrage
+
+Point d'entrée : **une factory**, `uvicorn app.main:create_app --factory`. Aucune configuration
+n'est lue à l'import du module, ce qui rend l'application testable et les migrations
+indépendantes de l'environnement d'exécution.
+
+```mermaid
+sequenceDiagram
+ participant U as uvicorn --factory
+ participant F as create_app
+ participant S as get_settings
+ participant A as FastAPI
+
+ U->>F: create_app()
+ F->>S: Settings depuis .env et variables APP_*
+ S-->>F: resolved
+ F->>F: configure_logging(resolved)
+ F->>A: FastAPI, docs fermés si prod
+ F->>A: CORSMiddleware, seulement si allowed_origins
+ F->>A: Instrumentator, expose /metrics
+ F->>A: include_router, préfixe /api/v1
+ A-->>U: application
+```
+
+**Le `lifespan` n'ouvre aucune connexion.** Au démarrage il journalise le nom, la version et
+l'environnement ; à l'arrêt il libère l'engine. L'engine lui-même est construit paresseusement au
+premier appel de `get_engine()`, mis en cache par `lru_cache`. Conséquence directe : une API qui
+démarre ne prouve rien sur la base, la première connexion réelle a lieu au premier
+`GET /api/v1/health/ready`. C'est ce qui rend cette sonde indispensable.
+
+## Configuration
+
+`Settings` est un `BaseSettings` Pydantic, lu depuis `.env` avec le préfixe `APP_`.
+
+| Variable | Défaut | Rôle |
+|---|---|---|
+| `APP_SECRET_KEY` | **aucun** | Secret applicatif, `SecretStr` |
+| `DATABASE_URL` | **aucun** | Chaîne de connexion, `postgresql+asyncpg://...` |
+| `APP_ENV` | `local` | `local`, `dev`, `staging` ou `prod` |
+| `APP_DEBUG` | `false` | Active aussi l'écho SQL de l'engine |
+| `APP_LOG_LEVEL` | `INFO` | |
+| `APP_CORS_ORIGINS` | `""` | Liste séparée par des virgules. Vide, aucun middleware CORS n'est posé |
+| `APP_API_PREFIX` | `/api/v1` | |
+| `APP_DATABASE_POOL_SIZE` | `5` | |
+| `APP_DATABASE_MAX_OVERFLOW` | `10` | |
+
+Deux pièges :
+
+- **`DATABASE_URL` ne prend pas le préfixe `APP_`.** C'est le seul réglage dans ce cas, par
+ `validation_alias`, pour rester compatible avec la convention d'Alembic et des hébergeurs.
+- **`APP_SECRET_KEY` et `DATABASE_URL` n'ont pas de valeur par défaut.** L'application refuse de
+ démarrer si l'un manque. C'est délibéré : mieux vaut un échec au démarrage qu'un service qui
+ tourne avec un secret de démonstration.
+
+Deux fichiers d'environnement, deux usages : `.env` à la racine alimente `docker-compose.yml`,
+`apps/backend/.env` alimente l'API lancée sur le poste.
+
+## Routes exposées
+
+| Méthode | Chemin | Dans l'OpenAPI | Rôle |
+|---|---|---|---|
+| GET | `/api/v1/health/live` | oui | Le processus répond. Ne touche pas la base |
+| GET | `/api/v1/health/ready` | oui | La base répond **et** l'extension TimescaleDB est chargée |
+| GET | `/metrics` | non | Format Prometheus, exposé par l'instrumentator |
+| GET | `/docs`, `/redoc`, `/openapi.json` | non | Désactivés quand `APP_ENV=prod` |
+
+Aucune route métier n'existe à ce jour.
+
+### `/health/ready`
+
+Cette sonde porte une garde décrite dans l'[ADR 0001](../adr/0001-postgresql-timescaledb.md) : un
+bootstrap de base sauté ne se voit pas au démarrage de l'API, elle le rend visible.
+
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant R as readiness
+ participant E as get_engine
+ participant D as PostgreSQL
+
+ C->>R: GET /api/v1/health/ready
+ R->>E: session, engine créé au premier appel
+ R->>D: SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'
+ alt base injoignable
+ D--xR: SQLAlchemyError ou OSError
+ R-->>C: 503 Base de donnees injoignable
+ else extension absente
+ D-->>R: NULL
+ R-->>C: 503 Extension TimescaleDB absente
+ else
+ D-->>R: version de l'extension
+ R-->>C: 200 status ready
+ end
+```
+
+## Sécurité
+
+Voir la vue consolidée dans [00-vue-ensemble.md](00-vue-ensemble.md). Côté backend :
+
+- **Aucune authentification, aucune autorisation.** Les deux routes sont publiques. Le premier
+ endpoint métier imposera de trancher ce point.
+- Le CORS n'autorise que les origines listées, et n'existe pas si la liste est vide.
+- `/docs`, `/redoc` et `/openapi.json` disparaissent en production.
+- Le conteneur tourne en utilisateur non-root, avec un `HEALTHCHECK` sur `/api/v1/health/live`.
+- Ni limitation de débit, ni journalisation des accès, ni en-têtes de sécurité.
+
+## Observabilité
+
+- Journalisation par `dictConfig` : format console en développement, JSON dès `APP_ENV=prod`.
+ `sqlalchemy.engine` est forcé à `WARNING` pour ne pas noyer les journaux.
+- `/metrics` au format Prometheus. **Aucun collecteur ne le lit** : `monitoring/` est vide.
+
+## Tests
+
+Conventions, gabarits et arborescence : [`apps/backend/TESTING.md`](../../apps/backend/TESTING.md).
+Deux points structurants y sont fixés : les doubles passent par `app.dependency_overrides` et
+jamais par `unittest.mock`, et les tests qui touchent la vraie base portent le marqueur
+`integration`, exclu par défaut.
+
+## Questions ouvertes
+
+- **Authentification et autorisation** : quel mécanisme, quelle granularité.
+- **Pagination et fenêtrage** des lectures de séries temporelles, qui conditionnent la forme des
+ endpoints métier.
+- **Politique de versionnement de l'API** au-delà du préfixe `/api/v1`.
diff --git a/docs/architecture/30-frontend.md b/docs/architecture/30-frontend.md
new file mode 100644
index 0000000..98da40a
--- /dev/null
+++ b/docs/architecture/30-frontend.md
@@ -0,0 +1,114 @@
+# Frontend
+
+Application Angular 22, 100 % standalone, testée avec Vitest. Source dans `apps/frontend`.
+
+## État actuel
+
+Statut : `En cours`. Le projet est un `ng new` intact. Le tableau de la
+[vue d'ensemble](00-vue-ensemble.md) le classe désormais correctement, le `README.md` racine le
+disait encore « à initialiser » alors que le squelette existe depuis `49f4697`.
+
+Ce qui est en place :
+
+- Bootstrap par `bootstrapApplication(App, appConfig)`, **aucun `NgModule`** dans le dépôt.
+- `app.config.ts` fournit `provideBrowserGlobalErrorListeners()` et `provideRouter(routes)`.
+- Vitest via le builder `@angular/build:unit-test`, couverture activée, un fichier de test.
+- Prettier configuré, parser `angular` pour les gabarits HTML.
+
+Ce qui n'existe pas encore :
+
+- `routes` est un tableau vide. Aucune page, aucune navigation.
+- **`provideHttpClient` n'est pas fourni** et `@angular/common/http` n'est importé nulle part :
+ l'application n'appelle aucune API.
+- `app.html` est la page d'accueil Angular par défaut, commentaires de remplacement compris.
+- Aucune bibliothèque de graphiques, aucun kit d'interface, aucune gestion d'état.
+- Aucun lint : ESLint n'est pas installé.
+
+## Arborescence cible
+
+Statut : `Cible`. Elle n'est pas inventée ici : [`TESTING.md`](../../apps/frontend/TESTING.md) la
+prescrit déjà dans ses gabarits de tests.
+
+```mermaid
+flowchart TB
+ subgraph src["src/app"]
+ core["core/
services, guards, interceptors"]
+ features["features/
un dossier par domaine"]
+ shared["shared/
composants réutilisables"]
+ end
+
+ features -.-> core
+ features -.-> shared
+ core -.-> env["environments/
apiUrl"]
+```
+
+Un service HTTP par domaine dans `core/services`, les composants de page dans `features`, et rien
+d'autre que du réutilisable dans `shared`. Les composants n'appellent jamais `HttpClient`
+directement : ils passent par un service, ce qui rend le double de test trivial.
+
+## Flux HTTP
+
+Statut : `Cible`. Le chemin est câblé, rien ne l'emprunte encore.
+
+```mermaid
+sequenceDiagram
+ participant C as Composant
+ participant S as Service Angular
+ participant P as ng serve, proxy
+ participant A as FastAPI
+
+ C->>S: appel de méthode
+ S->>P: GET /api/v1/...
+ P->>A: http://localhost:8000/api/v1/...
+ A-->>S: JSON
+ S-->>C: modèle typé
+```
+
+En développement, `proxy.conf.json` redirige tout `/api` vers `http://localhost:8000`. C'est ce
+qui évite le CORS sur le poste, et c'est pourquoi `environment.development.ts` se contente d'un
+`apiUrl` relatif, `/api/v1`.
+
+En production, il n'y a pas de proxy : `environment.ts` porte une URL absolue. Angular substitue
+le fichier via `fileReplacements`, et la configuration `production` est celle par défaut.
+
+**Dette connue.** `src/environments/environment.ts`, qui est la configuration de production,
+pointe `http://localhost:8000/api/v1` en dur. La valeur est celle du poste de développement :
+telle quelle, un build de production ne joindra jamais l'API. À corriger avant le premier
+déploiement, en même temps que sera tranchée la question de l'ingress dans
+[10-infra.md](10-infra.md).
+
+## Exécution
+
+| Commande | Effet |
+|---|---|
+| `npm ci` | Installe les dépendances. `node_modules/` n'est pas présent par défaut |
+| `npm start` | `ng serve` sur le port 4200, proxy actif |
+| `npm run build` | Build de production |
+| `npm run test` | Vitest en mode observateur |
+| `npm run test:ci` | Vitest en une passe |
+
+Le frontend **n'a pas de cible dans le `Makefile` racine** et **aucun service dans
+`docker-compose.yml`** : il se pilote uniquement par `npm`, depuis `apps/frontend`. Le port 4200
+n'apparaît dans le compose que comme valeur par défaut d'`APP_CORS_ORIGINS`, côté backend.
+
+Un `Dockerfile` frontend existe sur la branche `feat/pipeline-cd`, mais il est mono-étage et sans
+`CMD` : il construit sans rien servir. Le `README.md` de l'application demande un multi-étage
+avec un service statique, il reste à écrire.
+
+## Sécurité
+
+- Le frontend ne détient aucun secret : `environment.ts` ne porte qu'une URL.
+- L'authentification n'existe pas côté API, donc pas de garde ni d'intercepteur de jeton à ce
+ stade. `core/guards` et `core/interceptors` sont prévus pour cela.
+
+## Tests
+
+Conventions et gabarits : [`apps/frontend/TESTING.md`](../../apps/frontend/TESTING.md).
+
+## Questions ouvertes
+
+- **Quelle bibliothèque de graphiques** pour les séries temporelles, et si Grafana en couvre déjà
+ une partie du besoin.
+- **Gestion d'état** : signaux seuls, ou une bibliothèque dédiée.
+- **Comment `apiUrl` est injecté en production** : build par environnement, ou configuration lue
+ au démarrage.
diff --git a/docs/architecture/40-data.md b/docs/architecture/40-data.md
new file mode 100644
index 0000000..2d53844
--- /dev/null
+++ b/docs/architecture/40-data.md
@@ -0,0 +1,143 @@
+# Données
+
+PostgreSQL 17 avec l'extension TimescaleDB. Le choix, ses alternatives et ses conséquences sont
+dans l'[ADR 0001](../adr/0001-postgresql-timescaledb.md), qui fait foi. Ce document décrit le
+système qui en découle.
+
+## Avertissement
+
+**Aucune table applicative n'existe à ce jour.** `Base.metadata` est vide, `app/models/` ne
+contient qu'un commentaire, l'unique révision Alembic ne crée aucune table, et aucune hypertable
+n'a été déclarée. Tout ce qui suit sous le statut `Cible` est une proposition de structure, pas un
+relevé du code. Le modèle sera arrêté au jalon J2.
+
+## Trois emplacements, trois rôles
+
+C'est la règle que l'ADR 0001 existe surtout pour fixer. La confondre coûte cher : un script placé
+au mauvais endroit ne s'exécute jamais, ou s'exécute deux fois.
+
+| Emplacement | Contenu | Quand ça s'exécute |
+|---|---|---|
+| `db/init/` | Extensions, bases annexes | **Une seule fois**, à la première initialisation du conteneur, quand `PGDATA` est vide. Ne rejoue jamais |
+| `db/migrations/` | SQL versionné qui ne découle pas du schéma applicatif : rétention, compression | À la main, aujourd'hui vide |
+| `apps/backend/alembic/` | Le schéma exposé par l'API, et lui seul | `alembic upgrade head`, c'est `Base.metadata` qui fait foi |
+
+Une hypertable relève des deux derniers : **Alembic crée la table, et le `create_hypertable()`
+vit dans la même révision**. Les séparer rendrait le schéma irreproductible depuis un seul
+`alembic upgrade head`.
+
+Détail de `db/init/` et du piège de montage : [`db/README.md`](../../db/README.md).
+
+## Ce qui existe
+
+Statut : `Fait`.
+
+- `db/init/100-extensions.sql` crée l'extension `timescaledb`.
+- `db/init/110-test-database.sql` crée `enervision_test`, dont le nom est attendu en dur par
+ `apps/backend/tests/conftest.py`.
+- Une révision Alembic, `5353c0e4f094`, qui **ne crée aucune table**. Elle établit
+ `alembic_version` et refuse de s'appliquer si l'extension manque :
+
+```sql
+IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') THEN
+ RAISE EXCEPTION 'extension timescaledb absente, voir db/init et db/README.md';
+END IF;
+```
+
+Cette garde forme paire avec le 503 de `/api/v1/health/ready`. Un bootstrap sauté ne se voit pas
+au démarrage de l'API : ces deux gardes le rendent visible tôt, des deux côtés.
+
+## Cycle de vie d'une mesure
+
+Statut : `Cible`. Aucun de ces maillons n'existe.
+
+```mermaid
+flowchart LR
+ src["Source de mesures"] -.-> ing["Ingestion Airflow"]
+ ing -.-> hy[("Hypertable mesure")]
+ hy -.-> agg[("Agrégat continu")]
+ hy -.-> comp["Compression"]
+ hy -.-> ret["Rétention"]
+ agg -.-> api["API FastAPI"]
+ agg -.-> graf["Grafana"]
+```
+
+Les lectures de l'API et de Grafana visent l'agrégat continu, pas la table brute : c'est tout
+l'intérêt de TimescaleDB, et cela doit rester vrai quand les volumes augmenteront.
+
+## Modèle
+
+Statut : `Cible`. Les entités ci-dessous sont des **candidates**, à valider en J2. Elles
+s'appuient sur les gabarits de [`apps/backend/TESTING.md`](../../apps/backend/TESTING.md), qui
+évoquent déjà un modèle `Site`, un `SiteRepository` et un `ConsumptionService` exposant un
+`total_kwh(site_id)`.
+
+```mermaid
+erDiagram
+ SITE ||--o{ POINT_DE_MESURE : porte
+ POINT_DE_MESURE ||--o{ MESURE : produit
+
+ SITE {
+ int id PK
+ string nom
+ }
+ POINT_DE_MESURE {
+ int id PK
+ int site_id FK
+ string libelle
+ string unite
+ }
+ MESURE {
+ timestamptz horodatage PK
+ int point_id PK
+ double valeur
+ }
+```
+
+`MESURE` est la table destinée à devenir une hypertable, partitionnée sur `horodatage`. Sa clé
+primaire doit inclure la colonne de temps : TimescaleDB l'exige, une clé sur le seul identifiant
+de point serait refusée.
+
+## Gabarit de révision créant une hypertable
+
+Conforme à la règle de l'ADR 0001 : table et hypertable dans la même révision.
+
+```python
+def upgrade() -> None:
+ op.create_table(
+ "mesure",
+ sa.Column("horodatage", sa.DateTime(timezone=True), nullable=False),
+ sa.Column("point_id", sa.Integer(), sa.ForeignKey("point_de_mesure.id"), nullable=False),
+ sa.Column("valeur", sa.Float(), nullable=False),
+ sa.PrimaryKeyConstraint("horodatage", "point_id"),
+ )
+ op.execute("SELECT create_hypertable('mesure', by_range('horodatage'))")
+
+
+def downgrade() -> None:
+ op.drop_table("mesure")
+```
+
+`drop_table` suffit au retour arrière : supprimer la table supprime l'hypertable et ses partitions.
+
+## Conventions
+
+- **Noms au singulier**, en minuscules, sans préfixe de table.
+- **Toute colonne de temps en `timestamptz`.** Jamais de `timestamp` nu : une mesure sans fuseau
+ devient ininterprétable dès le premier changement d'heure.
+- **La colonne de partitionnement s'appelle `horodatage`** et entre dans la clé primaire.
+- **Les politiques de rétention et de compression** vont dans `db/migrations/`, pas dans Alembic :
+ elles ne découlent pas du schéma applicatif.
+- **Tout modèle doit être importé dans `app/models/__init__.py`**, sans quoi
+ `alembic revision --autogenerate` ne le voit pas et génère un `drop` de sa table.
+
+## Questions ouvertes
+
+Elles relèvent du jalon J2, « valider le périmètre retenu », et bloquent le modèle définitif.
+
+- **Quelles sources de mesures**, et selon quel protocole elles sont collectées.
+- **Quelle granularité** à l'ingestion : la seconde, la minute, le quart d'heure.
+- **Quels agrégats continus**, et sur quelles fenêtres.
+- **Quelle profondeur de rétention** en données brutes, et à partir de quand on compresse.
+- **Quelles unités** sont manipulées, et si une même table les mélange.
+- **Multi-tenant ou non** : un site appartient-il à un client, et faut-il cloisonner les lectures.
diff --git a/docs/architecture/README.md b/docs/architecture/README.md
new file mode 100644
index 0000000..a23be40
--- /dev/null
+++ b/docs/architecture/README.md
@@ -0,0 +1,58 @@
+# Architecture
+
+Les vues d'architecture d'EnerVision. Un ADR (`../adr/`) **décide** et date une décision
+structurante ; une vue d'architecture **décrit** le système qui en résulte. Quand les deux se
+contredisent, c'est l'ADR qui fait foi et la vue qui est en retard.
+
+## Les documents
+
+| Document | Ce qu'il couvre |
+|---|---|
+| [00-vue-ensemble.md](00-vue-ensemble.md) | Jalons du projet, contexte, conteneurs, sécurité, flux bout en bout |
+| [10-infra.md](10-infra.md) | Poste de développement, cible k3s, décisions figées, ports et noms |
+| [20-backend.md](20-backend.md) | Couches FastAPI, séquence de démarrage, routes, configuration |
+| [30-frontend.md](30-frontend.md) | Angular, arborescence cible, flux HTTP |
+| [40-data.md](40-data.md) | Frontières `db/` et `alembic/`, cycle de vie d'une mesure, modèle |
+
+L'observabilité, la sécurité et la CI/CD n'ont pas de document propre : ce sont des sections des
+cinq ci-dessus, tant que `monitoring/`, `.github/workflows/` 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.
+
+## Conventions
+
+### Mermaid, et rien d'autre
+
+GitHub rend Mermaid nativement dans les fichiers `.md`. Un diagramme est donc du texte : il se
+relit en revue, il se diffe, et il ne se périme pas dans un binaire que plus personne ne sait
+rouvrir six mois plus tard. Aucune image exportée, aucun `.drawio`, aucun `.png`.
+
+### Chaque section porte son statut
+
+Une large part de la stack n'est pas écrite. Une vue qui mélange l'existant et la cible sans le
+dire devient fausse sans prévenir.
+
+| Statut | Sens |
+|---|---|
+| `Fait` | Le code existe et tourne |
+| `En cours` | Commencé, incomplet |
+| `Cible` | Décidé, pas encore écrit |
+
+### Légende des diagrammes
+
+Trait plein pour ce qui tourne, trait pointillé pour ce qui est cible.
+
+```mermaid
+flowchart LR
+ A[Composant en place] --> B[Composant en place]
+ B -.-> C[Composant cible]
+```
+
+## Maintenance
+
+**Toute PR qui change un composant met à jour sa vue dans la même PR.** Une vue qu'on promet de
+mettre à jour plus tard ne l'est jamais.
+
+Une documentation fausse coûte plus cher qu'une documentation absente : on la lit, on la croit, et
+on construit dessus. Si une section ne peut plus être tenue à jour, elle est supprimée plutôt que
+laissée à dériver.