feat(frontend): modèle et service Recommendations (liste, détail, génération)

Types alignés sur RecommendationResponse et RecommendationGenerationResponse
du backend. RecommendationsService couvre GET /recommendations,
GET /recommendations/{id} et POST /recommendations/generate?site_id= (réservé
admin côté API). recommendation-presentation.ts traduit les sept règles connues
du moteur et retombe sur la référence brute pour une règle inconnue, la
politique de renommage en -v2 de l'ADR 0006 l'impose.
This commit is contained in:
Johan LEROY
2026-09-21 14:39:56 +02:00
parent 83caa9006e
commit e147ea69bc
5 changed files with 168 additions and 0 deletions
@@ -0,0 +1,23 @@
import { libelleRegle, tonRegle } from './recommendation-presentation';
describe('recommendation-presentation', () => {
it('traduit les sept règles connues du moteur', () => {
expect(libelleRegle('spike-delestage-v1')).toBe('Délestage');
expect(libelleRegle('threshold-reduction-v1')).toBe('Réduction de puissance');
expect(libelleRegle('outage-secours-v1')).toBe('Alimentation de secours');
expect(libelleRegle('sensor-maintenance-v1')).toBe('Maintenance capteur');
expect(libelleRegle('anomaly-verification-v1')).toBe('Vérification');
expect(libelleRegle('escalade-astreinte-v1')).toBe('Escalade astreinte');
expect(libelleRegle('contrat-puissance-v1')).toBe('Contrat de puissance');
});
it('affiche telle quelle une référence de règle inconnue', () => {
expect(libelleRegle('spike-delestage-v2')).toBe('spike-delestage-v2');
});
it("réserve le ton critique à l'escalade vers l'astreinte", () => {
expect(tonRegle('escalade-astreinte-v1')).toBe('critical');
expect(tonRegle('spike-delestage-v1')).toBe('neutral');
expect(tonRegle('inconnue-v9')).toBe('neutral');
});
});
@@ -0,0 +1,23 @@
import { BadgeTone } from '../components/ui/badge/badge';
// Contrainte : une règle dont le sens change reçoit un suffixe -v2 côté backend (ADR 0006) ;
// une référence inconnue s'affiche donc telle quelle plutôt que de casser la vue.
const LIBELLE_PAR_REGLE: Record<string, string> = {
'spike-delestage-v1': 'Délestage',
'threshold-reduction-v1': 'Réduction de puissance',
'outage-secours-v1': 'Alimentation de secours',
'sensor-maintenance-v1': 'Maintenance capteur',
'anomaly-verification-v1': 'Vérification',
'escalade-astreinte-v1': 'Escalade astreinte',
'contrat-puissance-v1': 'Contrat de puissance',
};
const REGLE_ESCALADE = 'escalade-astreinte-v1';
export function libelleRegle(reference: string): string {
return LIBELLE_PAR_REGLE[reference] ?? reference;
}
export function tonRegle(reference: string): BadgeTone {
return reference === REGLE_ESCALADE ? 'critical' : 'neutral';
}
@@ -0,0 +1,14 @@
export interface Recommendation {
recommendation_id: number;
alert_id: number;
action: string;
explanation: string;
rule_reference: string;
created_at: string;
}
export interface RecommendationGenerationReport {
alerts_examined: number;
recommendations_created: number;
already_present: number;
}