Files
ENI-projet-piscine/docs/architecture/00-vue-ensemble.md
T
Johan LEROY 68239371f6 docs(ml,backend,etl): ordonnance la derive et corrige ce que le depot disait faux
Trois phrases du depot annonçaient une surveillance de derive inexistante, et une quatrieme
disait qu'aucune base PostgreSQL n'etait joignable pour tester le chargement ML. Les quatre
sont maintenant fausses, donc reecrites plutot que laissees en dette.

- ADR 0011 : ou vit le calcul et pourquoi pas dans `ml/`, les deux dedoublonnages qu'impose la
  jointure, et quatre alternatives ecartees avec la contrainte qui les interdit (la metrique
  MLflow n'est pas la meme grandeur, `alert` borne ses valeurs et refuse un site nul, Prometheus
  n'a pas de collecteur, ne rien persister ne repond pas a la question du jury).
- DAG `derive` quotidien, hors du DAG `alertes` : un echec de derive y ferait croire que la
  detection a echoue, et la fenetre de 168 h ne se recalcule pas toutes les heures.
- ML-START : la limite de `--now` est dite au lieu d'etre decouverte en demonstration. Elle ne
  decale que l'instant de reference, pas la fenetre de lecture, donc aucun rattrapage ne peut
  fabriquer de paires prevu/realise sur un jeu fige.
- 50-cicd : pourquoi le job ML installe aussi le backend (le schema n'a qu'une source), et ce
  que coute le filtre de chemins qui l'accompagne.
2026-09-22 14:27:13 +02:00

12 KiB

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.

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.

flowchart TB
  navigateur["Navigateur"]

  subgraph machine["Machine on-premise"]
    proxy["Reverse proxy Nginx<br/>:80 et :443"]
    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 --> proxy
  proxy --> front
  proxy --> api
  front -.-> api
  api --> db
  airflow --> db
  prom -.-> api
  grafana -.-> db
  grafana -.-> prom

Le lien front -.-> api reste en pointillé : le frontend appelle bien une API, mais un intercepteur répond à sa place tant que les endpoints n'existent pas. Voir 30-frontend.md.

Le lien airflow --> db est maintenant en trait plein : cinq DAGs tournent, deux pour l'entraînement et le scoring du modèle ML (issue #115), un pour la détection d'alertes et la génération des recommandations (issue #116), et historical_import pour l'ingestion du dataset historique (issue #119). L'orchestration de l'import API Mock et la réconciliation globale des deux sources restent à compléter dans l'issue #15.

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, contrat OpenAPI versionné, routes sites, alerts, recommendations, stats/summary, readings, sensors/status et predictions en lecture (endpoints → services → repositories → models)
Frontend Angular 22, Node 24 apps/frontend En cours Tableau de bord sur route /dashboard, authentification complète (garde de route, intercepteur de jeton), cinq services HTTP, graphiques Chart.js. stats/alerts sur fixtures, predictions branché sur l'API réelle
Base PostgreSQL 17 + TimescaleDB db Fait Bootstrap de l'extension, base de test, chaîne Alembic. Schéma applicatif créé (site, dataset, reading en hypertable, prediction, alert, recommendation)
ML LightGBM, MLflow ml En cours Pipeline d'entraînement et de scoring (enervision_ml.train/.score, features par lags/moyennes glissantes partagées entre les deux, baseline de persistance saisonnière, suivi MLflow local), exposé en lecture via GET /predictions, orchestré par Airflow (ml_train/ml_score). Voir ADR 0005 et ML-START.md. Surveillance de dérive livrée côté backend (app.monitoring.drift, table drift_report, GET /monitoring/drift, DAG derive), voir ADR 0011
Infra Docker Compose, Nginx, Terraform, k3s single-node infra, docker-compose.prod.yml En cours Reverse proxy et overlay de déploiement écrits et validés, jamais lancés sur le serveur (ADR 0007). Provisionnement de la VM par Terraform, qui installe Docker, prépare les deux environnements et enregistre le runner, jamais appliqué (ADR 0010). Module d'installation k3s 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 En cours Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Cinq DAGs en sous-processus uv run : ml_train, ml_score, alertes, historical_import et derive (quotidien, surveillance de dérive). Le DAG historique orchestre app.etl.historical_import et charge dataset, site et reading. L'orchestration API Mock reste à compléter dans #15
CI/CD GitHub Actions .github/workflows En cours 7 workflows, 19 jobs : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow, formatage et validation du Terraform. Déploiement continu vers la VM ENI écrit par deploy.yml, dev en recette et main en production après approbation (ADR 0009), mais jamais exécuté : la machine n'est pas provisionnée et le runner n'y est pas enregistré. Détail dans 50-cicd.md

Flux bout en bout

Statut : En cours. Le chemin de lecture tourne : base, API et frontend. Le chemin d'ingestion dessiné ci-dessous n'existe pas : les DAGs livrés (ml_train, ml_score, issue #115 ; alertes, issue #116) orchestrent le pipeline ML et la détection d'alertes, pas l'ingestion, qui reste lancée à la main par les scripts d'import (issues #15 et #16).

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

  • Authentification et autorisation. JWT d'accès de 15 minutes, jeton de rafraîchissement opaque en cookie HttpOnly avec rotation et détection de réutilisation, mots de passe en Argon2id, RBAC à trois rôles. Détail dans 20-backend.md, décisions dans les ADR 0002 et 0003.
  • Interdire par défaut. Toute route exige un jeton, sauf quatre exceptions listées dans un fichier de test qui interroge réellement chaque route sans identifiant.
  • Révocation immédiate. Le compte est relu en base à chaque requête : une désactivation ou un changement de rôle prend effet à la requête suivante, pas au bout de 15 minutes.
  • Limitation de débit à fenêtre glissante sur trois clés, évaluée avant le hachage. Pas de verrouillage de compte, qui serait un déni de service trivial.
  • Journal d'audit en ajout seul, garanti par deux déclencheurs PostgreSQL (ADR 0004).
  • Les secrets n'ont pas de valeur par défaut. APP_SECRET_KEY et DATABASE_URL sont requis sans repli, et la configuration refuse de démarrer sur cinq erreurs silencieuses : secret trop court ou laissé à sa valeur d'exemple, debug en production, joker CORS, origines vides hors local, cookie SameSite=None sans Secure.
  • CORS explicite : origines listées, méthodes et en-têtes énumérés, jamais de joker.
  • En-têtes de sécurité posés par l'application (nosniff, DENY, no-referrer) et Cache-Control: no-store sur les routes d'authentification.
  • Caviardage des journaux : jetons, empreintes Argon2, mots de passe et cookies sont expurgés avant écriture.
  • Documentation interactive fermée en préproduction et en production, /metrics derrière un jeton facultatif, sonde de disponibilité qui ne publie plus la version de TimescaleDB.
  • CI backend bloquante : format, lint, typage strict et tests avec seuil de couverture.
  • Conteneur backend non-root, déclaré dans apps/backend/Dockerfile.
  • Terminaison TLS au frontal : un reverse proxy Nginx est le seul service publié, il redirige 80 vers 443, sert le SPA et l'API sous la même origine, pose HSTS et CSP que l'application refuse délibérément de poser, et ajoute une limitation de débit au frontal distincte de celle de l'application. Voir ADR 0007.
  • 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, et assumé

  • Rôles PostgreSQL cantonnés pour l'ETL et le travail d'apprentissage. C'est la vraie frontière pour ces deux consommateurs, qui écrivent en base et non par HTTP. Reporté parce que cela impose une réinitialisation de base à toute l'équipe. Voir l'ADR 0003.
  • REVOKE sur audit_log : les déclencheurs arrêtent les accidents, les privilèges arrêteraient une application compromise. Même raison de report.
  • Portée par site dans l'autorisation : les rôles sont globaux, un opérateur du site A peut agir sur le site B. C'est la limite connue du modèle.
  • Certificat reconnu : aucun nom de domaine public ne résout vers la machine, donc le défi HTTP-01 de Let's Encrypt ne peut pas aboutir. Le certificat servi est auto-signé, le chemin ACME est livré et documenté mais pas exercé.
  • Analyse des images de conteneur dans la CI. Celle des dépendances, elle, est en place (pip-audit, npm audit, Dependabot sur 5 écosystèmes), de même que le SAST Bandit. Voir 50-cicd.md.

Décisions structurantes

Elles vivent dans ../adr/, pas ici.

ADR Objet
0001 PostgreSQL 17 avec l'extension TimescaleDB, et la frontière db/ vs alembic/
0002 Authentification par JWT d'accès et jeton de rafraîchissement opaque
0003 Autorisation RBAC à trois rôles, avec relecture du compte à chaque requête
0004 Journal d'audit en ajout seul, garanti par PostgreSQL
0005 Modèle de prédiction de consommation : LightGBM
0006 Le moteur de règles de recommandation vit dans le backend, pas dans ml/
0007 Terminaison TLS par un reverse proxy Nginx, en Docker Compose
0008 Airflow exécute le code du backend en sous-processus, dans son propre environnement
0009 Deux environnements sur la VM ENI, un projet Compose chacun, déployés par un runner auto-hébergé
0010 Terraform provisionne la machine, GitHub Actions déploie l'application