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.
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -3,7 +3,9 @@
|
||||
<ev-card>
|
||||
<ev-brand class="auth-brand" />
|
||||
<h1>Nouveau mot de passe</h1>
|
||||
<p class="auth-subtitle">Votre mot de passe est provisoire, vous devez le modifier avant de continuer</p>
|
||||
<p class="auth-subtitle">
|
||||
Votre mot de passe est provisoire, vous devez le modifier avant de continuer
|
||||
</p>
|
||||
|
||||
<label class="form-label" for="current_password">Mot de passe actuel</label>
|
||||
<input
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ export class ChangePassword {
|
||||
},
|
||||
error: () => {
|
||||
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}).`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<p class="dashboard__subtitle">Consommation instantanée du parc</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="logout-button" (click)="onLogout()">Déconnexion</button>
|
||||
<ev-button class="logout-button" variant="secondary" [fullWidth]="false" (click)="onLogout()"
|
||||
>Déconnexion</ev-button
|
||||
>
|
||||
</header>
|
||||
|
||||
@if (error(); as message) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<AlertSeverity, BadgeTone> = {
|
||||
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',
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<button [type]="type()" class="ev-button" [class]="'ev-button--' + variant()" [disabled]="disabled()">
|
||||
<button
|
||||
[type]="type()"
|
||||
class="ev-button"
|
||||
[class]="'ev-button--' + variant()"
|
||||
[class.ev-button--inline]="!fullWidth()"
|
||||
[disabled]="disabled()"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</button>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,5 @@ export class Button {
|
||||
variant = input<ButtonVariant>('primary');
|
||||
type = input<'button' | 'submit'>('button');
|
||||
disabled = input(false);
|
||||
fullWidth = input(true);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user