Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8b8ca7163 | ||
|
|
8efa034f9c | ||
|
|
39b1daea51 |
@@ -3,36 +3,21 @@
|
|||||||
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 22, Node 24 LTS | `apps/frontend` | Squelette |
|
| Frontend | Angular, Node 24 LTS | `apps/frontend` | A initialiser |
|
||||||
| 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. Le frontend
|
Le backend, la base et l'infrastructure (Terraform/k3s) sont initialises a ce stade. Les autres dossiers
|
||||||
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
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -97,4 +82,3 @@ 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.
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# ==================
|
||||||
|
# Étape 1 : Build
|
||||||
|
# ==================
|
||||||
|
|
||||||
|
# Image pour frontend
|
||||||
|
FROM dhi.io/node:24-alpine3.22-dev AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
|
# Installation des dépendances du projet avec npm
|
||||||
|
RUN --mount=type=cache,target=/root/.npm npm ci
|
||||||
|
|
||||||
|
# Copie du code source vers le conteneur
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# Étape 2 : Runner
|
||||||
|
# ==================
|
||||||
|
|
||||||
|
|
||||||
|
FROM dhi.io/nginx:1.28.0-alpine3.21-dev AS runner
|
||||||
|
|
||||||
|
# Copie de la configuration de nginx
|
||||||
|
COPY --chown=nginx:nginx nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Copy the static build output from the build stage to Nginx's default HTML serving directory
|
||||||
|
COPY --chown=nginx:nginx --from=builder /app/dist/*/browser /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Create necessary directories with proper permissions for nginx
|
||||||
|
RUN mkdir -p /var/log/nginx /var/cache/nginx && \
|
||||||
|
chown -R nginx:nginx /var/log/nginx /var/cache/nginx /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Use a non-root user for security best practices
|
||||||
|
USER nginx
|
||||||
|
|
||||||
|
# Frontend : port 3000
|
||||||
|
# Backend : port 8000
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start Nginx directly with custom config
|
||||||
|
ENTRYPOINT ["nginx", "-c", "/etc/nginx/nginx.conf"]
|
||||||
|
CMD ["-g", "daemon off;"]
|
||||||
@@ -72,8 +72,7 @@ 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, valeur par défaut d'`APP_CORS_ORIGINS`
|
3. Vérifier que `npm start` sert bien sur le port 4200 attendu par `docker-compose.yml`.
|
||||||
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,10 +27,6 @@ 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';
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 3000;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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` : les vues du systeme. Point d'entree : [architecture/README.md](architecture/README.md).
|
- `architecture` : schemas et vues d'ensemble.
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
# 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/` |
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# 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.
|
|
||||||
Reference in New Issue
Block a user