Merge branch 'dev' into feat/supervision-des-capteurs

This commit is contained in:
ValentinDeFaria
2026-09-18 16:54:39 +02:00
committed by GitHub
61 changed files with 6646 additions and 249 deletions
@@ -1,15 +1,17 @@
import { Component, OnInit, inject, signal, DestroyRef } from '@angular/core';
import { Component, OnInit, inject, signal, DestroyRef, WritableSignal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { timer, switchMap, catchError, EMPTY, Observable } from 'rxjs';
import { DecimalPipe } from '@angular/common';
import { DecimalPipe, DatePipe } from '@angular/common';
import { Router, RouterLink } from '@angular/router';
import { StatsService } from '../../core/services/stats.service';
import { ConsumptionGauge } from '../../shared/components/consumption-gauge/consumption-gauge';
import { SiteLoadChart } from '../../shared/components/site-load-chart/site-load-chart';
import { AlertsService } from '../../core/services/alerts.service';
import { PredictionsService } from '../../core/services/predictions.service';
import { AuthService } from '../../core/services/auth.service';
import { StatsSummary } from '../../shared/models/stats.model';
import { Alert, AlertSeverity } from '../../shared/models/alert.model';
import { PredictionStatus, SitePredictionSummary } from '../../shared/models/prediction.model';
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';
@@ -27,11 +29,21 @@ const TON_PAR_SEVERITE: Record<AlertSeverity, BadgeTone> = {
critical: 'critical',
};
// `error` n'a pas de précédent dans les fixtures ou l'API à ce jour, mais figure dans le
// domaine du schéma backend (`ck_prediction_status`) : mieux vaut une couleur définie que
// tomber sur `undefined` si ce statut apparaît un jour.
const TON_PAR_STATUT_PREDICTION: Record<PredictionStatus, BadgeTone> = {
available: 'success',
insufficient_data: 'warning',
error: 'danger',
};
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
DecimalPipe,
DatePipe,
RouterLink,
ConsumptionGauge,
SiteLoadChart,
@@ -48,30 +60,52 @@ export class Dashboard implements OnInit {
private statsService = inject(StatsService);
private alertsService = inject(AlertsService);
public auth = inject(AuthService);
private predictionsService = inject(PredictionsService);
private auth = inject(AuthService);
private router = inject(Router);
private destroyRef = inject(DestroyRef);
stats = signal<StatsSummary | null>(null);
alerts = signal<Alert[]>([]);
error = signal<string | null>(null);
predictions = signal<SitePredictionSummary[]>([]);
// Un signal par flux, pas un seul `error` partagé : sinon le tick suivant de `timer` (stats)
// efface silencieusement un message d'échec des prévisions ou des alertes après 10s au plus,
// sans retry ni indication pour l'utilisateur que la section correspondante est restée vide.
statsError = signal<string | null>(null);
alertsError = signal<string | null>(null);
predictionsError = signal<string | null>(null);
ngOnInit(): void {
this.alertsService
.getAlerts()
.pipe(catchError(() => this.reportUnavailable()))
.subscribe((alerts) => this.alerts.set(alerts));
.pipe(catchError(() => this.reportUnavailable(this.alertsError)))
.subscribe((alerts) => {
this.alertsError.set(null);
this.alerts.set(alerts);
});
// Les prévisions viennent d'un scoring hors ligne, pas d'un calcul à la demande : un seul
// chargement au démarrage suffit, pas besoin du rafraîchissement périodique de `stats`.
this.predictionsService
.getPredictions()
.pipe(catchError(() => this.reportUnavailable(this.predictionsError)))
.subscribe((summary) => {
this.predictionsError.set(null);
this.predictions.set(summary.sites);
});
// Piège : le catchError porte sur l'observable interne. Sur le flux externe il
// terminerait le timer, et le rafraîchissement ne repartirait jamais.
timer(0, REFRESH_INTERVAL_MS)
.pipe(
switchMap(() =>
this.statsService.getSummary().pipe(catchError(() => this.reportUnavailable())),
this.statsService.getSummary().pipe(catchError(() => this.reportUnavailable(this.statsError))),
),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((stats) => {
this.error.set(null);
this.statsError.set(null);
this.stats.set(stats);
});
}
@@ -80,6 +114,10 @@ export class Dashboard implements OnInit {
return TON_PAR_SEVERITE[severity];
}
badgeToneForPredictionStatus(status: PredictionStatus): BadgeTone {
return TON_PAR_STATUT_PREDICTION[status];
}
onLogout(): void {
this.auth.logout().subscribe({
next: () => this.router.navigate(['/login']),
@@ -91,8 +129,8 @@ export class Dashboard implements OnInit {
});
}
private reportUnavailable(): Observable<never> {
this.error.set(UNAVAILABLE_MESSAGE);
private reportUnavailable(target: WritableSignal<string | null>): Observable<never> {
target.set(UNAVAILABLE_MESSAGE);
return EMPTY;
}
}