Route, paramètres ?site= et ?alert=, jointure côté client et sa raison (pas de site_id sur une recommandation, aucun filtre sur GET /recommendations, /alerts non paginé), génération réservée aux admins, points d'entrée.
178 lines
9.2 KiB
Markdown
178 lines
9.2 KiB
Markdown
# Frontend
|
|
|
|
Application Angular 22, 100 % standalone, testée avec Vitest. Source dans `apps/frontend`.
|
|
|
|
## É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.
|
|
|
|
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.
|
|
- 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 :
|
|
[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 :
|
|
[32-design-systeme-frontend.md](32-design-systeme-frontend.md).
|
|
- L'état vit dans des signaux, sans bibliothèque dédiée.
|
|
- 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.
|
|
- Aucun lint : ESLint n'est pas installé.
|
|
|
|
## Arborescence
|
|
|
|
Statut : `Fait`. Elle suit ce que [`TESTING.md`](../../apps/frontend/TESTING.md) prescrit 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 : `En cours`. Le chemin complet est câblé, mais un intercepteur se place devant et répond
|
|
lui-même tant que les endpoints n'existent pas.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant C as Composant
|
|
participant S as Service Angular
|
|
participant I as mockApiInterceptor
|
|
participant P as ng serve, proxy
|
|
participant A as FastAPI
|
|
|
|
C->>S: appel de méthode
|
|
S->>I: GET /api/v1/...
|
|
alt useMockFixtures actif et route connue
|
|
I-->>S: fixture locale
|
|
else
|
|
I->>P: la requête poursuit
|
|
P->>A: http://localhost:8000/api/v1/...
|
|
A-->>S: JSON
|
|
end
|
|
S-->>C: modèle typé
|
|
```
|
|
|
|
`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.
|
|
|
|
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, `environment.ts` porte lui aussi un `apiUrl` relatif (`/api/v1`) plutôt qu'une URL
|
|
absolue : la dette qui pointait en dur sur `http://localhost:8000/api/v1` a été corrigée. Un build
|
|
de production sert donc l'appel `/api/v1/...` sur son propre origin, et c'est le **reverse proxy**
|
|
qui route `/api` vers le backend : `location /api/` dans `infra/proxy/conf.d/enervision.conf`, voir
|
|
[10-infra.md](10-infra.md) et l'[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.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 |
|
|
|
|
**Version de Node.** L'Angular CLI refuse de démarrer en dessous de 22.22.3, 24.15.0 ou 26.0.0, et
|
|
le message d'erreur arrive avant toute compilation. Un poste en 22.21 ou en 24.12 ne peut donc ni
|
|
tester ni construire le frontend.
|
|
|
|
Le frontend a ses cibles dans le `Makefile` racine (`install-frontend`, `dev-frontend`,
|
|
englobées par `install` et `dev`). En développement il tourne directement via `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.
|
|
|
|
Le service `frontend` du `docker-compose.yml` sert le build statique par le nginx de
|
|
`apps/frontend/Dockerfile`, multi-étage, qui **écoute sur 3000**. En déploiement il n'est plus
|
|
publié du tout : le reverse proxy est seul à sortir sur le réseau, et l'atteint par le réseau
|
|
Compose.
|
|
|
|
## Sécurité
|
|
|
|
- Le frontend ne détient aucun secret : `environment.ts` ne porte qu'une URL.
|
|
- L'authentification existe des deux côtés désormais : `authGuard` protège `/dashboard` et
|
|
`/sites`, `authInterceptor` pose le jeton porteur sur les requêtes sortantes et déclenche le
|
|
rafraîchissement sur 401. Détail complet dans
|
|
[31-contrat-authentification.md](31-contrat-authentification.md).
|
|
- **La CSP posée par le reverse proxy contraint le build.** `script-src 'self'` interdit les
|
|
gestionnaires d'événements en ligne ; l'inlining du CSS critique en produisait un
|
|
(`<link media="print" onload="this.media='all'">`), ce qui aurait laissé l'application sans
|
|
style derrière le proxy. D'où `optimization.styles.inlineCritical: false` dans la configuration
|
|
de production d'`angular.json`. La contrepartie est un rendu non stylé très bref au premier
|
|
affichage. `style-src` conserve `'unsafe-inline'` : Angular injecte les styles de composants à
|
|
l'exécution, et s'en passer demanderait un `ngCspNonce` que le SPA statique ne peut pas produire.
|
|
|
|
## Tests
|
|
|
|
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
|
|
pages partageront le même état.
|
|
- **Comment `apiUrl` est injecté en production** : build par environnement, ou configuration lue
|
|
au démarrage.
|