Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3550dde2 | ||
|
|
7f710c9084 |
@@ -51,14 +51,30 @@ jobs:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: apps/frontend/package-lock.json
|
||||
- name : Installation des dépendances (Front)
|
||||
run: npm ci
|
||||
- run: npm ci
|
||||
working-directory: apps/frontend
|
||||
- name : Lancement des tests et génénration du rapport de couverture (Front)
|
||||
run: npm test --watch=false --code-coverage --coverageReporters=lcov
|
||||
- run: npm test -- --watch=false --coverage
|
||||
working-directory: apps/frontend
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: apps/frontend/coverage/frontend/lcov.info
|
||||
|
||||
sonarqube:
|
||||
needs: [build, test]
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: apps/frontend/coverage/frontend
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v8
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
name: SonarQube
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "apps/frontend/**"
|
||||
- "apps/backend/**"
|
||||
- ".github/workflows/sonarqube.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/frontend/**"
|
||||
- "apps/backend/**"
|
||||
- ".github/workflows/sonarqube.yml"
|
||||
|
||||
|
||||
# Build l'ensemble du projet, puis lance les tests
|
||||
# Génère les rapports de couverture, puis lance l'analyse SonarQube
|
||||
|
||||
jobs:
|
||||
build-front:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: apps/frontend/package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
working-directory: apps/frontend
|
||||
- run: npm run build
|
||||
working-directory: apps/frontend
|
||||
|
||||
test-front:
|
||||
needs: build-front
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: apps/frontend/package-lock.json
|
||||
|
||||
- name : Installation des dépendances (Front)
|
||||
run: npm ci
|
||||
working-directory: apps/frontend
|
||||
|
||||
- name : Lancement des tests et génénration du rapport de couverture (Front)
|
||||
run: npm test --watch=false --code-coverage --coverageReporters=lcov
|
||||
working-directory: apps/frontend
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: apps/frontend/coverage/frontend/lcov.info
|
||||
|
||||
build-back:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Installe uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: apps/backend/uv.lock
|
||||
- name: Installe l'interpréteur déclaré par .python-version
|
||||
run: uv python install
|
||||
working-directory: apps/backend
|
||||
|
||||
- name: Synchronise les dépendances sans dévier du verrou
|
||||
run: uv sync --all-groups --frozen
|
||||
working-directory: apps/backend
|
||||
|
||||
- name: Vérifie le formatage
|
||||
run: uv run ruff format --check .
|
||||
working-directory: apps/backend
|
||||
|
||||
- name: Analyse statique
|
||||
run: uv run ruff check --output-format=github .
|
||||
working-directory: apps/backend
|
||||
|
||||
- name: Typage
|
||||
run: uv run mypy app
|
||||
working-directory: apps/backend
|
||||
|
||||
|
||||
test-back:
|
||||
needs: build-back
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Installe uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: apps/backend/uv.lock
|
||||
|
||||
- name : Lancement des tests et génénration du rapport de couverture (Back)
|
||||
run: uv run pytest --cov-fail-under=85 --cov-report=xml
|
||||
working-directory: apps/backend
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-coverage
|
||||
path: apps/backend/coverage.xml
|
||||
|
||||
sonarqube:
|
||||
needs: [build-front, build-back, test-front, test-back]
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Téléchargement du rapport de couverture (Front)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: apps/frontend/coverage/frontend
|
||||
- name: Téléchargement du rapport de couverture (Back)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: backend-coverage
|
||||
path: apps/backend
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v8
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
sonar.projectKey=ProjetPiscine_EnerVision
|
||||
sonar.organization=groupe3-ener-vision
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Dossier contenant le code source
|
||||
sonar.sources=apps/frontend/src,apps/backend/app
|
||||
sonar.tests=apps/backend/tests
|
||||
|
||||
# Liste des fichiers et dossiers à exclure de l'analyse
|
||||
# Liste des fichiers et dossiers à exclure de l'analyse
|
||||
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.js,**/*.test.js,github,db,ml,docker-compose.yml,**/**/Dockerfile,**/**/proxy.conf.json,**/**/package.json,**/**/angular.json
|
||||
|
||||
# Chemin vers le rapport de couverture de code
|
||||
# Fichier généré par Vitest
|
||||
# Chemin vers le rapport de couverture de code
|
||||
# Fichier généré par Vitest
|
||||
sonar.javascript.lcov.reportPaths=apps/frontend/coverage/frontend/lcov.info
|
||||
sonar.python.coverage.reportPaths=apps/backend/cov.info
|
||||
@@ -25,4 +25,11 @@ export const routes: Routes = [
|
||||
(m) => m.SiteDetailPlaceholder,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'monitoring/sensors',
|
||||
canActivate: [authGuard],
|
||||
data: { role: 'admin' },
|
||||
loadComponent: () =>
|
||||
import('./features/monitoring/sensor-status/sensor-status').then((m) => m.SensorStatusView),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { SensorsService } from './sensors.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
describe('SensorsService', () => {
|
||||
let service: SensorsService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting()],
|
||||
});
|
||||
service = TestBed.inject(SensorsService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it("appelle l'endpoint /sensors/status et retourne la réponse", () => {
|
||||
let result: unknown;
|
||||
service.getStatus().subscribe((r) => (result = r));
|
||||
|
||||
const req = httpMock.expectOne(`${environment.apiUrl}/sensors/status`);
|
||||
expect(req.request.method).toBe('GET');
|
||||
|
||||
req.flush({
|
||||
timestamp: '2026-09-18T08:00:00',
|
||||
sites: [
|
||||
{
|
||||
site_id: 'SITE001',
|
||||
site_name: 'Test',
|
||||
overall: 'ok',
|
||||
sensors: {
|
||||
consumption: { status: 'ok', since: null },
|
||||
electrical: { status: 'ok', since: null },
|
||||
temperature: { status: 'ok', since: null },
|
||||
humidity: { status: 'ok', since: null },
|
||||
network: { status: 'ok', since: null },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect((result as { sites: unknown[] }).sites.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Service, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import {SensorStatusResponse} from '../../shared/models/sensor-status.model';
|
||||
|
||||
@Service()
|
||||
export class SensorsService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getStatus() {
|
||||
return this.http.get<SensorStatusResponse>(`${environment.apiUrl}/sensors/status`);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard__actions">
|
||||
@if (auth.principal()?.role === 'admin') {
|
||||
<a routerLink="/monitoring/sensors" class="ev-link">Supervision des capteurs</a>
|
||||
}
|
||||
<a routerLink="/sites" class="ev-link">Voir les sites</a>
|
||||
<ev-button
|
||||
class="logout-button"
|
||||
|
||||
@@ -68,6 +68,15 @@ h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card--link {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.card__label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
@@ -101,8 +101,11 @@ describe('Dashboard', () => {
|
||||
it('appelle logout et redirige vers /login au clic sur le bouton de déconnexion', () => {
|
||||
const statsMock = { getSummary: vi.fn().mockReturnValue(of({ total_sites: 7, sites: [] })) };
|
||||
const alertsMock = { getAlerts: vi.fn().mockReturnValue(of([])) };
|
||||
const authMock = { logout: vi.fn().mockReturnValue(of(undefined)), clearSession: vi.fn() };
|
||||
|
||||
const authMock = {
|
||||
logout: vi.fn().mockReturnValue(of(undefined)),
|
||||
clearSession: vi.fn(),
|
||||
principal: vi.fn().mockReturnValue({ role: 'admin' }),
|
||||
};
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Dashboard],
|
||||
providers: [
|
||||
@@ -128,9 +131,10 @@ describe('Dashboard', () => {
|
||||
it('déconnecte localement et redirige vers /login même si logout échoue côté réseau', () => {
|
||||
const statsMock = { getSummary: vi.fn().mockReturnValue(of({ total_sites: 7, sites: [] })) };
|
||||
const alertsMock = { getAlerts: vi.fn().mockReturnValue(of([])) };
|
||||
const authMock = {
|
||||
const authMock = {
|
||||
logout: vi.fn().mockReturnValue(throwError(() => new Error('réseau indisponible'))),
|
||||
clearSession: vi.fn(),
|
||||
principal: vi.fn().mockReturnValue({ role: 'admin' }),
|
||||
};
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Dashboard],
|
||||
|
||||
@@ -47,7 +47,7 @@ const TON_PAR_SEVERITE: Record<AlertSeverity, BadgeTone> = {
|
||||
export class Dashboard implements OnInit {
|
||||
private statsService = inject(StatsService);
|
||||
private alertsService = inject(AlertsService);
|
||||
private auth = inject(AuthService);
|
||||
public auth = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="sensor-status">
|
||||
<nav class="ev-breadcrumb">
|
||||
<a routerLink="/dashboard">Tableau de bord</a>
|
||||
</nav>
|
||||
|
||||
<header class="sensor-status__header">
|
||||
<a routerLink="/dashboard" class="ev-brand-link">
|
||||
<ev-brand class="sensor-status__logo" />
|
||||
</a>
|
||||
<div>
|
||||
<h1>Supervision des capteurs</h1>
|
||||
<p class="sensor-status__subtitle">État de santé par capteur et par site</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (error(); as message) {
|
||||
<ev-alert severity="danger" class="banner-error">{{ message }}</ev-alert>
|
||||
}
|
||||
|
||||
@if (data(); as d) {
|
||||
<div class="sites-grid">
|
||||
@for (site of d.sites; track site.site_id) {
|
||||
<ev-card class="site-card">
|
||||
<div class="site-card__header">
|
||||
<span class="site-card__name">{{ site.site_name }}</span>
|
||||
<ev-badge [tone]="badgeToneForOverall(site.overall)">{{ site.overall }}</ev-badge>
|
||||
</div>
|
||||
|
||||
<ul class="sensor-list">
|
||||
@for (entry of sensorEntries; track entry[0]) {
|
||||
<li class="sensor-item">
|
||||
<span
|
||||
class="sensor-dot"
|
||||
[class]="'sensor-dot--' + sensorOf(site.sensors, entry[0]).status"
|
||||
></span>
|
||||
<span class="sensor-item__label">{{ entry[1] }}</span>
|
||||
@if (sensorOf(site.sensors, entry[0]).status === 'failing') {
|
||||
<span class="sensor-item__since">
|
||||
depuis {{ sensorOf(site.sensors, entry[0]).since | date: 'short' }}
|
||||
</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ev-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,90 @@
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--color-text);
|
||||
padding: 2.5rem 2rem;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.sensor-status__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.sensor-status__logo {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.sensor-status__subtitle {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.banner-error {
|
||||
display: block;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.sites-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.site-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.site-card__name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sensor-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sensor-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.sensor-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--ok {
|
||||
background: var(--color-success);
|
||||
}
|
||||
&--failing {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.sensor-item__label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sensor-item__since {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { vi } from 'vitest';
|
||||
import { SensorStatusView } from './sensor-status';
|
||||
import { SensorsService } from '../../../core/services/sensors.service';
|
||||
import { SiteSensors } from '../../../shared/models/sensor-status.model';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
const OK_SENSORS: SiteSensors = {
|
||||
consumption: { status: 'ok', since: null },
|
||||
electrical: { status: 'ok', since: null },
|
||||
temperature: { status: 'ok', since: null },
|
||||
humidity: { status: 'ok', since: null },
|
||||
network: { status: 'ok', since: null },
|
||||
};
|
||||
|
||||
describe('SensorStatusView', () => {
|
||||
let sensorsMock: { getStatus: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
sensorsMock = { getStatus: vi.fn() };
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SensorStatusView],
|
||||
providers: [
|
||||
{ provide: SensorsService, useValue: sensorsMock },
|
||||
provideRouter([]),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('charge et affiche les données au démarrage', () => {
|
||||
sensorsMock.getStatus.mockReturnValue(
|
||||
of({
|
||||
timestamp: '2026-09-18T08:00:00',
|
||||
sites: [
|
||||
{ site_id: 'SITE001', site_name: 'Bureau Test', overall: 'ok', sensors: OK_SENSORS },
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const fixture = TestBed.createComponent(SensorStatusView);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.data()?.sites.length).toBe(1);
|
||||
expect(fixture.componentInstance.error()).toBeNull();
|
||||
expect(fixture.nativeElement.textContent).toContain('Bureau Test');
|
||||
});
|
||||
|
||||
it("affiche un message d'erreur si l'appel échoue", () => {
|
||||
sensorsMock.getStatus.mockReturnValue(throwError(() => new Error('boom')));
|
||||
|
||||
const fixture = TestBed.createComponent(SensorStatusView);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.error()).toBe(
|
||||
'État des capteurs indisponible, réessayez plus tard.'
|
||||
);
|
||||
expect(fixture.componentInstance.data()).toBeNull();
|
||||
expect(fixture.nativeElement.textContent).toContain('État des capteurs indisponible');
|
||||
});
|
||||
|
||||
it('associe le bon ton de badge à chaque statut global', () => {
|
||||
sensorsMock.getStatus.mockReturnValue(of({ timestamp: '2026-09-18T08:00:00', sites: [] }));
|
||||
const fixture = TestBed.createComponent(SensorStatusView);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
expect(component.badgeToneForOverall('ok')).toBe('success');
|
||||
expect(component.badgeToneForOverall('degraded')).toBe('warning');
|
||||
expect(component.badgeToneForOverall('critical')).toBe('critical');
|
||||
expect(component.badgeToneForOverall('inconnu')).toBe('neutral');
|
||||
});
|
||||
|
||||
it('retourne le bon diagnostic via sensorOf', () => {
|
||||
sensorsMock.getStatus.mockReturnValue(of({ timestamp: '2026-09-18T08:00:00', sites: [] }));
|
||||
const fixture = TestBed.createComponent(SensorStatusView);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
expect(component.sensorOf(OK_SENSORS, 'temperature')).toEqual({ status: 'ok', since: null });
|
||||
});
|
||||
|
||||
it('affiche la date depuis quand un capteur est en panne', () => {
|
||||
const sensors: SiteSensors = {
|
||||
...OK_SENSORS,
|
||||
temperature: { status: 'failing', since: '2026-09-18T08:00:00' },
|
||||
};
|
||||
sensorsMock.getStatus.mockReturnValue(
|
||||
of({
|
||||
timestamp: '2026-09-18T08:00:00',
|
||||
sites: [{ site_id: 'SITE001', site_name: 'Bureau Test', overall: 'degraded', sensors }],
|
||||
})
|
||||
);
|
||||
|
||||
const fixture = TestBed.createComponent(SensorStatusView);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain('depuis');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { catchError, EMPTY, Observable } from 'rxjs';
|
||||
import {Badge, BadgeTone} from '../../../shared/components/ui/badge/badge';
|
||||
import {Card} from '../../../shared/components/ui/card/card';
|
||||
import {Alert} from '../../../shared/components/ui/alert/alert';
|
||||
import {Brand} from '../../../shared/components/ui/brand/brand';
|
||||
import {SensorsService} from '../../../core/services/sensors.service';
|
||||
import {SensorDiagnostic, SensorStatusResponse} from '../../../shared/models/sensor-status.model';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
const UNAVAILABLE_MESSAGE = 'État des capteurs indisponible, réessayez plus tard.';
|
||||
|
||||
const SENSOR_LABELS: Record<string, string> = {
|
||||
consumption: 'Consommation',
|
||||
electrical: 'Électrique',
|
||||
temperature: 'Température',
|
||||
humidity: 'Humidité',
|
||||
network: 'Réseau',
|
||||
};
|
||||
|
||||
const TON_PAR_OVERALL: Record<string, BadgeTone> = {
|
||||
ok: 'success',
|
||||
degraded: 'warning',
|
||||
critical: 'critical',
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-sensor-status',
|
||||
standalone: true,
|
||||
imports: [RouterLink, Card, Alert, Badge, Brand, DatePipe],
|
||||
templateUrl: './sensor-status.html',
|
||||
styleUrl: './sensor-status.scss',
|
||||
})
|
||||
export class SensorStatusView implements OnInit {
|
||||
private sensorsService = inject(SensorsService);
|
||||
|
||||
data = signal<SensorStatusResponse | null>(null);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
readonly sensorEntries = Object.entries(SENSOR_LABELS);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sensorsService
|
||||
.getStatus()
|
||||
.pipe(catchError(() => this.reportUnavailable()))
|
||||
.subscribe((response) => {
|
||||
this.error.set(null);
|
||||
this.data.set(response);
|
||||
});
|
||||
}
|
||||
|
||||
sensorOf(sensors: Record<string, SensorDiagnostic>, key: string): SensorDiagnostic {
|
||||
return sensors[key];
|
||||
}
|
||||
|
||||
badgeToneForOverall(overall: string): BadgeTone {
|
||||
return TON_PAR_OVERALL[overall] ?? 'neutral';
|
||||
}
|
||||
|
||||
private reportUnavailable(): Observable<never> {
|
||||
this.error.set(UNAVAILABLE_MESSAGE);
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export type SensorStatus = 'ok' | 'failing';
|
||||
export type OverallStatus = 'ok' | 'degraded' | 'critical';
|
||||
|
||||
export interface SensorDiagnostic {
|
||||
status: SensorStatus;
|
||||
since: string | null;
|
||||
}
|
||||
|
||||
export interface SiteSensors {
|
||||
consumption: SensorDiagnostic;
|
||||
electrical: SensorDiagnostic;
|
||||
temperature: SensorDiagnostic;
|
||||
humidity: SensorDiagnostic;
|
||||
network: SensorDiagnostic;
|
||||
[key: string]: SensorDiagnostic;
|
||||
}
|
||||
|
||||
export interface SiteSensorStatus {
|
||||
site_id: string;
|
||||
site_name: string;
|
||||
sensors: SiteSensors;
|
||||
overall: OverallStatus;
|
||||
}
|
||||
|
||||
export interface SensorStatusResponse {
|
||||
timestamp: string;
|
||||
sites: SiteSensorStatus[];
|
||||
}
|
||||
@@ -2,16 +2,8 @@ sonar.projectKey=ProjetPiscine_EnerVision
|
||||
sonar.organization=groupe3-ener-vision
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Dossier contenant le code source
|
||||
sonar.sources=apps/frontend/src,apps/backend
|
||||
# Dossier contenant les tests
|
||||
sonar.tests=apps/frontend/src,apps/backend/tests
|
||||
sonar.test.inclusions=**/*.spec.ts,**/*.test.ts,**/*test_*.py,**/*test.py
|
||||
sonar.sources=apps/frontend/src,apps/backend/app
|
||||
|
||||
# Liste des fichiers et dossiers à exclure de l'analyse
|
||||
sonar.exclusions=.pytest_cache,.venv,alembic,tests,**/*/node_modules/**,**/*/dist/**,**/*/build/**,**/*.spec.ts,**/*.test.ts,**/*test_*.py,**/*test.py
|
||||
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.js,**/*.test.js,github,db,ml,docker-compose.yml,**/**/Dockerfile,**/**/proxy.conf.json,**/**/package.json,**/**/angular.json
|
||||
|
||||
# Chemin vers le rapport de couverture de code
|
||||
# Fichier généré par Pytest
|
||||
sonar.python.coverage.reportPaths=apps/backend/coverage.xml
|
||||
sonar.javascript.lcov.reportPaths=apps/frontend/coverage/frontend/lcov.info
|
||||
|
||||
Reference in New Issue
Block a user