Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9c707592 | ||
|
|
dcdee8fc6e | ||
|
|
e47235bd7f | ||
|
|
4c72fbbb69 |
@@ -3,21 +3,36 @@
|
|||||||
Monorepo de la plateforme EnerVision : collecte, stockage, analyse et restitution de
|
Monorepo de la plateforme EnerVision : collecte, stockage, analyse et restitution de
|
||||||
series temporelles energetiques, deployee sur une machine on-premise.
|
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
|
## Stack cible
|
||||||
|
|
||||||
| Domaine | Technologie | Emplacement | Etat |
|
| Domaine | Technologie | Emplacement | Etat |
|
||||||
|------------|-------------------------------------|---------------------|---------------|
|
|------------|-------------------------------------|---------------------|---------------|
|
||||||
| Backend | FastAPI, Python 3.14 | `apps/backend` | Initialise |
|
| 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 |
|
| Base | PostgreSQL 17 + TimescaleDB | `db` | Initialise |
|
||||||
| ETL | Apache Airflow | `etl/airflow` | A initialiser |
|
| ETL | Apache Airflow | `etl/airflow` | A initialiser |
|
||||||
| Infra | Terraform (k3s single-node) | `infra/terraform` | Initialise |
|
| Infra | Terraform (k3s single-node) | `infra/terraform` | Initialise |
|
||||||
| CI/CD | GitHub Actions | `.github/workflows` | A initialiser |
|
| CI/CD | GitHub Actions | `.github/workflows` | A initialiser |
|
||||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | 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.
|
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
|
## Arborescence
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -82,3 +97,4 @@ curl -s localhost:8000/api/v1/health/ready
|
|||||||
- Branches : `feat/`, `fix/`, `chore/`, `docs/`, `test/` suivi d'un libelle court.
|
- Branches : `feat/`, `fix/`, `chore/`, `docs/`, `test/` suivi d'un libelle court.
|
||||||
- Commits : Conventional Commits, portee = dossier de premier niveau concerne.
|
- Commits : Conventional Commits, portee = dossier de premier niveau concerne.
|
||||||
- Toute decision structurante donne lieu a un ADR dans `docs/adr`.
|
- 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.
|
||||||
|
|||||||
@@ -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`.
|
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.
|
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).
|
4. Ajouter le `Dockerfile` multi-stage (build Angular puis service statique nginx).
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ it('devrait faire X quand Y', () => {
|
|||||||
- Composants avec logique (formulaires, conditions d'affichage) — pas nécessaire pour
|
- Composants avec logique (formulaires, conditions d'affichage) — pas nécessaire pour
|
||||||
un composant 100% template, sans logique
|
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
|
## Gabarit — tester un service avec appel HTTP
|
||||||
```typescript
|
```typescript
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
- `adr` : decisions d'architecture, une par fichier, numerotees et immuables.
|
- `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).
|
||||||
|
|||||||
@@ -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/` |
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*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.
|
||||||
@@ -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 |
Reference in New Issue
Block a user