fix(frontend): annule le chargement en cours lors d'un changement rapide de site

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.
This commit is contained in:
Johan LEROY
2026-09-17 16:47:40 +02:00
parent 41acdb54b6
commit e26948932e
@@ -1,7 +1,7 @@
import { Component, computed, effect, inject, signal } from '@angular/core'; import { Component, DestroyRef, computed, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { catchError, EMPTY, map, Observable, switchMap } from 'rxjs'; import { catchError, EMPTY, filter, map, Observable, switchMap } from 'rxjs';
import { SitesService } from '../../../core/services/sites.service'; import { SitesService } from '../../../core/services/sites.service';
import { ReadingsService } from '../../../core/services/readings.service'; import { ReadingsService } from '../../../core/services/readings.service';
import { Site } from '../../../shared/models/site.model'; import { Site } from '../../../shared/models/site.model';
@@ -83,6 +83,7 @@ export class SiteDetail {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private sitesService = inject(SitesService); private sitesService = inject(SitesService);
private readingsService = inject(ReadingsService); private readingsService = inject(ReadingsService);
private destroyRef = inject(DestroyRef);
siteId = toSignal(this.route.paramMap.pipe(map((params) => params.get('siteId') ?? ''))); siteId = toSignal(this.route.paramMap.pipe(map((params) => params.get('siteId') ?? '')));
@@ -105,38 +106,13 @@ export class SiteDetail {
}); });
constructor() { constructor() {
effect(() => { toObservable(this.siteId)
const siteId = this.siteId();
if (siteId) {
this.load(siteId);
}
});
}
badgeToneForStatus(status: string | null): BadgeTone {
return status ? (TON_PAR_STATUT[status] ?? 'neutral') : 'neutral';
}
private load(siteId: string): void {
this.sitesService
.getSite(siteId)
.pipe( .pipe(
switchMap((site) => filter((siteId): siteId is string => !!siteId),
this.readingsService.getLatest(siteId).pipe(map((latest) => ({ site, latest }))), // Piège : switchMap sur le flux externe annule le chargement en cours dès qu'un
), // nouveau siteId arrive, sinon une réponse en retard peut écraser le site affiché.
switchMap(({ site, latest }) => { switchMap((siteId) => this.load(siteId)),
// Piège : le dataset historique se termine bien avant « maintenant ». Ancrer la takeUntilDestroyed(this.destroyRef),
// fenêtre sur la dernière mesure connue plutôt que sur l'horloge évite un historique
// vide dès que le jeu de données n'est plus récent.
const end = latest?.timestamp;
const start = end
? new Date(new Date(end).getTime() - HISTORY_WINDOW_MS).toISOString()
: undefined;
return this.readingsService
.getHistory(siteId, start, end)
.pipe(map((history) => ({ site, latest, history })));
}),
catchError(() => this.reportUnavailable()),
) )
.subscribe((result) => { .subscribe((result) => {
this.error.set(null); this.error.set(null);
@@ -146,6 +122,31 @@ export class SiteDetail {
}); });
} }
badgeToneForStatus(status: string | null): BadgeTone {
return status ? (TON_PAR_STATUT[status] ?? 'neutral') : 'neutral';
}
private load(siteId: string) {
return this.sitesService.getSite(siteId).pipe(
switchMap((site) =>
this.readingsService.getLatest(siteId).pipe(map((latest) => ({ site, latest }))),
),
switchMap(({ site, latest }) => {
// Piège : le dataset historique se termine bien avant « maintenant ». Ancrer la
// fenêtre sur la dernière mesure connue plutôt que sur l'horloge évite un historique
// vide dès que le jeu de données n'est plus récent.
const end = latest?.timestamp;
const start = end
? new Date(new Date(end).getTime() - HISTORY_WINDOW_MS).toISOString()
: undefined;
return this.readingsService
.getHistory(siteId, start, end)
.pipe(map((history) => ({ site, latest, history })));
}),
catchError(() => this.reportUnavailable()),
);
}
private reasonFor(field: MetricKey, reading: Reading | null): string { private reasonFor(field: MetricKey, reading: Reading | null): string {
const raisons = RAISONS_PAR_CHAMP[field]; const raisons = RAISONS_PAR_CHAMP[field];
const trouvees = (reading?.null_reasons ?? []) const trouvees = (reading?.null_reasons ?? [])