diff --git a/apps/backend/app/main.py b/apps/backend/app/main.py
index de1235e..5a8ec3e 100644
--- a/apps/backend/app/main.py
+++ b/apps/backend/app/main.py
@@ -1,9 +1,15 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
+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
+from starlette.responses import HTMLResponse
from app.api.errors import register_error_handlers
from app.api.middleware import SecurityHeadersMiddleware
@@ -18,6 +24,8 @@ logger = get_logger(__name__)
METHODES_AUTORISEES = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
EN_TETES_AUTORISES = ["Authorization", "Content-Type"]
+STATIC_DIR = Path(__file__).parent / "static"
+LOGO_URL = "/static/logo-icon.png"
@asynccontextmanager
@@ -43,11 +51,49 @@ def create_app(settings: Settings | None = None) -> FastAPI:
openapi_tags=TAGS,
debug=resolved.debug,
lifespan=lifespan,
- docs_url="/docs" if documentee else None,
- redoc_url="/redoc" if documentee else None,
+ docs_url=None,
+ redoc_url=None,
openapi_url="/openapi.json" if documentee else None,
)
+ if documentee:
+ application.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
+
+ # 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.
+ 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["info"]["x-logo"] = {"url": LOGO_URL, "altText": "EnerVision"}
+ application.openapi_schema = schema
+ return application.openapi_schema
+
+ application.openapi = openapi_avec_logo # type: ignore[method-assign]
+
+ @application.get("/docs", include_in_schema=False)
+ async def docs_swagger(_: Request) -> HTMLResponse:
+ return get_swagger_ui_html(
+ openapi_url="/openapi.json",
+ title=f"{application.title} · Swagger UI",
+ swagger_favicon_url=LOGO_URL,
+ )
+
+ @application.get("/redoc", include_in_schema=False)
+ async def docs_redoc(_: Request) -> HTMLResponse:
+ return get_redoc_html(
+ openapi_url="/openapi.json",
+ title=f"{application.title} · ReDoc",
+ redoc_favicon_url=LOGO_URL,
+ )
+
application.add_middleware(SecurityHeadersMiddleware)
if resolved.allowed_origins:
diff --git a/apps/backend/app/static/logo-icon.png b/apps/backend/app/static/logo-icon.png
new file mode 100644
index 0000000..d3bdc53
Binary files /dev/null and b/apps/backend/app/static/logo-icon.png differ
diff --git a/apps/frontend/README.md b/apps/frontend/README.md
index aeaf788..c5b7484 100644
--- a/apps/frontend/README.md
+++ b/apps/frontend/README.md
@@ -76,6 +76,13 @@ Points à vérifier après toute regénération :
côté backend. Le `docker-compose.yml` n'a aucun service frontend.
4. Ajouter le `Dockerfile` multi-stage (build Angular puis service statique nginx).
+## Design système
+
+Tokens (couleurs, typo, espacements) et composants partagés (`ev-button`, `ev-card`,
+`ev-alert`, `ev-badge`) sont documentés dans
+[`docs/architecture/32-design-systeme-frontend.md`](../../docs/architecture/32-design-systeme-frontend.md).
+Toute nouvelle page doit les réutiliser plutôt que définir ses propres valeurs.
+
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
diff --git a/apps/frontend/public/logo.png b/apps/frontend/public/logo.png
new file mode 100644
index 0000000..e5e41b2
Binary files /dev/null and b/apps/frontend/public/logo.png differ
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 edf2146..7bee6a3 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
@@ -1,31 +1,36 @@
diff --git a/apps/frontend/src/app/features/auth/change-password/change-password.scss b/apps/frontend/src/app/features/auth/change-password/change-password.scss
index f44fcb8..48051a5 100644
--- a/apps/frontend/src/app/features/auth/change-password/change-password.scss
+++ b/apps/frontend/src/app/features/auth/change-password/change-password.scss
@@ -3,86 +3,42 @@
align-items: center;
justify-content: center;
min-height: 100vh;
- background: #f3f4f6;
- font-family: 'Segoe UI', system-ui, sans-serif;
+ background: var(--color-bg);
}
-.auth-card {
- background: #ffffff;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- padding: 2.5rem;
+.auth-card-wrapper {
width: 100%;
max-width: 360px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
- display: flex;
- flex-direction: column;
-
- h1 {
- margin: 0;
- font-size: 1.5rem;
- font-weight: 700;
- color: #1f2937;
- }
-
- .auth-subtitle {
- margin: 0.25rem 0 1.5rem;
- color: #6b7280;
- font-size: 0.9rem;
- line-height: 1.4;
- }
-
- label {
- font-size: 0.85rem;
- font-weight: 600;
- color: #374151;
- margin-bottom: 0.35rem;
- margin-top: 1rem;
- }
-
- input {
- padding: 0.6rem 0.75rem;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- font-size: 0.95rem;
-
- &:focus {
- outline: none;
- border-color: #3b82f6;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
- }
- }
-
- button {
- margin-top: 1.5rem;
- padding: 0.7rem;
- background: #3b82f6;
- color: #fff;
- border: none;
- border-radius: 8px;
- font-size: 0.95rem;
- font-weight: 600;
- cursor: pointer;
-
- &:disabled {
- background: #9ca3af;
- cursor: not-allowed;
- }
-
- &:not(:disabled):hover {
- background: #2563eb;
- }
- }
}
-.auth-hint {
- font-size: 0.75rem;
- color: #9ca3af;
- margin-top: 0.25rem;
+.auth-logo {
+ display: block;
+ height: 48px;
+ margin: 0 auto 1rem;
}
-.auth-error {
- margin: 0.75rem 0 0;
- color: #dc2626;
- font-size: 0.85rem;
+h1 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--color-text);
+ text-align: center;
+}
+
+.auth-subtitle {
+ margin: 0.25rem 0 1.5rem;
+ color: var(--color-text-muted);
+ font-size: 0.9rem;
+ line-height: 1.4;
+ text-align: center;
+}
+
+ev-alert {
+ display: block;
+ margin-top: 0.75rem;
+}
+
+ev-button {
+ display: block;
+ margin-top: 1.5rem;
}
diff --git a/apps/frontend/src/app/features/auth/change-password/change-password.spec.ts b/apps/frontend/src/app/features/auth/change-password/change-password.spec.ts
index 63e1872..6fe09d0 100644
--- a/apps/frontend/src/app/features/auth/change-password/change-password.spec.ts
+++ b/apps/frontend/src/app/features/auth/change-password/change-password.spec.ts
@@ -54,7 +54,7 @@ describe('ChangePassword', () => {
fixture.detectChanges(); // rend le bloc @if (errorMessage())
expect(component.errorMessage()).toContain('incorrect');
- const errorEl = fixture.nativeElement.querySelector('.auth-error');
+ const errorEl = fixture.nativeElement.querySelector('.ev-alert');
expect(errorEl?.textContent).toContain('incorrect');
});
@@ -64,7 +64,7 @@ describe('ChangePassword', () => {
const button = fixture.nativeElement.querySelector('button[type="submit"]');
expect(button.disabled).toBe(true);
- expect(fixture.nativeElement.querySelector('.auth-error')).toBeNull();
+ expect(fixture.nativeElement.querySelector('.ev-alert')).toBeNull();
});
it('déclenche onSubmit via la soumission réelle du formulaire (ngSubmit)', () => {
diff --git a/apps/frontend/src/app/features/auth/change-password/change-password.ts b/apps/frontend/src/app/features/auth/change-password/change-password.ts
index 507af14..1acf568 100644
--- a/apps/frontend/src/app/features/auth/change-password/change-password.ts
+++ b/apps/frontend/src/app/features/auth/change-password/change-password.ts
@@ -2,11 +2,14 @@ import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../../../core/services/auth.service';
+import { Button } from '../../../shared/components/ui/button/button';
+import { Card } from '../../../shared/components/ui/card/card';
+import { Alert } from '../../../shared/components/ui/alert/alert';
@Component({
selector: 'app-change-password',
standalone: true,
- imports: [ReactiveFormsModule],
+ imports: [ReactiveFormsModule, Button, Card, Alert],
templateUrl: './change-password.html',
styleUrl: './change-password.scss',
})
diff --git a/apps/frontend/src/app/features/auth/login/login.html b/apps/frontend/src/app/features/auth/login/login.html
index 0083bd2..89e67fe 100644
--- a/apps/frontend/src/app/features/auth/login/login.html
+++ b/apps/frontend/src/app/features/auth/login/login.html
@@ -1,36 +1,41 @@
diff --git a/apps/frontend/src/app/features/auth/login/login.scss b/apps/frontend/src/app/features/auth/login/login.scss
index cc415b8..46cc393 100644
--- a/apps/frontend/src/app/features/auth/login/login.scss
+++ b/apps/frontend/src/app/features/auth/login/login.scss
@@ -3,79 +3,41 @@
align-items: center;
justify-content: center;
min-height: 100vh;
- background: #f3f4f6;
- font-family: 'Segoe UI', system-ui, sans-serif;
+ background: var(--color-bg);
}
-.auth-card {
- background: #ffffff;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- padding: 2.5rem;
+.auth-card-wrapper {
width: 100%;
max-width: 360px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
- display: flex;
- flex-direction: column;
-
- h1 {
- margin: 0;
- font-size: 1.5rem;
- font-weight: 700;
- color: #1f2937;
- }
-
- .auth-subtitle {
- margin: 0.25rem 0 1.5rem;
- color: #6b7280;
- font-size: 0.9rem;
- }
-
- label {
- font-size: 0.85rem;
- font-weight: 600;
- color: #374151;
- margin-bottom: 0.35rem;
- margin-top: 1rem;
- }
-
- input {
- padding: 0.6rem 0.75rem;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- font-size: 0.95rem;
-
- &:focus {
- outline: none;
- border-color: #3b82f6;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
- }
- }
-
- button {
- margin-top: 1.5rem;
- padding: 0.7rem;
- background: #3b82f6;
- color: #fff;
- border: none;
- border-radius: 8px;
- font-size: 0.95rem;
- font-weight: 600;
- cursor: pointer;
-
- &:disabled {
- background: #9ca3af;
- cursor: not-allowed;
- }
-
- &:not(:disabled):hover {
- background: #2563eb;
- }
- }
}
-.auth-error {
- margin: 0.75rem 0 0;
- color: #dc2626;
- font-size: 0.85rem;
+.auth-logo {
+ display: block;
+ height: 48px;
+ margin: 0 auto 1rem;
+}
+
+h1 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--color-text);
+ text-align: center;
+}
+
+.auth-subtitle {
+ margin: 0.25rem 0 1.5rem;
+ color: var(--color-text-muted);
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+ev-alert {
+ display: block;
+ margin-top: 0.75rem;
+}
+
+ev-button {
+ display: block;
+ margin-top: 1.5rem;
}
diff --git a/apps/frontend/src/app/features/auth/login/login.spec.ts b/apps/frontend/src/app/features/auth/login/login.spec.ts
index 3c9bac1..46afce2 100644
--- a/apps/frontend/src/app/features/auth/login/login.spec.ts
+++ b/apps/frontend/src/app/features/auth/login/login.spec.ts
@@ -63,7 +63,7 @@ describe('Login', () => {
fixture.detectChanges(); // rend le bloc @if (errorMessage()) du template
expect(component.errorMessage()).toBe('Email ou mot de passe incorrect.');
- const errorEl = fixture.nativeElement.querySelector('.auth-error');
+ const errorEl = fixture.nativeElement.querySelector('.ev-alert');
expect(errorEl?.textContent).toContain('Email ou mot de passe incorrect.');
});
@@ -80,7 +80,7 @@ describe('Login', () => {
fixture.detectChanges(); // rend aussi le sous-bloc @if (retryAfterSeconds(); as seconds)
expect(component.retryAfterSeconds()).toBe(30);
- const errorEl = fixture.nativeElement.querySelector('.auth-error');
+ const errorEl = fixture.nativeElement.querySelector('.ev-alert');
expect(errorEl?.textContent).toContain('30s');
});
@@ -90,7 +90,7 @@ describe('Login', () => {
const button = fixture.nativeElement.querySelector('button[type="submit"]');
expect(button.disabled).toBe(true);
- expect(fixture.nativeElement.querySelector('.auth-error')).toBeNull();
+ expect(fixture.nativeElement.querySelector('.ev-alert')).toBeNull();
});
it('déclenche onSubmit via la soumission réelle du formulaire (ngSubmit)', () => {
diff --git a/apps/frontend/src/app/features/auth/login/login.ts b/apps/frontend/src/app/features/auth/login/login.ts
index 34b9ff2..ea8f62f 100644
--- a/apps/frontend/src/app/features/auth/login/login.ts
+++ b/apps/frontend/src/app/features/auth/login/login.ts
@@ -3,11 +3,14 @@ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '../../../core/services/auth.service';
+import { Button } from '../../../shared/components/ui/button/button';
+import { Card } from '../../../shared/components/ui/card/card';
+import { Alert } from '../../../shared/components/ui/alert/alert';
@Component({
selector: 'app-login',
standalone: true,
- imports: [ReactiveFormsModule],
+ imports: [ReactiveFormsModule, Button, Card, Alert],
templateUrl: './login.html',
styleUrl: './login.scss',
})
diff --git a/apps/frontend/src/app/features/dashboard/dashboard.html b/apps/frontend/src/app/features/dashboard/dashboard.html
index 684b444..533f0e3 100644
--- a/apps/frontend/src/app/features/dashboard/dashboard.html
+++ b/apps/frontend/src/app/features/dashboard/dashboard.html
@@ -1,19 +1,22 @@