Merge remote-tracking branch 'origin/dev' into feat/deploy-rec-prod
# Conflicts: # docs/architecture/00-vue-ensemble.md
This commit is contained in:
@@ -70,10 +70,11 @@ Le lien `front -.-> api` reste en pointillé : le frontend appelle bien une API,
|
||||
intercepteur répond à sa place tant que les endpoints n'existent pas. Voir
|
||||
[30-frontend.md](30-frontend.md).
|
||||
|
||||
Le lien `airflow --> db` est maintenant en trait plein : trois DAGs tournent, deux pour
|
||||
Le lien `airflow --> db` est maintenant en trait plein : quatre 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), cf. plus bas et [20-backend.md](20-backend.md). Le
|
||||
reste du périmètre Airflow envisagé (ingestion, issues #15/#16) reste en pointillé, non construit.
|
||||
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.
|
||||
@@ -88,7 +89,7 @@ collecteur ne vient le lire.
|
||||
| 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](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../ML-START.md). Surveillance de dérive (EC06, #44/#45) pas encore construite |
|
||||
| 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](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). 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. Trois DAGs en sous-processus `uv run` : `ml_train` manuel et `ml_score` `@hourly` pour le pipeline ML (issue #115), `alertes` à `15 * * * *` pour la détection et les recommandations (issue #116, [ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)). L'ingestion (issues #15/#16) n'a pas encore de DAG |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Quatre DAGs en sous-processus `uv run` : `ml_train`, `ml_score`, `alertes` et `historical_import`. 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` | 6 workflows, 18 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. Déploiement continu vers la VM ENI écrit par `deploy.yml`, `dev` en recette et `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), 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](50-cicd.md) |
|
||||
|
||||
## Flux bout en bout
|
||||
|
||||
@@ -52,7 +52,7 @@ Trois pièges sont documentés en tête du `docker-compose.yml`, ils ne se devin
|
||||
- `LocalExecutor` exécute les tâches comme sous-processus du **scheduler**, jamais du webserver :
|
||||
c'est le scheduler qui a besoin du volume `airflow_ml_state` (modèle, magasin MLflow).
|
||||
|
||||
### Airflow (issues #115 et #116)
|
||||
### Airflow (issues #115, #116 et #119)
|
||||
|
||||
Trois services, `docker compose profiles` non utilisés (démarrage explicite via `make
|
||||
airflow-up`, pas dans `make dev`) :
|
||||
@@ -76,6 +76,12 @@ l'[ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md).
|
||||
| `ml_train` | manuelle | `enervision_ml.train`, dans `/opt/ml/.venv` |
|
||||
| `ml_score` | `0 * * * *` | `enervision_ml.score`, dans `/opt/ml/.venv` |
|
||||
| `alertes` | `15 * * * *` | `app.detection.internal_alerts` puis `app.cli generate-recommendations`, dans `/opt/backend/.venv` |
|
||||
| `historical_import` | manuelle | `app.etl.historical_import`, dans `/opt/backend/.venv` ; les fichiers de `data/raw` sont montés en lecture seule dans `/opt/data/raw` |
|
||||
|
||||
Le DAG `historical_import` réutilise le pipeline historique existant sans dupliquer sa logique.
|
||||
Il reste manuel, car le dataset sert à initialiser l'environnement. Le montage
|
||||
`./data/raw:/opt/data/raw:ro` permet au scheduler de lire les fichiers CSV/JSON sans pouvoir les
|
||||
modifier.
|
||||
|
||||
**Pourquoi `alertes` tourne à la quinzième minute.** Sa règle `anomaly` compare une lecture à la
|
||||
`prediction` du même instant, que `ml_score` écrit à l'heure pile. Le décalage laisse le scoring
|
||||
|
||||
@@ -4,39 +4,47 @@ Application Angular 22, 100 % standalone, testée avec Vitest. Source dans `apps
|
||||
|
||||
## État actuel
|
||||
|
||||
Statut : `En cours`. L'application sert une première page métier, le tableau de bord, alimentée
|
||||
par des fixtures : les endpoints qu'elle appelle n'existent pas encore côté API.
|
||||
Statut : `En cours`. L'application sert le tableau de bord, la liste et le détail des sites, la
|
||||
supervision des capteurs (admin) et le flux des alertes actives, tous branchés sur l'API réelle.
|
||||
|
||||
Ce qui est en place :
|
||||
|
||||
- Bootstrap par `bootstrapApplication(App, appConfig)`, **aucun `NgModule`** dans le dépôt.
|
||||
- `app.config.ts` fournit `provideBrowserGlobalErrorListeners()`, `provideRouter(routes)` et
|
||||
`provideHttpClient(withInterceptors([mockApiInterceptor]))`.
|
||||
- Une route `/dashboard` en composant différé, et une redirection depuis la racine.
|
||||
- `core/services` porte `StatsService`, `AlertsService`, `PredictionsService`, `SitesService` et
|
||||
`AuthService`, `core/interceptors` l'intercepteur de fixtures et l'intercepteur d'authentification
|
||||
(jeton porteur, rafraîchissement sur 401), `core/guards` la garde de route `authGuard`,
|
||||
`features/dashboard` la page principale, `shared/components` la jauge de consommation et le
|
||||
graphique de charge par site, tous deux construits sur Chart.js.
|
||||
`provideHttpClient(withInterceptors([authInterceptor, mockApiInterceptor]))`.
|
||||
- Des routes en composants différés (`/dashboard`, `/sites`, `/sites/:siteId`,
|
||||
`/monitoring/sensors` réservée au rôle `admin`) et une redirection depuis la racine.
|
||||
- `core/services` porte un service HTTP par domaine (`StatsService`, `AlertsService` avec ses
|
||||
filtres `site_id` et `severity`, `PredictionsService`, `SitesService`, `ReadingsService`,
|
||||
`SensorsService`, `AuthService`), `core/interceptors` l'intercepteur de fixtures et l'intercepteur
|
||||
d'authentification (jeton porteur, rafraîchissement sur 401), `core/guards` la garde `authGuard`.
|
||||
- `features/` porte une page par domaine. `shared/components` porte la jauge de consommation et
|
||||
les graphiques Chart.js, le widget `app-alert-feed` (flux d'alertes filtrable par site et
|
||||
sévérité, rafraîchi toutes les 60 s, première vue de l'application avec des états chargement /
|
||||
vide / indisponible) et, dans `shared/models`, des types alignés sur les schémas Pydantic du
|
||||
backend, plus les tables de présentation partagées (`alert-presentation.ts` : ton, libellé et
|
||||
unité par sévérité, type et métrique).
|
||||
- Une authentification complète côté interface : connexion, mot de passe oublié/réinitialisation,
|
||||
changement de mot de passe, garde de route sur `/dashboard` et `/sites`. Détail :
|
||||
changement de mot de passe, garde de route sur toute la zone authentifiée. Détail :
|
||||
[31-contrat-authentification.md](31-contrat-authentification.md).
|
||||
- Un système de design partagé (`shared/components/ui/` : `ev-button`, `ev-card`, `ev-alert`,
|
||||
`ev-badge`, `ev-brand`, tokens CSS dans `styles/_tokens.scss`) que toute nouvelle page doit
|
||||
réutiliser plutôt que redéfinir ses propres styles. Détail :
|
||||
`ev-badge`, `ev-brand`, `ev-icon`, tokens CSS dans `styles/_tokens.scss`, classes globales de
|
||||
formulaire, de navigation et de tableau) que toute nouvelle page doit réutiliser plutôt que
|
||||
redéfinir ses propres styles. Détail :
|
||||
[32-design-systeme-frontend.md](32-design-systeme-frontend.md).
|
||||
- L'état vit dans des signaux, sans bibliothèque dédiée.
|
||||
- TypeScript en `"strict": true` ; `strictTemplates` n'est pas encore activé.
|
||||
- Vitest via le builder `@angular/build:unit-test`, couverture activée.
|
||||
- Prettier configuré, parser `angular` pour les gabarits HTML.
|
||||
|
||||
Ce qui n'existe pas encore :
|
||||
|
||||
- **`stats`/`alerts` restent sur fixtures.** `GET /api/v1/stats/summary` et `GET /api/v1/alerts`
|
||||
sont servis par l'intercepteur de fixtures ; l'API expose bien ces routes désormais, mais rien
|
||||
ne bascule `useMockFixtures` à `false` en développement pour les consommer réellement.
|
||||
`GET /api/v1/predictions` fait exception : jamais mocké, branché sur l'API réelle depuis cette
|
||||
PR (voir plus bas).
|
||||
- Aucun état de chargement : tant que la première réponse n'est pas arrivée, la page reste vide.
|
||||
- **Le mode fixtures est inactif.** `useMockFixtures` vaut `false` dans `environment.ts` comme dans
|
||||
`environment.development.ts` : `mockApiInterceptor` ne sert `/stats/summary` et `/alerts` que
|
||||
dans son propre spec. En développement, toutes les pages exigent un backend joignable et un jeton
|
||||
valide.
|
||||
- Un état de chargement généralisé : seul `app-alert-feed` en a un, les autres pages restent vides
|
||||
tant que la première réponse n'est pas arrivée.
|
||||
- Aucun lint : ESLint n'est pas installé.
|
||||
|
||||
## Arborescence
|
||||
@@ -87,11 +95,11 @@ sequenceDiagram
|
||||
```
|
||||
|
||||
`mockApiInterceptor` n'intercepte que `/stats/summary` et `/alerts`, et seulement si
|
||||
`environment.useMockFixtures` est vrai. Le drapeau est à `true` en développement, à `false` en
|
||||
production : toute autre requête, et toutes les requêtes en production, suivent le chemin réel.
|
||||
`/predictions` est volontairement exclu de cette liste (contrairement à `stats`/`alerts`) : il
|
||||
suit toujours le chemin réel, comme `/auth/*` - en développement, ça veut dire qu'un jeton valide
|
||||
et un backend joignable sont nécessaires pour que la section prévisions du dashboard s'affiche.
|
||||
`environment.useMockFixtures` est vrai. Le drapeau vaut `false` dans les deux fichiers
|
||||
d'environnement : en pratique toutes les requêtes suivent le chemin réel et l'intercepteur n'est
|
||||
exercé que par son spec. `/predictions` et `/auth/*` ne sont de toute façon jamais mockés. En
|
||||
développement, un jeton valide et un backend joignable sont donc nécessaires pour que le tableau de
|
||||
bord s'affiche.
|
||||
|
||||
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
|
||||
@@ -146,6 +154,29 @@ Compose.
|
||||
|
||||
Conventions et gabarits : [`apps/frontend/TESTING.md`](../../apps/frontend/TESTING.md).
|
||||
|
||||
## Recommandations
|
||||
|
||||
Statut : `Fait`. La vue `/recommendations` (`features/recommendations`, derrière `authGuard`, tous
|
||||
rôles) présente les recommandations du moteur de règles groupées par alerte, du plus récent au plus
|
||||
ancien, avec le contexte de l'alerte (sévérité, type, site, horodatage, message) puis chaque action,
|
||||
son explication et la règle qui l'a produite.
|
||||
|
||||
- **Jointure côté client.** Une recommandation ne porte que `alert_id`, jamais `site_id`, et
|
||||
`GET /recommendations` n'a aucun filtre. `app-recommendation-list` (`shared/components/`) charge
|
||||
donc en parallèle `GET /alerts` (filtré par `site_id` quand un site est fixé) et
|
||||
`GET /recommendations`, puis les joint par `alert_id` (`joinByAlert`, fonction pure testée à
|
||||
part). Les recommandations dont l'alerte n'est pas dans le jeu chargé sont ignorées : c'est ainsi
|
||||
que le filtre site s'applique. `/alerts` n'étant pas paginé, un seul appel suffit.
|
||||
- **Paramètres d'URL.** `?site=<site_id>` présélectionne le filtre site ; `?alert=<alert_id>`
|
||||
réduit la vue à une alerte et la met en évidence (entier strictement positif, sinon ignoré).
|
||||
- **Génération.** Le bouton « Générer les recommandations » n'apparaît que pour le rôle `admin`
|
||||
(`POST /recommendations/generate?site_id=`, réservé admin côté API) et affiche le bilan renvoyé
|
||||
(créées, déjà présentes, alertes examinées) avant de recharger la liste. La voie normale reste le
|
||||
DAG Airflow `alertes` ([ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)).
|
||||
- **Entrées.** Lien « Recommandations » dans l'en-tête du tableau de bord ; section
|
||||
« Recommandations » sur la vue détail d'un site (liste restreinte au site, lien vers la vue
|
||||
complète préfiltrée).
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
- **Gestion d'état** : les signaux suffisent aujourd'hui, la question se reposera quand plusieurs
|
||||
|
||||
@@ -20,16 +20,18 @@ seule fois dans `src/styles.scss`. Disponibles partout sans import supplémentai
|
||||
| `--color-success` / `-bg`, `--color-warning` / `-bg` / `-text`, `--color-danger` / `-hover` / `-bg` / `-border`, `--color-critical` | États sémantiques (alertes, badges) |
|
||||
| `--color-text-inverse` | Texte sur fond coloré plein (boutons/badges) |
|
||||
| `--font-family` | Police unique de l'application |
|
||||
| `--font-size-xs` à `--font-size-2xl` | Échelle typographique (0.75rem à 2.25rem) : libellés, corps, titres, grands nombres |
|
||||
| `--radius-sm`, `--radius-md`, `--radius-pill` | Rayons de bordure (input/bouton, carte, pastille) |
|
||||
| `--shadow-card` | Ombre portée des cartes |
|
||||
| `--shadow-card`, `--shadow-card-hover` | Ombre portée des cartes, au repos et au survol |
|
||||
| `--space-1` à `--space-5` | Échelle d'espacement (0.35rem à 2.5rem) |
|
||||
|
||||
Les classes de formulaire partagées (`.form-label`, `.form-input`, `.form-hint`) sont dans
|
||||
`apps/frontend/src/styles/_forms.scss`, importées globalement de la même façon. Elles
|
||||
s'appliquent directement à des `<label>`/`<input>` natifs liés par `formControlName` : pas de
|
||||
composant `ControlValueAccessor` dédié, le gain n'en vaut pas la complexité pour des formulaires
|
||||
aussi simples que ceux de ce projet. Les erreurs de formulaire, elles, s'affichent via
|
||||
`<ev-alert severity="danger">`, pas une classe dédiée.
|
||||
Les classes de formulaire partagées (`.form-label`, `.form-input`, `.form-select`, `.form-hint`)
|
||||
sont dans `apps/frontend/src/styles/_forms.scss`, importées globalement de la même façon. Elles
|
||||
s'appliquent directement à des `<label>`/`<input>`/`<select>` natifs, liés par `formControlName` ou
|
||||
par un simple `(change)` : pas de composant `ControlValueAccessor` dédié, le gain n'en vaut pas la
|
||||
complexité pour des formulaires aussi simples que ceux de ce projet. `.form-select` habille un
|
||||
`<select>` natif avec la bordure et le focus de `.form-input`, plus un chevron. Les erreurs de
|
||||
formulaire, elles, s'affichent via `<ev-alert severity="danger">`, pas une classe dédiée.
|
||||
|
||||
La classe `.auth-page` (`apps/frontend/src/styles/_auth-page.scss`, importée globalement) porte
|
||||
le fond dégradé et le centrage commun aux pages d'authentification (`login`, `change-password`,
|
||||
@@ -83,6 +85,23 @@ dans le tableau `imports` du composant qui l'utilise.
|
||||
```html
|
||||
<ev-brand />
|
||||
```
|
||||
- **`<ev-icon>`** (`icon/`) : `name` (obligatoire : `spike` / `threshold` / `anomaly` / `outage` /
|
||||
`sensor`, les types d'alerte du contrat) et `label` (facultatif). SVG inline en trait sur
|
||||
`currentColor`, dimensionné par `font-size` comme `ev-brand`. Sans `label` l'icône est décorative
|
||||
(`aria-hidden`) ; avec, elle porte `role="img"` et `aria-label`. Pas de bibliothèque d'icônes : la
|
||||
CSP du reverse proxy (`script-src 'self'`) interdit les scripts tiers, pas le SVG inline.
|
||||
```html
|
||||
<ev-icon name="spike" label="Pic de consommation" />
|
||||
```
|
||||
- **`<app-alert-feed>`** (`shared/components/alert-feed/`) : widget métier plutôt qu'atome du kit,
|
||||
mais réutilisable tel quel. Input `siteId` (facultatif : fige le site et masque son filtre). Il
|
||||
porte ses filtres (`.form-select`), ses états et son rafraîchissement ; le parent ne fait que le
|
||||
poser dans une section.
|
||||
|
||||
Les classes de tableau partagées sont dans `apps/frontend/src/styles/_tables.scss`, importées
|
||||
globalement : `.ev-table-card` sur la `<ev-card>` qui enveloppe un tableau (padding nul),
|
||||
`.ev-table` sur le `<table>`, `.ev-table__number` pour une cellule numérique en chiffres
|
||||
tabulaires, `.ev-table__muted` pour une cellule sans valeur.
|
||||
|
||||
## Logo
|
||||
|
||||
|
||||
Reference in New Issue
Block a user