Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd428feba | ||
|
|
8fd2f19cce |
@@ -56,27 +56,3 @@ jobs:
|
|||||||
# Le marqueur `integration` est exclu par défaut, donc aucune base n'est nécessaire ici.
|
# Le marqueur `integration` est exclu par défaut, donc aucune base n'est nécessaire ici.
|
||||||
- name: Tests et couverture
|
- name: Tests et couverture
|
||||||
run: uv run pytest --cov-fail-under=85
|
run: uv run pytest --cov-fail-under=85
|
||||||
|
|
||||||
security-audit:
|
|
||||||
name: Audit des dépendances
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/backend
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Récupère le dépôt
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Installe uv
|
|
||||||
uses: astral-sh/setup-uv@v5
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-dependency-glob: apps/backend/uv.lock
|
|
||||||
|
|
||||||
# L'audit porte sur le verrou, pas sur l'environnement : sinon pip-audit auditerait
|
|
||||||
# aussi les paquets que son propre `--with` injecte, hors dépendances du projet.
|
|
||||||
- name: Audite les dépendances livrées
|
|
||||||
# Piège : sans `shell: bash`, un échec de `uv export` serait masqué par le pipe.
|
|
||||||
shell: bash
|
|
||||||
run: uv export --frozen --no-dev --no-emit-project --no-hashes | uvx pip-audit --requirement /dev/stdin --no-deps
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
name: Frontend
|
name: Frontend
|
||||||
|
# Pipeline à choix multiple
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -10,8 +11,8 @@ on:
|
|||||||
- "apps/frontend/**"
|
- "apps/frontend/**"
|
||||||
- ".github/workflows/frontend.yml"
|
- ".github/workflows/frontend.yml"
|
||||||
|
|
||||||
permissions:
|
# Ordre de lancement des jobs
|
||||||
contents: read
|
# build -> test -> sonarqube -> deploy
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -29,18 +30,6 @@ jobs:
|
|||||||
- run: npm run build
|
- run: npm run build
|
||||||
working-directory: apps/frontend
|
working-directory: apps/frontend
|
||||||
|
|
||||||
security-audit:
|
|
||||||
name: Audit des dépendances
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
# Seuil high : une vulnérabilité moderate de devDependency ne doit pas bloquer une livraison.
|
|
||||||
- run: npm audit --audit-level=high --package-lock-only
|
|
||||||
working-directory: apps/frontend
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -53,7 +42,7 @@ jobs:
|
|||||||
cache-dependency-path: apps/frontend/package-lock.json
|
cache-dependency-path: apps/frontend/package-lock.json
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
working-directory: apps/frontend
|
working-directory: apps/frontend
|
||||||
- run: npm test -- --watch=false --coverage
|
- run: npm test --watch=false --code-coverage --coverageReporters=lcov
|
||||||
working-directory: apps/frontend
|
working-directory: apps/frontend
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -25,11 +25,4 @@ export const routes: Routes = [
|
|||||||
(m) => m.SiteDetailPlaceholder,
|
(m) => m.SiteDetailPlaceholder,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'monitoring/sensors',
|
|
||||||
canActivate: [authGuard],
|
|
||||||
data: { role: 'admin' },
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./features/monitoring/sensor-status/sensor-status').then((m) => m.SensorStatusView),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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,9 +10,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard__actions">
|
<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>
|
<a routerLink="/sites" class="ev-link">Voir les sites</a>
|
||||||
<ev-button
|
<ev-button
|
||||||
class="logout-button"
|
class="logout-button"
|
||||||
|
|||||||
@@ -68,15 +68,6 @@ h2 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--link {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__label {
|
.card__label {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
|
|||||||
@@ -101,11 +101,8 @@ describe('Dashboard', () => {
|
|||||||
it('appelle logout et redirige vers /login au clic sur le bouton de déconnexion', () => {
|
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 statsMock = { getSummary: vi.fn().mockReturnValue(of({ total_sites: 7, sites: [] })) };
|
||||||
const alertsMock = { getAlerts: vi.fn().mockReturnValue(of([])) };
|
const alertsMock = { getAlerts: vi.fn().mockReturnValue(of([])) };
|
||||||
const authMock = {
|
const authMock = { logout: vi.fn().mockReturnValue(of(undefined)), clearSession: vi.fn() };
|
||||||
logout: vi.fn().mockReturnValue(of(undefined)),
|
|
||||||
clearSession: vi.fn(),
|
|
||||||
principal: vi.fn().mockReturnValue({ role: 'admin' }),
|
|
||||||
};
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [Dashboard],
|
imports: [Dashboard],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -134,7 +131,6 @@ describe('Dashboard', () => {
|
|||||||
const authMock = {
|
const authMock = {
|
||||||
logout: vi.fn().mockReturnValue(throwError(() => new Error('réseau indisponible'))),
|
logout: vi.fn().mockReturnValue(throwError(() => new Error('réseau indisponible'))),
|
||||||
clearSession: vi.fn(),
|
clearSession: vi.fn(),
|
||||||
principal: vi.fn().mockReturnValue({ role: 'admin' }),
|
|
||||||
};
|
};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [Dashboard],
|
imports: [Dashboard],
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const TON_PAR_SEVERITE: Record<AlertSeverity, BadgeTone> = {
|
|||||||
export class Dashboard implements OnInit {
|
export class Dashboard implements OnInit {
|
||||||
private statsService = inject(StatsService);
|
private statsService = inject(StatsService);
|
||||||
private alertsService = inject(AlertsService);
|
private alertsService = inject(AlertsService);
|
||||||
public auth = inject(AuthService);
|
private auth = inject(AuthService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
: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;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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[];
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user