fix(frontend): aligne le modèle Alert et AlertsService sur le contrat /alerts
Le modèle front portait un alert_id texte, des value/threshold non nullables et ignorait metric et prediction_id, alors que l'API sérialise un entier, des flottants nullables et ces deux champs. Toute comparaison avec l'alert_id d'une recommandation échouait silencieusement. - alert.model.ts : alert_id number, value/threshold/metric/prediction_id nullables - alerts.service.ts : getAlerts(filters) pose site_id et severity en HttpParams, les deux filtres que l'API accepte - alerts.fixture.ts : réaligné sur le contrat (ids entiers, horodatages UTC, champs nuls sur outage/sensor, un cas spike) - alert-presentation.ts : tons, libellés et unités partagés ; low passe en neutre, le vert se lisait comme un état sain
This commit is contained in:
@@ -2,53 +2,63 @@ import { Alert } from '../../shared/models/alert.model';
|
||||
|
||||
export const ALERTS_FIXTURE: Alert[] = [
|
||||
{
|
||||
alert_id: 'ALR-SITE002-1718458320',
|
||||
timestamp: '2026-09-15T11:12:00',
|
||||
alert_id: 5,
|
||||
site_id: 'SITE002',
|
||||
timestamp: '2026-09-15T11:12:00Z',
|
||||
type: 'threshold',
|
||||
severity: 'critical',
|
||||
type: 'outage',
|
||||
message: 'Risque de surcharge sur Usine Lyon Vénissieux',
|
||||
message: 'Puissance appelée 812.5 kW au-dessus de la capacité du site (720.0 kW)',
|
||||
value: 812.5,
|
||||
threshold: 720.0,
|
||||
metric: 'consumption_kw',
|
||||
prediction_id: null,
|
||||
},
|
||||
{
|
||||
alert_id: 'ALR-SITE003-1718458321',
|
||||
timestamp: '2026-09-15T11:05:00',
|
||||
alert_id: 4,
|
||||
site_id: 'SITE003',
|
||||
timestamp: '2026-09-15T11:05:00Z',
|
||||
type: 'outage',
|
||||
severity: 'critical',
|
||||
type: 'sensor',
|
||||
message: 'Perte réseau totale sur Data Center Marseille',
|
||||
value: 0,
|
||||
threshold: 0,
|
||||
message: 'Aucune lecture depuis 5:00:00 (dernière lecture : 2026-09-15T06:05:00+00:00)',
|
||||
value: null,
|
||||
threshold: null,
|
||||
metric: null,
|
||||
prediction_id: null,
|
||||
},
|
||||
{
|
||||
alert_id: 'ALR-SITE005-1718458322',
|
||||
timestamp: '2026-09-15T10:47:00',
|
||||
alert_id: 3,
|
||||
site_id: 'SITE005',
|
||||
timestamp: '2026-09-15T10:47:00Z',
|
||||
type: 'spike',
|
||||
severity: 'high',
|
||||
type: 'threshold',
|
||||
message: 'Usine Toulouse approche de son seuil de capacité',
|
||||
message: 'Variation brutale entre deux lectures consécutives (260.0 kW -> 410.0 kW)',
|
||||
value: 410.0,
|
||||
threshold: 480.0,
|
||||
threshold: 260.0,
|
||||
metric: 'consumption_kw',
|
||||
prediction_id: null,
|
||||
},
|
||||
{
|
||||
alert_id: 'ALR-SITE006-1718458323',
|
||||
timestamp: '2026-09-15T10:30:00',
|
||||
alert_id: 2,
|
||||
site_id: 'SITE006',
|
||||
severity: 'medium',
|
||||
timestamp: '2026-09-15T10:30:00Z',
|
||||
type: 'sensor',
|
||||
message: 'Capteur de température défaillant sur Bureau Lille',
|
||||
value: 0,
|
||||
threshold: 0,
|
||||
severity: 'medium',
|
||||
message: 'Qualité de mesure degraded (capteur hors ligne, valeur nulle)',
|
||||
value: null,
|
||||
threshold: null,
|
||||
metric: null,
|
||||
prediction_id: null,
|
||||
},
|
||||
{
|
||||
alert_id: 'ALR-SITE004-1718458324',
|
||||
timestamp: '2026-09-15T09:58:00',
|
||||
alert_id: 1,
|
||||
site_id: 'SITE004',
|
||||
severity: 'low',
|
||||
timestamp: '2026-09-15T09:58:00Z',
|
||||
type: 'anomaly',
|
||||
message: 'Comportement de consommation inhabituel sur Bureau Bordeaux',
|
||||
severity: 'low',
|
||||
message: 'Écart de 13% entre la consommation mesurée (62.0 kWh) et la prévision (55.0 kWh)',
|
||||
value: 62.0,
|
||||
threshold: 55.0,
|
||||
metric: 'consumption_kwh',
|
||||
prediction_id: 42,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,6 +3,20 @@ import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { AlertsService } from './alerts.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { Alert } from '../../shared/models/alert.model';
|
||||
|
||||
const ALERT_API: Alert = {
|
||||
alert_id: 1,
|
||||
site_id: 'site-1',
|
||||
timestamp: '2026-09-16T00:00:00Z',
|
||||
type: 'threshold',
|
||||
severity: 'high',
|
||||
message: 'Dépassement du seuil configuré',
|
||||
value: 812.5,
|
||||
threshold: 720.0,
|
||||
metric: 'consumption_kw',
|
||||
prediction_id: null,
|
||||
};
|
||||
|
||||
describe('AlertsService', () => {
|
||||
let service: AlertsService;
|
||||
@@ -18,26 +32,35 @@ describe('AlertsService', () => {
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it("appelle le bon endpoint et retourne un tableau d'alertes", () => {
|
||||
let result: unknown;
|
||||
it("appelle le bon endpoint sans paramètre et retourne un tableau d'alertes", () => {
|
||||
let result: Alert[] = [];
|
||||
service.getAlerts().subscribe((r) => (result = r));
|
||||
|
||||
const req = httpMock.expectOne(`${environment.apiUrl}/alerts`);
|
||||
expect(req.request.method).toBe('GET');
|
||||
const req = httpMock.expectOne(
|
||||
(r) => r.url === `${environment.apiUrl}/alerts` && r.method === 'GET',
|
||||
);
|
||||
expect(req.request.params.keys()).toEqual([]);
|
||||
req.flush([ALERT_API]);
|
||||
|
||||
req.flush([
|
||||
{
|
||||
alert_id: 'ALR-TEST-1',
|
||||
timestamp: '2026-09-15T12:00:00',
|
||||
site_id: 'SITE001',
|
||||
severity: 'high',
|
||||
type: 'threshold',
|
||||
message: 'Test',
|
||||
value: 100,
|
||||
threshold: 90,
|
||||
},
|
||||
]);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].alert_id).toBe(1);
|
||||
expect(result[0].prediction_id).toBeNull();
|
||||
});
|
||||
|
||||
expect((result as unknown[]).length).toBe(1);
|
||||
it('transmet les filtres site_id et severity en paramètres de requête', () => {
|
||||
service.getAlerts({ site_id: 'SITE001', severity: 'high' }).subscribe();
|
||||
|
||||
const req = httpMock.expectOne((r) => r.url === `${environment.apiUrl}/alerts`);
|
||||
expect(req.request.params.get('site_id')).toBe('SITE001');
|
||||
expect(req.request.params.get('severity')).toBe('high');
|
||||
req.flush([]);
|
||||
});
|
||||
|
||||
it('ne pose pas de paramètre pour un filtre omis', () => {
|
||||
service.getAlerts({ site_id: 'SITE001' }).subscribe();
|
||||
|
||||
const req = httpMock.expectOne((r) => r.url === `${environment.apiUrl}/alerts`);
|
||||
expect(req.request.params.has('severity')).toBe(false);
|
||||
req.flush([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { Service, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { Alert } from '../../shared/models/alert.model';
|
||||
import { Alert, AlertSeverity } from '../../shared/models/alert.model';
|
||||
|
||||
export interface AlertFilters {
|
||||
site_id?: string;
|
||||
severity?: AlertSeverity;
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class AlertsService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getAlerts() {
|
||||
return this.http.get<Alert[]>(`${environment.apiUrl}/alerts`);
|
||||
getAlerts(filters: AlertFilters = {}) {
|
||||
let params = new HttpParams();
|
||||
if (filters.site_id) {
|
||||
params = params.set('site_id', filters.site_id);
|
||||
}
|
||||
if (filters.severity) {
|
||||
params = params.set('severity', filters.severity);
|
||||
}
|
||||
return this.http.get<Alert[]>(`${environment.apiUrl}/alerts`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
LIBELLE_PAR_SEVERITE,
|
||||
LIBELLE_PAR_TYPE,
|
||||
SEVERITES,
|
||||
TON_PAR_SEVERITE,
|
||||
TYPES_ALERTE,
|
||||
UNITE_PAR_METRIQUE,
|
||||
} from './alert-presentation';
|
||||
|
||||
describe('alert-presentation', () => {
|
||||
it('distingue le ton des sévérités high et critical', () => {
|
||||
expect(TON_PAR_SEVERITE.high).toBe('danger');
|
||||
expect(TON_PAR_SEVERITE.critical).toBe('critical');
|
||||
expect(TON_PAR_SEVERITE.high).not.toBe(TON_PAR_SEVERITE.critical);
|
||||
});
|
||||
|
||||
it("n'affiche pas une alerte faible avec le ton de succès", () => {
|
||||
expect(TON_PAR_SEVERITE.low).toBe('neutral');
|
||||
expect(TON_PAR_SEVERITE.medium).toBe('warning');
|
||||
});
|
||||
|
||||
it('donne un libellé français à chaque sévérité et à chaque type', () => {
|
||||
for (const severite of SEVERITES) {
|
||||
expect(LIBELLE_PAR_SEVERITE[severite]).toBeTruthy();
|
||||
}
|
||||
for (const type of TYPES_ALERTE) {
|
||||
expect(LIBELLE_PAR_TYPE[type]).toBeTruthy();
|
||||
}
|
||||
expect(SEVERITES.length).toBe(4);
|
||||
expect(TYPES_ALERTE.length).toBe(5);
|
||||
});
|
||||
|
||||
it('associe une unité à chaque métrique du contrat', () => {
|
||||
expect(UNITE_PAR_METRIQUE.consumption_kw).toBe('kW');
|
||||
expect(UNITE_PAR_METRIQUE.consumption_kwh).toBe('kWh');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { BadgeTone } from '../components/ui/badge/badge';
|
||||
import { AlertMetric, AlertSeverity, AlertType } from './alert.model';
|
||||
|
||||
// Pourquoi : `low` en neutre plutôt qu'en vert, une alerte faible reste une alerte ; le vert se
|
||||
// lisait comme « tout va bien » à côté des rouges.
|
||||
export const TON_PAR_SEVERITE: Record<AlertSeverity, BadgeTone> = {
|
||||
low: 'neutral',
|
||||
medium: 'warning',
|
||||
high: 'danger',
|
||||
critical: 'critical',
|
||||
};
|
||||
|
||||
export const LIBELLE_PAR_SEVERITE: Record<AlertSeverity, string> = {
|
||||
low: 'Faible',
|
||||
medium: 'Moyenne',
|
||||
high: 'Élevée',
|
||||
critical: 'Critique',
|
||||
};
|
||||
|
||||
export const LIBELLE_PAR_TYPE: Record<AlertType, string> = {
|
||||
spike: 'Pic de consommation',
|
||||
threshold: 'Seuil dépassé',
|
||||
anomaly: 'Anomalie',
|
||||
outage: 'Coupure',
|
||||
sensor: 'Capteur',
|
||||
};
|
||||
|
||||
export const UNITE_PAR_METRIQUE: Record<AlertMetric, string> = {
|
||||
consumption_kw: 'kW',
|
||||
consumption_kwh: 'kWh',
|
||||
};
|
||||
|
||||
export const SEVERITES: readonly AlertSeverity[] = ['low', 'medium', 'high', 'critical'];
|
||||
|
||||
export const TYPES_ALERTE: readonly AlertType[] = [
|
||||
'spike',
|
||||
'threshold',
|
||||
'anomaly',
|
||||
'outage',
|
||||
'sensor',
|
||||
];
|
||||
@@ -1,13 +1,16 @@
|
||||
export type AlertSeverity = 'low' | 'medium' | 'high' | 'critical';
|
||||
export type AlertType = 'spike' | 'threshold' | 'anomaly' | 'outage' | 'sensor';
|
||||
export type AlertMetric = 'consumption_kw' | 'consumption_kwh';
|
||||
|
||||
export interface Alert {
|
||||
alert_id: string;
|
||||
timestamp: string;
|
||||
alert_id: number;
|
||||
site_id: string;
|
||||
severity: AlertSeverity;
|
||||
timestamp: string;
|
||||
type: AlertType;
|
||||
severity: AlertSeverity;
|
||||
message: string;
|
||||
value: number;
|
||||
threshold: number;
|
||||
value: number | null;
|
||||
threshold: number | null;
|
||||
metric: AlertMetric | null;
|
||||
prediction_id: number | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user