From 85cb7c9eebac19b03ca1a497d949326c9b5241eb Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Thu, 17 Sep 2026 15:53:55 +0200 Subject: [PATCH] fix(frontend,backend): traite la revue de phyri0s sur la PR #93 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. --- apps/backend/app/main.py | 17 ++------ .../tests/api/test_route_protection.py | 15 +++++++ apps/backend/tests/test_static_assets.py | 13 ++++++ .../auth/change-password/change-password.html | 4 +- .../auth/change-password/change-password.scss | 39 ----------------- .../auth/change-password/change-password.ts | 4 +- .../src/app/features/auth/login/login.scss | 39 ----------------- .../src/app/features/auth/login/login.ts | 5 ++- .../src/app/features/dashboard/dashboard.html | 4 +- .../src/app/features/dashboard/dashboard.scss | 15 ------- .../app/features/dashboard/dashboard.spec.ts | 24 +++++++++++ .../src/app/features/dashboard/dashboard.ts | 5 ++- .../app/shared/components/ui/alert/alert.scss | 2 +- .../app/shared/components/ui/badge/badge.scss | 12 +++++- .../app/shared/components/ui/badge/badge.ts | 2 +- .../shared/components/ui/button/button.html | 8 +++- .../shared/components/ui/button/button.scss | 10 +++-- .../app/shared/components/ui/button/button.ts | 1 + apps/frontend/src/styles/_auth-page.scss | 43 ++++++++++++++++++- apps/frontend/src/styles/_forms.scss | 6 --- apps/frontend/src/styles/_tokens.scss | 4 ++ docs/architecture/30-frontend.md | 4 ++ .../32-design-systeme-frontend.md | 21 ++++++--- docs/architecture/README.md | 1 + 24 files changed, 164 insertions(+), 134 deletions(-) create mode 100644 apps/backend/tests/test_static_assets.py diff --git a/apps/backend/app/main.py b/apps/backend/app/main.py index 5a8ec3e..1380a84 100644 --- a/apps/backend/app/main.py +++ b/apps/backend/app/main.py @@ -5,7 +5,6 @@ from pathlib import Path from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html -from fastapi.openapi.utils import get_openapi from fastapi.staticfiles import StaticFiles from prometheus_fastapi_instrumentator import Instrumentator from starlette.requests import Request @@ -61,20 +60,12 @@ def create_app(settings: Settings | None = None) -> FastAPI: # ReDoc supporte nativement `info.x-logo` (extension Redocly) pour afficher un logo # en en-tête ; Swagger UI n'a pas d'equivalent, il ne reprend que le favicon. + openapi_original = application.openapi + def openapi_avec_logo() -> dict[str, object]: - if application.openapi_schema: - return application.openapi_schema - schema = get_openapi( - title=application.title, - version=application.version, - summary=application.summary, - description=application.description, - routes=application.routes, - tags=application.openapi_tags, - ) + schema = openapi_original() schema["info"]["x-logo"] = {"url": LOGO_URL, "altText": "EnerVision"} - application.openapi_schema = schema - return application.openapi_schema + return schema application.openapi = openapi_avec_logo # type: ignore[method-assign] diff --git a/apps/backend/tests/api/test_route_protection.py b/apps/backend/tests/api/test_route_protection.py index 734c9db..25e4760 100644 --- a/apps/backend/tests/api/test_route_protection.py +++ b/apps/backend/tests/api/test_route_protection.py @@ -81,3 +81,18 @@ async def test_the_declared_routes_are_actually_reachable(app: FastAPI) -> None: ) def test_the_health_probes_stay_public(app: FastAPI, chemin: str) -> None: assert ("GET", chemin) in ROUTES_PUBLIQUES + + +# Piège : ni les routes `include_in_schema=False` (/docs, /redoc) ni un `Mount` Starlette +# (/static) n'apparaissent dans `app.openapi()["paths"]`. `routes_declarees()` ne les voit +# donc jamais, et elles échapperaient silencieusement au garde-fou ci-dessus. +@pytest.mark.parametrize( + "chemin", + ["/docs", "/redoc", "/static/logo-icon.png"], + ids=["swagger_ui", "redoc", "logo_statique"], +) +async def test_the_documentation_routes_are_public_by_design( + app: FastAPI, client: AsyncClient, chemin: str +) -> None: + response = await client.get(chemin) + assert response.status_code == 200 diff --git a/apps/backend/tests/test_static_assets.py b/apps/backend/tests/test_static_assets.py new file mode 100644 index 0000000..125ecda --- /dev/null +++ b/apps/backend/tests/test_static_assets.py @@ -0,0 +1,13 @@ +# Piège : le logo est committé indépendamment à deux endroits (`app/static/`, servi par +# `/docs`/`/redoc`, et `apps/frontend/public/`, servi au front) faute d'étape de build partagée. +# Sans ce test, une mise à jour d'un seul des deux fichiers dérive silencieusement : rien en CI +# ne le détecte. + +from pathlib import Path + +BACKEND_LOGO = Path(__file__).parent.parent / "app" / "static" / "logo-icon.png" +FRONTEND_LOGO = Path(__file__).parent.parent.parent / "frontend" / "public" / "logo-icon.png" + + +def test_the_backend_logo_stays_in_sync_with_the_frontend_one() -> None: + assert BACKEND_LOGO.read_bytes() == FRONTEND_LOGO.read_bytes() diff --git a/apps/frontend/src/app/features/auth/change-password/change-password.html b/apps/frontend/src/app/features/auth/change-password/change-password.html index b589da8..6c3de32 100644 --- a/apps/frontend/src/app/features/auth/change-password/change-password.html +++ b/apps/frontend/src/app/features/auth/change-password/change-password.html @@ -3,7 +3,9 @@

Nouveau mot de passe

-

Votre mot de passe est provisoire, vous devez le modifier avant de continuer

+

+ Votre mot de passe est provisoire, vous devez le modifier avant de continuer +

{ this.isLoading.set(false); - this.errorMessage.set(`Mot de passe actuel incorrect, ou nouveau mot de passe invalide (${this.passwordHint}).`); + this.errorMessage.set( + `Mot de passe actuel incorrect, ou nouveau mot de passe invalide (${this.passwordHint}).`, + ); }, }); } diff --git a/apps/frontend/src/app/features/auth/login/login.scss b/apps/frontend/src/app/features/auth/login/login.scss index b008e3e..f0ffb17 100644 --- a/apps/frontend/src/app/features/auth/login/login.scss +++ b/apps/frontend/src/app/features/auth/login/login.scss @@ -1,42 +1,3 @@ -ev-card { - padding: 3rem 2.5rem; - box-shadow: - 0 20px 25px -5px rgba(0, 0, 0, 0.06), - 0 8px 10px -6px rgba(0, 0, 0, 0.04); -} - -.auth-brand { - justify-content: center; - width: 100%; - font-size: 2.1rem; - margin-bottom: 1.75rem; -} - -h1 { - margin: 0; - font-size: 1.85rem; - font-weight: 700; - color: var(--color-text); - text-align: center; -} - -.auth-subtitle { - margin: 0.4rem 0 2rem; - color: var(--color-text-muted); - font-size: 0.95rem; - text-align: center; -} - -ev-alert { - display: block; - margin-top: 0.75rem; -} - -ev-button { - display: block; - margin-top: 1.75rem; -} - .auth-link { margin-top: 1rem; font-size: 0.85rem; diff --git a/apps/frontend/src/app/features/auth/login/login.ts b/apps/frontend/src/app/features/auth/login/login.ts index bc230a8..22fbe8d 100644 --- a/apps/frontend/src/app/features/auth/login/login.ts +++ b/apps/frontend/src/app/features/auth/login/login.ts @@ -7,7 +7,10 @@ import { Button } from '../../../shared/components/ui/button/button'; import { Card } from '../../../shared/components/ui/card/card'; import { Alert } from '../../../shared/components/ui/alert/alert'; import { Brand } from '../../../shared/components/ui/brand/brand'; -import { MESSAGE_LIEN_RESET_INVALIDE, MOTIF_LIEN_RESET_INVALIDE } from '../../../shared/models/auth-redirect-reason'; +import { + MESSAGE_LIEN_RESET_INVALIDE, + MOTIF_LIEN_RESET_INVALIDE, +} from '../../../shared/models/auth-redirect-reason'; @Component({ selector: 'app-login', diff --git a/apps/frontend/src/app/features/dashboard/dashboard.html b/apps/frontend/src/app/features/dashboard/dashboard.html index 40dd995..6519fc9 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.html +++ b/apps/frontend/src/app/features/dashboard/dashboard.html @@ -7,7 +7,9 @@

Consommation instantanée du parc

- + Déconnexion @if (error(); as message) { diff --git a/apps/frontend/src/app/features/dashboard/dashboard.scss b/apps/frontend/src/app/features/dashboard/dashboard.scss index 30b8788..715ce8c 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.scss +++ b/apps/frontend/src/app/features/dashboard/dashboard.scss @@ -115,18 +115,3 @@ h2 { .alert-item__message { font-size: 0.9rem; } - -.logout-button { - padding: 0.5rem 1rem; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - font-size: 0.85rem; - font-weight: 600; - color: var(--color-label); - cursor: pointer; - - &:hover { - background: var(--color-bg); - } -} diff --git a/apps/frontend/src/app/features/dashboard/dashboard.spec.ts b/apps/frontend/src/app/features/dashboard/dashboard.spec.ts index 5030b0d..4254945 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.spec.ts +++ b/apps/frontend/src/app/features/dashboard/dashboard.spec.ts @@ -148,4 +148,28 @@ describe('Dashboard', () => { expect(authMock.clearSession).toHaveBeenCalled(); expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); }); + + it('distingue le ton des sévérités high et critical', () => { + const statsMock = { getSummary: vi.fn().mockReturnValue(of({ total_sites: 7, sites: [] })) }; + const alertsMock = { getAlerts: vi.fn().mockReturnValue(of([])) }; + + TestBed.configureTestingModule({ + imports: [Dashboard], + providers: [ + { provide: StatsService, useValue: statsMock }, + { provide: AlertsService, useValue: alertsMock }, + ], + }); + + const fixture = TestBed.createComponent(Dashboard); + const dashboard = fixture.componentInstance; + + expect(dashboard.badgeToneForSeverity('low')).toBe('success'); + expect(dashboard.badgeToneForSeverity('medium')).toBe('warning'); + expect(dashboard.badgeToneForSeverity('high')).toBe('danger'); + expect(dashboard.badgeToneForSeverity('critical')).toBe('critical'); + expect(dashboard.badgeToneForSeverity('high')).not.toBe( + dashboard.badgeToneForSeverity('critical'), + ); + }); }); diff --git a/apps/frontend/src/app/features/dashboard/dashboard.ts b/apps/frontend/src/app/features/dashboard/dashboard.ts index 57160ab..b2137a1 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.ts +++ b/apps/frontend/src/app/features/dashboard/dashboard.ts @@ -14,6 +14,7 @@ import { Card } from '../../shared/components/ui/card/card'; import { Alert as EvAlert } from '../../shared/components/ui/alert/alert'; import { Badge, BadgeTone } from '../../shared/components/ui/badge/badge'; import { Brand } from '../../shared/components/ui/brand/brand'; +import { Button } from '../../shared/components/ui/button/button'; const REFRESH_INTERVAL_MS = 10000; const UNAVAILABLE_MESSAGE = @@ -23,13 +24,13 @@ const TON_PAR_SEVERITE: Record = { low: 'success', medium: 'warning', high: 'danger', - critical: 'danger', + critical: 'critical', }; @Component({ selector: 'app-dashboard', standalone: true, - imports: [DecimalPipe, ConsumptionGauge, SiteLoadChart, Card, EvAlert, Badge, Brand], + imports: [DecimalPipe, ConsumptionGauge, SiteLoadChart, Card, EvAlert, Badge, Brand, Button], templateUrl: './dashboard.html', styleUrl: './dashboard.scss', }) diff --git a/apps/frontend/src/app/shared/components/ui/alert/alert.scss b/apps/frontend/src/app/shared/components/ui/alert/alert.scss index 667f779..21e7d6a 100644 --- a/apps/frontend/src/app/shared/components/ui/alert/alert.scss +++ b/apps/frontend/src/app/shared/components/ui/alert/alert.scss @@ -17,7 +17,7 @@ :host.ev-alert--warning { background: var(--color-warning-bg); border-color: var(--color-warning); - color: #92400e; + color: var(--color-warning-text); } :host.ev-alert--danger { diff --git a/apps/frontend/src/app/shared/components/ui/badge/badge.scss b/apps/frontend/src/app/shared/components/ui/badge/badge.scss index 212281f..00c7b7d 100644 --- a/apps/frontend/src/app/shared/components/ui/badge/badge.scss +++ b/apps/frontend/src/app/shared/components/ui/badge/badge.scss @@ -1,3 +1,8 @@ +:host { + display: inline-flex; + flex-shrink: 0; +} + .ev-badge { display: inline-block; font-size: 0.7rem; @@ -6,8 +11,7 @@ letter-spacing: 0.02em; padding: 0.2rem 0.55rem; border-radius: var(--radius-pill); - color: #fff; - flex-shrink: 0; + color: var(--color-text-inverse); } .ev-badge--success { @@ -22,6 +26,10 @@ background: var(--color-danger); } +.ev-badge--critical { + background: var(--color-critical); +} + .ev-badge--neutral { background: var(--color-text-muted); } diff --git a/apps/frontend/src/app/shared/components/ui/badge/badge.ts b/apps/frontend/src/app/shared/components/ui/badge/badge.ts index e9cc0ca..e08ac82 100644 --- a/apps/frontend/src/app/shared/components/ui/badge/badge.ts +++ b/apps/frontend/src/app/shared/components/ui/badge/badge.ts @@ -1,6 +1,6 @@ import { Component, input } from '@angular/core'; -export type BadgeTone = 'success' | 'warning' | 'danger' | 'neutral'; +export type BadgeTone = 'success' | 'warning' | 'danger' | 'critical' | 'neutral'; @Component({ selector: 'ev-badge', diff --git a/apps/frontend/src/app/shared/components/ui/button/button.html b/apps/frontend/src/app/shared/components/ui/button/button.html index 08ab099..4cf5a67 100644 --- a/apps/frontend/src/app/shared/components/ui/button/button.html +++ b/apps/frontend/src/app/shared/components/ui/button/button.html @@ -1,3 +1,9 @@ - diff --git a/apps/frontend/src/app/shared/components/ui/button/button.scss b/apps/frontend/src/app/shared/components/ui/button/button.scss index 0f33524..8fe3753 100644 --- a/apps/frontend/src/app/shared/components/ui/button/button.scss +++ b/apps/frontend/src/app/shared/components/ui/button/button.scss @@ -12,11 +12,15 @@ cursor: not-allowed; opacity: 0.7; } + + &.ev-button--inline { + width: auto; + } } .ev-button--primary { background: var(--color-primary); - color: #fff; + color: var(--color-text-inverse); &:disabled { background: var(--color-disabled); @@ -39,13 +43,13 @@ .ev-button--danger { background: var(--color-danger); - color: #fff; + color: var(--color-text-inverse); &:disabled { background: var(--color-disabled); } &:not(:disabled):hover { - background: #b91c1c; + background: var(--color-danger-hover); } } diff --git a/apps/frontend/src/app/shared/components/ui/button/button.ts b/apps/frontend/src/app/shared/components/ui/button/button.ts index c9d6b11..c7b3148 100644 --- a/apps/frontend/src/app/shared/components/ui/button/button.ts +++ b/apps/frontend/src/app/shared/components/ui/button/button.ts @@ -12,4 +12,5 @@ export class Button { variant = input('primary'); type = input<'button' | 'submit'>('button'); disabled = input(false); + fullWidth = input(true); } diff --git a/apps/frontend/src/styles/_auth-page.scss b/apps/frontend/src/styles/_auth-page.scss index 1c9c0a3..be4c566 100644 --- a/apps/frontend/src/styles/_auth-page.scss +++ b/apps/frontend/src/styles/_auth-page.scss @@ -5,7 +5,8 @@ min-height: 100vh; padding: var(--space-4); box-sizing: border-box; - background: radial-gradient(circle at 15% 10%, var(--color-primary-light) 0%, transparent 45%), + background: + radial-gradient(circle at 15% 10%, var(--color-primary-light) 0%, transparent 45%), radial-gradient(circle at 85% 90%, var(--color-primary-light) 0%, transparent 40%), var(--color-bg); } @@ -13,4 +14,44 @@ .auth-card-wrapper { width: 100%; max-width: 420px; + + ev-card { + padding: 3rem 2.5rem; + box-shadow: + 0 20px 25px -5px rgba(0, 0, 0, 0.06), + 0 8px 10px -6px rgba(0, 0, 0, 0.04); + } + + .auth-brand { + justify-content: center; + width: 100%; + font-size: 2.1rem; + margin-bottom: 1.75rem; + } + + h1 { + margin: 0; + font-size: 1.85rem; + font-weight: 700; + color: var(--color-text); + text-align: center; + } + + .auth-subtitle { + margin: 0.4rem 0 2rem; + color: var(--color-text-muted); + font-size: 0.95rem; + line-height: 1.4; + text-align: center; + } + + ev-alert { + display: block; + margin-top: 0.75rem; + } + + ev-button { + display: block; + margin-top: 1.75rem; + } } diff --git a/apps/frontend/src/styles/_forms.scss b/apps/frontend/src/styles/_forms.scss index fc35ad4..9bbfb0c 100644 --- a/apps/frontend/src/styles/_forms.scss +++ b/apps/frontend/src/styles/_forms.scss @@ -29,9 +29,3 @@ color: var(--color-disabled); margin-top: 0.25rem; } - -.form-error { - margin: var(--space-2) 0 0; - color: var(--color-danger); - font-size: 0.85rem; -} diff --git a/apps/frontend/src/styles/_tokens.scss b/apps/frontend/src/styles/_tokens.scss index 4b85e61..2e7663b 100644 --- a/apps/frontend/src/styles/_tokens.scss +++ b/apps/frontend/src/styles/_tokens.scss @@ -20,8 +20,12 @@ --color-warning: #f9a825; --color-warning-bg: #fef9e7; --color-danger: #dc2626; + --color-danger-hover: #b91c1c; --color-danger-bg: #fef2f2; --color-danger-border: #fecaca; + --color-critical: #b91c1c; + --color-warning-text: #92400e; + --color-text-inverse: #ffffff; // Typo, rayons, ombre --font-family: 'Segoe UI', system-ui, sans-serif; diff --git a/docs/architecture/30-frontend.md b/docs/architecture/30-frontend.md index 3c361e5..de73599 100644 --- a/docs/architecture/30-frontend.md +++ b/docs/architecture/30-frontend.md @@ -16,6 +16,10 @@ Ce qui est en place : - `core/services` porte `StatsService` et `AlertsService`, `core/interceptors` l'intercepteur de fixtures, `features/dashboard` la page, `shared/components` la jauge de consommation et le graphique de charge par site, tous deux construits sur Chart.js. +- 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, sept fichiers de test. - Prettier configuré, parser `angular` pour les gabarits HTML. diff --git a/docs/architecture/32-design-systeme-frontend.md b/docs/architecture/32-design-systeme-frontend.md index ad47a81..3d1c442 100644 --- a/docs/architecture/32-design-systeme-frontend.md +++ b/docs/architecture/32-design-systeme-frontend.md @@ -17,17 +17,19 @@ seule fois dans `src/styles.scss`. Disponibles partout sans import supplémentai | `--color-border`, `--color-border-light` | Bordures d'inputs et de cartes | | `--color-bg`, `--color-surface` | Fond de page vs fond des cartes/panneaux | | `--color-disabled` | Éléments désactivés | -| `--color-success` / `-bg`, `--color-warning` / `-bg`, `--color-danger` / `-bg` / `-border` | États sémantiques (alertes, badges) | +| `--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 | | `--radius-sm`, `--radius-md`, `--radius-pill` | Rayons de bordure (input/bouton, carte, pastille) | | `--shadow-card` | Ombre portée des cartes | | `--space-1` à `--space-5` | Échelle d'espacement (0.35rem à 2.5rem) | -Les classes de formulaire partagées (`.form-label`, `.form-input`, `.form-hint`, `.form-error`) -sont dans `apps/frontend/src/styles/_forms.scss`, importées globalement de la même façon. Elles +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 `