`.ev-table-card` et le `:host` de `ev-card`, compilé en `[_nghost-…]`, ont la
même spécificité. Les styles de composant sont injectés dans le head après la
feuille globale : le padding de la carte gagnait, et le tableau de la liste des
sites comme celui des prévisions perdaient leur mise à plat. Le sélecteur
d'élément `ev-card.ev-table-card` tranche, comme `_auth-page.scss` le fait déjà.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La vue détail pose app-recommendation-list restreint au site consulté (les
alertes sont demandées avec site_id) et renvoie vers la vue complète préfiltrée
sur ce site. Le spec fournit les deux services du composant enfant et vérifie le
filtre transmis.
Page derrière authGuard, ouverte à tous les rôles : filtre site (.form-select,
présélectionné par ?site=), focus sur une alerte par ?alert= (entier strictement
positif, sinon ignoré), et pour les admins un bouton « Générer les
recommandations » qui appelle POST /recommendations/generate sur le site filtré,
affiche le bilan accordé en nombre et recharge la liste. Route ajoutée dans
app.routes.ts, lien « Recommandations » dans la navigation du tableau de bord.
Une recommandation ne porte que alert_id et GET /recommendations n'a aucun
filtre : le composant charge en parallèle les alertes (filtrées par site quand
`siteId` est fourni) et toutes les recommandations, puis les joint côté client
(joinByAlert, fonction pure testée à part) en groupes par alerte, du plus récent
au plus ancien. Chaque groupe montre le contexte de l'alerte (sévérité, type,
site, horodatage, message) puis ses actions avec l'explication et la règle.
L'input `alertId` réduit la vue à une alerte et la met en évidence ; `reload()`
rejoue les deux appels. Un échec de l'un des deux vide tout : une demi-jointure
tromperait.
Types alignés sur RecommendationResponse et RecommendationGenerationResponse
du backend. RecommendationsService couvre GET /recommendations,
GET /recommendations/{id} et POST /recommendations/generate?site_id= (réservé
admin côté API). recommendation-presentation.ts traduit les sept règles connues
du moteur et retombe sur la référence brute pour une règle inconnue, la
politique de renommage en -v2 de l'ADR 0006 l'impose.
L'afterEach forçait le drapeau à true alors qu'il vaut false dans les deux
environnements : avec isolate désactivé, tout spec joué ensuite sur
/stats/summary ou /alerts aurait reçu une fixture au lieu d'atteindre
HttpTestingController.
- ligne d'état « Actualisé à HH:mm:ss · N sites suivis » avec un point animé,
figé sous prefers-reduced-motion
- indicateurs en cartes : libellé en capitales, grand nombre en chiffres
tabulaires, filet supérieur, ombre au survol ; la barre de charge moyenne
passe au jaune à 70 % et au rouge à 90 %, avec un texte d'aide
- grille à deux colonnes : graphique de charge et prévisions à gauche, flux
d'alertes en colonne collante à droite, une colonne sous 900 px
- prévisions présentées en tableau (site, prévision, échéance, lien détail)
- liens de navigation de l'en-tête restylés en pastilles, par SCSS seulement :
le bloc HTML de navigation est inchangé octet pour octet
- styles de tableau sortis dans _tables.scss (.ev-table, .ev-table-card), la
liste des sites les adopte au lieu de sa copie locale
Le dashboard ne charge plus les alertes lui-même : le widget porte le flux, ses
filtres, ses états et son rafraîchissement. Disparaissent la liste rouge quelle
que soit la sévérité, le bandeau d'erreur dédié et la copie locale de la table
sévérité vers ton, désormais dans alert-presentation.ts. Le spec passe par un
helper setup() qui fournit aussi les services du widget enfant.
Flux des alertes actives sur GET /alerts : filtres site et sévérité posés en
paramètres de requête (l'API les accepte), couleur et badge par sévérité, icône
par type, nom du site résolu depuis GET /sites, mesure et seuil avec leur unité.
Rafraîchissement par timer(0, 60 s) relancé à chaque changement de filtre, états
chargement / vide / indisponible, pagination côté client par dix car l'API ne
pagine pas. L'input `siteId` fige le site et masque son filtre, pour la vue
détail d'un site.
Cinq tracés (spike, threshold, anomaly, outage, sensor) dessinés en trait sur
currentColor, dimensionnés par font-size comme ev-brand. Décoratif par défaut
(aria-hidden), rôle image et libellé accessible quand `label` est fourni.
Aucune bibliothèque : la CSP du reverse proxy interdit les scripts tiers, pas le
SVG inline.
- tsconfig.json : "strict": true, vérifié sans erreur sur app et specs
- _forms.scss : .form-select, select natif habillé comme .form-input avec un
chevron, documenté dans le design système
- TESTING.md : commande pour jouer un seul fichier ou dossier de specs
Le modèle front portait un alert_id texte, des value/threshold non nullables et
ignorait metric et prediction_id, alors que l'API sérialise un entier, des
flottants nullables et ces deux champs. Toute comparaison avec l'alert_id d'une
recommandation échouait silencieusement.
- alert.model.ts : alert_id number, value/threshold/metric/prediction_id nullables
- alerts.service.ts : getAlerts(filters) pose site_id et severity en HttpParams,
les deux filtres que l'API accepte
- alerts.fixture.ts : réaligné sur le contrat (ids entiers, horodatages UTC,
champs nuls sur outage/sensor, un cas spike)
- alert-presentation.ts : tons, libellés et unités partagés ; low passe en
neutre, le vert se lisait comme un état sain
Compose interpole tout le fichier avant n'importe quelle sous-commande : la garde
`${PUBLIC_HOST:?}` de l'overlay cassait `stack-down` et `stack-logs` autant que le
démarrage. La valeur retombe sur `enervision.local`, et `stack-up` vérifie à la place
que le certificat présent couvre l'hôte demandé, ce qui est la condition réelle à tenir.
La CSP `script-src 'self'` bloquait le gestionnaire `onload` que l'inlining du CSS
critique d'Angular pose sur la feuille de styles : l'application se serait affichée sans
style derrière le proxy. `inlineCritical` passe à faux, le build de production ne produit
plus aucun script en ligne.
La zone de limitation resserrée ne couvre plus que les routes qui vérifient un secret.
Derrière le NAT de l'école, où une seule adresse porte toute la promotion, `/auth/me` et
`/auth/refresh` y auraient produit des 429 en usage normal.
Enfin `certbot/certbot` est épinglé en v5.8.0 pour que Dependabot puisse le suivre, le
proxy attend une API saine plutôt que démarrée, et la redirection vers `$host` est actée
comme risque accepté : figer un nom canonique couperait l'accès par adresse IP, seule
voie ouverte sur la machine cible.
Le schéma backend dit que since est l'horodatage de la dernière lecture du
site, identique pour tous ses capteurs en panne et sans rapport avec le début
de la panne. Le template annonçait « depuis <date> », ce que l'exploitant lit
comme une date de début de panne.
Un site sans aucune lecture renvoie ses cinq capteurs en échec avec since à
null : le template affichait « depuis » suivi d'une chaîne vide. Ce cas dit
maintenant « aucune lecture reçue ».
Format de date explicite plutôt que 'short' : aucune locale n'est enregistrée
dans app.config.ts, donc 'short' rendait la date au format en-US.
Quatre points portant sur le code de cette PR :
- L'échec de chargement laissait à l'écran le site précédemment affiché sous le
bandeau d'erreur : `reportUnavailable()` vide désormais site, mesure et
historique, pour qu'on ne lise pas les chiffres de A en croyant regarder B.
- L'historique était tracé à rebours : l'API trie en timestamp décroissant
(`ReadingRepository.list_history`), le graphique rétablit la chronologie.
- Une consommation `null` (panne capteur) alimentait la jauge avec un 0,
indiscernable d'un site qui ne consomme rien : la jauge n'est plus montée dans
ce cas, la raison de l'absence est affichée à la place. Une consommation
réellement mesurée à 0 continue d'afficher la jauge.
- `getSite` et `getCurrent` ne dépendent pas l'un de l'autre : `forkJoin` économise
un aller-retour en série à chaque ouverture de la page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le cas « site sans mesure » ne passe plus par une réponse nulle mais par
`timestamp` à null : il ne doit alors pas interroger `/readings`, et la vue
annonce l'absence de mesure au lieu de six métriques de cause inconnue.
`/sites/{site_id}/current` renvoie un objet toujours présent, sans `reading_id`
ni `source`, avec `timestamp` nullable et `null_reasons` non nullable. La vue
s'appuyait sur une réponse nulle pour détecter l'absence de mesure : elle
s'appuie désormais sur `timestamp`, et affiche `data_quality`, que le contrat
précédent ne portait pas.
`getCurrent()` rejoint `SitesService`, l'endpoint appartenant à `/sites`.
Le backend de la branche réimplémentait `/sites/{site_id}/current` en renvoyant
`ReadingResponse | None`. La PR #84, mergée entre-temps, l'expose via
`SiteCurrentResponse`. Les cinq conflits sont donc tranchés en faveur de `dev`,
et tout `apps/backend/` est repris à l'identique : la branche redevient
purement frontend.
Sans switchMap sur le flux externe, une réponse HTTP en retard pouvait écraser
l'affichage du site actuellement sélectionné après une navigation rapide entre
deux sites.
Ajoute la page de détail d'un site (fiche, mesure instantanée, jauge de
consommation, historique) en remplacement du placeholder. Les champs
null sont affichés explicitement avec leur raison plutôt que masqués.
Ajoute l'endpoint GET /sites/{id}/current côté backend, qui retourne la
dernière mesure connue d'un site sans filtre temporel, conformément au
contrat de l'issue.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012i5NteMLRZgfTAKB5GD37X
- Reutilise .ev-link pour le lien "Detail" de la liste des sites au
lieu de dupliquer ses regles de style.
- site.location vide est traite comme absent (affiche "-"), pas
seulement null/undefined.
- siteId de la page detail suit desormais route.paramMap de facon
reactive plutot qu'une lecture ponctuelle du snapshot, pour rester
a jour quand Angular reutilise l'instance du composant en changeant
de site.
- Ajoute provideRouter([]) manquant dans un test dashboard existant,
necessaire depuis l'ajout du lien "Voir les sites" au rebase sur dev.
Logo cliquable vers le tableau de bord (ev-brand-link) et fil d'Ariane
(ev-breadcrumb) sur les sous-pages, pour éviter les impasses de
navigation entre dashboard, liste des sites et détail de site.
Nouveau SitesService (GET /sites) et page SiteList consommant le design
système (ev-card, ev-badge, ev-alert, ev-brand). Ajoute la route /sites,
un lien depuis le dashboard, et une route détail /sites/:siteId pointant
vers un placeholder minimal en attendant l'issue #51.
Closes#49
Corrige les 10 points de la revue du systeme de design : garde-fou de
route explicite pour /docs, /redoc et /static, ton distinct pour les
alertes critical vs high, flex-shrink sur le bon element du badge,
mutualisation du bloc ev-card dans _auth-page.scss, bouton de
deconnexion migre vers ev-button (nouvel input fullWidth), tokens
manquants (--color-danger-hover, --color-warning-text,
--color-text-inverse, --color-critical), test de synchronisation des
deux copies du logo, openapi_avec_logo qui enveloppe application.openapi
au lieu de le reimplementer, doc du frontend et index mis a jour, et
suppression du CSS mort .form-error.
Ajoute GET /auth/reset-password/validate (lecture seule, sans rate
limit : le jeton est un secret de 256 bits non brute-forcable) pour que
la page reset-password redirige immediatement vers /login si le lien
est invalide ou expire, plutot que d'attendre la soumission du
formulaire. La verification a la soumission (confirm_password_reset)
reste la seule source de verite atomique.
Remplace l'indice statique sous le champ nouveau mot de passe par une
checklist qui coche chaque regle (longueur, majuscule, minuscule,
chiffre, caractere special) au fur et a mesure de la saisie. Les regles
individuelles (PASSWORD_REQUIREMENTS) sont exposees depuis le meme
validateur que PASSWORD_PATTERN pour rester la seule source de verite.
Avant, un token absent affichait un message inline sur /reset-password, et
un token invalide/expire ne se voyait qu'apres soumission du formulaire.
Les deux cas redirigent maintenant vers /login avec le motif
"lien-expire", qui y affiche le message standard "Ce lien de
reinitialisation est invalide ou a expire. Connectez-vous ou
redemandez-en un."
Le refresh de session lance par provideAppInitializer echoue silencieusement
sans cookie valide, mais l'intercepteur forcait quand meme un
router.navigate(['/login']) sur le 401 resultant, ecrasant la navigation
vers /reset-password?token=... venue de l'email. L'intercepteur ne
redirige plus quand on est deja sur une route invitee (login,
forgot-password, reset-password).
Anti-enumeration cassee sur /auth/forgot-password : l'envoi SMTP etait
synchrone dans le chemin de reponse, donc un email existant prenait plus
de temps qu'un email inconnu (et pouvait renvoyer 500 si le relais SMTP
echouait, contre 202 sinon). L'envoi part desormais en BackgroundTasks,
apres que la reponse 202 a ete envoyee au client, avec un try/except qui
logue plutot que de laisser une exception SMTP remonter.
confirm_password_reset() ne revalidait pas is_active/kind du compte avant
de changer le mot de passe : un compte desactive dans les 15 minutes
suivant l'emission du lien pouvait quand meme voir son mot de passe
change et son must_change_password efface.
Les plages [A-ZA-Y]/[a-za-y] de la regle de complexite incluaient par
erreur x et / (U+00D7, U+00F7), donc un mot de passe sans aucune
majuscule ou minuscule pouvait passer la validation.
Le validateur frontend (JS, \w ASCII) et le validateur backend (Python,
\w Unicode) divergeaient sur les caracteres accentues : un mot de passe
comme "Securite1" passait cote front puis se faisait rejeter en 422 cote
back. Les deux cotes utilisent maintenant le meme jeu explicite de
caracteres speciaux (SPECIAL_CHARACTERS, partage aussi avec cli.py).