Compare commits

...
Author SHA1 Message Date
Meryemel-gham cf9c707592 docs(docs): ajoute schema de donnees et sa description 2026-09-15 14:52:14 +02:00
Johan LEROYandGitHub dcdee8fc6e Merge pull request #68 from ineszang/docs/architecture
docs: fonder la documentation d'architecture du monorepo
2026-09-15 13:01:33 +02:00
PhyriosandGitHub e47235bd7f Mise à jour des jalons 2026-09-15 12:33:15 +02:00
Johan LEROY 4c72fbbb69 docs: fonde les vues d'architecture du monorepo
Cinq vues Mermaid dans docs/architecture (vue d'ensemble, infra, backend,
frontend, donnees), plus leur index, les conventions de statut et la regle
de maintenance en PR.

Reprend les jalons J1-J4, disparus de dev lors de la reecriture du README
(2670483) et restes seulement sur main : plus rien sur la branche de travail
ne disait ce que le projet doit prouver.

Fige les decisions du module Terraform k3s, qui ne vivaient jusqu'ici que
dans des commentaires de code et des description de variables : version
epinglee obligatoire, Traefik desactive, kubeconfig en 600/root, state local.

Corrige trois affirmations devenues fausses : le frontend classe
"a initialiser" alors que le squelette existe depuis 49f4697, le port 4200
dit attendu par docker-compose.yml qui n'a aucun service frontend, et
l'arborescence core/ prescrite par TESTING.md sans exister.
2026-09-15 11:56:18 +02:00
12 changed files with 815 additions and 4 deletions
+18 -2
View File
@@ -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.
+2 -1
View File
@@ -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
+4
View File
@@ -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';
+1 -1
View File
@@ -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).
View File
+137
View File
@@ -0,0 +1,137 @@
# 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 | Ingestion & backend | `20-backend.md` |
| J4 | Architecture, sécurité & frontend | Les cinq vues, et la section « Sécurité » ci-dessous qui consolide les surfaces exposées |
| J5 | 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<br/>consulte les courbes"]
admin["Administrateur<br/>exploite la plateforme"]
sources["Sources de mesures<br/>à définir en J2"]
subgraph systeme["EnerVision"]
plateforme["Collecte, stockage,<br/>analyse et restitution<br/>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<br/>apps/frontend"]
api["API FastAPI<br/>apps/backend"]
db[("PostgreSQL 17<br/>TimescaleDB")]
airflow["Airflow<br/>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/` |
+132
View File
@@ -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<br/>:4200"]
api["uvicorn --reload<br/>:8000"]
end
subgraph compose["docker compose"]
back["service backend<br/>image construite depuis apps/backend"]
db[("service db<br/>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<br/>terraform apply"]
kube["kubeconfig local"]
subgraph serveur["Serveur on-premise"]
k3s["k3s server single-node<br/>Traefik désactivé"]
charges["Charges de travail<br/>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`.
+163
View File
@@ -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<br/>2 routes"]
sc["schemas<br/>2 modèles Pydantic"]
sv["services<br/>vide"]
rp["repositories<br/>vide"]
md["models<br/>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`.
+114
View File
@@ -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/<br/>services, guards, interceptors"]
features["features/<br/>un dossier par domaine"]
shared["shared/<br/>composants réutilisables"]
end
features -.-> core
features -.-> shared
core -.-> env["environments/<br/>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.
+186
View File
@@ -0,0 +1,186 @@
# 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.
## Modélisation détaillée des données
Cette modélisation prend en compte les fichiers CSV historiques,
leurs métadonnées JSON et les données de l’API Mock.
Elle comprend six tables, depuis le stockage des mesures
jusqu’aux recommandations proposées à l’utilisateur.
### Schéma de données
Le diagramme ci-dessous présente les tables et leurs relations.
Il décrit une structure de conception ; les migrations correspondantes
restent à implémenter.
![Schéma de données EnerVision](images/EnerVision-schema-donnees.png)
*Figure — Modélisation des données EnerVision.*
### Description des tables
Chaque table remplit un rôle précis dans le traitement et l’exploitation
des données.
| Table | Rôle | Origine des informations |
|---|---|---|
| `datasets` | Identifier les jeux historiques, retrouver leurs fichiers et conserver leurs métadonnées | Archive CSV/JSON et informations ajoutées lors de l’import |
| `sites` | Regrouper les informations des sites : identifiant, nom, type et caractéristiques disponibles | CSV et API Mock `/api/v1/sites` |
| `readings` | Stocker les mesures, leur provenance, leur qualité et les éventuelles valeurs imputées | CSV et API Mock `/current` et `/readings` |
| `predictions` | Conserver les prévisions, leur période cible et la référence du modèle utilisé | Traitements ML d’EnerVision |
| `alerts` | Enregistrer les alertes, leur type, leur gravité et leur message | API Mock `/alerts` et détections EnerVision |
| `recommendations` | Proposer des actions et expliquer la règle qui les motive | Règles métier d’EnerVision |
Les anomalies historiques décrites dans les JSON sont conservées
dans `datasets.metadata`. Elles servent à l’analyse des données
et ne sont pas considérées comme des alertes actuelles.
### Relations entre les tables
- Un site possède plusieurs mesures, prévisions et alertes.
- Un jeu de données historique contient plusieurs mesures CSV.
- Les mesures API ne sont pas rattachées à un dataset historique.
- Une alerte peut être associée à une prévision du même site.
- Une alerte peut donner lieu à plusieurs recommandations.
+58
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB