diff --git a/ui/angular.json b/ui/angular.json
index 82734e5..b09b23c 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -86,7 +86,8 @@
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"port": 4200,
- "open": true
+ "open": true,
+ "proxyConfig": "proxy.conf.json"
},
"configurations": {
"production": {
diff --git a/ui/proxy.conf.json b/ui/proxy.conf.json
new file mode 100644
index 0000000..f5b181d
--- /dev/null
+++ b/ui/proxy.conf.json
@@ -0,0 +1,8 @@
+{
+ "/api": {
+ "target": "https://api.db.e.kb28.ch",
+ "secure": true,
+ "changeOrigin": true,
+ "logLevel": "info"
+ }
+}
diff --git a/ui/src/app/app.config.ts b/ui/src/app/app.config.ts
index fff2d5f..6137499 100644
--- a/ui/src/app/app.config.ts
+++ b/ui/src/app/app.config.ts
@@ -1,15 +1,16 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
-import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
+import { basicAuthInterceptor } from './interceptors/basic-auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimationsAsync(),
- provideHttpClient(),
+ provideHttpClient(withInterceptors([basicAuthInterceptor])),
],
};
diff --git a/ui/src/app/components/room-details-panel/room-details-panel.component.html b/ui/src/app/components/room-details-panel/room-details-panel.component.html
index 963eafb..e3c16dc 100644
--- a/ui/src/app/components/room-details-panel/room-details-panel.component.html
+++ b/ui/src/app/components/room-details-panel/room-details-panel.component.html
@@ -88,7 +88,7 @@
} @else {
-
No sensor installed in this room.
+ No data available currently.
}
diff --git a/ui/src/app/components/room-details-panel/room-details-panel.component.ts b/ui/src/app/components/room-details-panel/room-details-panel.component.ts
index f94953c..ba13c64 100644
--- a/ui/src/app/components/room-details-panel/room-details-panel.component.ts
+++ b/ui/src/app/components/room-details-panel/room-details-panel.component.ts
@@ -10,7 +10,7 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterModule } from '@angular/router';
-import { combineLatest, timer } from 'rxjs';
+import { combineLatest, concat, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { RoomLayout } from '../../config/rooms-layout.config';
@@ -58,8 +58,14 @@ export class RoomDetailsPanelComponent implements OnInit {
map(params => params.get('id') ?? ''),
switchMap(id => combineLatest([
this.roomService.getRoomById(id),
- timer(0, interval).pipe(switchMap(() => this.sensorService.getLatestReadingForRoom(id))),
- timer(0, interval).pipe(switchMap(() => this.sensorService.getHistoryForRoom(id))),
+ concat(
+ this.sensorService.getLatestReadingForRoom(id),
+ timer(interval, interval).pipe(switchMap(() => this.sensorService.getLatestReadingForRoom(id))),
+ ),
+ concat(
+ this.sensorService.getHistoryForRoom(id),
+ timer(interval, interval).pipe(switchMap(() => this.sensorService.getHistoryForRoom(id))),
+ ),
])),
takeUntilDestroyed(this.destroyRef),
).subscribe(([room, reading, history]) => {
diff --git a/ui/src/app/components/room-map/room-map.component.html b/ui/src/app/components/room-map/room-map.component.html
index 0d27692..97e344b 100644
--- a/ui/src/app/components/room-map/room-map.component.html
+++ b/ui/src/app/components/room-map/room-map.component.html
@@ -227,7 +227,7 @@
} @else {
- No sensor installed
+ No data available currently
}
}
diff --git a/ui/src/app/components/room-map/room-map.component.spec.ts b/ui/src/app/components/room-map/room-map.component.spec.ts
index 41ca260..5bd8cd9 100644
--- a/ui/src/app/components/room-map/room-map.component.spec.ts
+++ b/ui/src/app/components/room-map/room-map.component.spec.ts
@@ -1,4 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
import { provideRouter, Router } from '@angular/router';
import { of } from 'rxjs';
import { RoomMapComponent } from './room-map.component';
@@ -35,6 +37,8 @@ describe('RoomMapComponent', () => {
await TestBed.configureTestingModule({
imports: [RoomMapComponent],
providers: [
+ provideHttpClient(),
+ provideHttpClientTesting(),
provideRouter([]),
{ provide: RoomService, useValue: roomServiceSpy },
{ provide: SensorService, useValue: sensorServiceSpy },
@@ -53,7 +57,7 @@ describe('RoomMapComponent', () => {
it('should call getRooms() and getLatestReadings() on init', () => {
expect(roomServiceSpy.getRooms).toHaveBeenCalledOnceWith();
- expect(sensorServiceSpy.getLatestReadings).toHaveBeenCalledOnceWith();
+ expect(sensorServiceSpy.getLatestReadings).toHaveBeenCalledOnceWith(jasmine.any(Array));
});
it('should populate rooms signal after init', () => {
diff --git a/ui/src/app/components/room-map/room-map.component.ts b/ui/src/app/components/room-map/room-map.component.ts
index ba73bc9..a6514f7 100644
--- a/ui/src/app/components/room-map/room-map.component.ts
+++ b/ui/src/app/components/room-map/room-map.component.ts
@@ -14,7 +14,7 @@ import {
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
-import { Subscription, timer } from 'rxjs';
+import { Subscription, concat, timer } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { getCO2Color } from '../../config/co2-levels.config';
import { SensorReading } from '../../models/sensor-reading.model';
@@ -156,8 +156,12 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy {
// Charge les salles une fois, puis poll les capteurs selon l'intervalle configuré
this.pollSub = this.roomService.getRooms().pipe(
tap((layouts) => { this.roomLayouts = layouts; }),
- switchMap(() => timer(0, environment.polling.mapIntervalMs)),
- switchMap(() => this.sensorService.getLatestReadings()),
+ switchMap((rooms) => concat(
+ this.sensorService.getLatestReadings(rooms),
+ timer(environment.polling.mapIntervalMs, environment.polling.mapIntervalMs).pipe(
+ switchMap(() => this.sensorService.getLatestReadings(rooms)),
+ ),
+ )),
).subscribe((readings) => {
this.rooms.set(
this.roomLayouts.map((layout) => {
diff --git a/ui/src/app/config/co2-levels.config.ts b/ui/src/app/config/co2-levels.config.ts
index 1c2490d..26b63c0 100644
--- a/ui/src/app/config/co2-levels.config.ts
+++ b/ui/src/app/config/co2-levels.config.ts
@@ -11,11 +11,12 @@ export interface CO2Level {
}
export const CO2_LEVELS: CO2Level[] = [
- { label: 'Excellent', range: '< 800 ppm', color: '#4caf50', maxPpm: 800 },
- { label: 'Good', range: '800-1000 ppm', color: '#8bc34a', maxPpm: 1000 },
- { label: 'Moderate', range: '1000-1400 ppm', color: '#ffc107', maxPpm: 1400 },
- { label: 'Poor', range: '1400-2000 ppm', color: '#ff9800', maxPpm: 2000 },
- { label: 'Critical', range: '> 2000 ppm', color: '#f44336', maxPpm: Infinity },
+ { label: 'Excellent', range: '< 800 ppm', color: '#4caf50', maxPpm: 800 },
+ { label: 'Good', range: '800-1000 ppm', color: '#8bc34a', maxPpm: 1000 },
+ { label: 'Moderate', range: '1000-1200 ppm', color: '#ffc107', maxPpm: 1200 },
+ { label: 'Poor', range: '1200-1500 ppm', color: '#ff9800', maxPpm: 1500 },
+ { label: 'Very Poor', range: '1500-2000 ppm', color: '#ff5722', maxPpm: 2000 },
+ { label: 'Critical', range: '> 2000 ppm', color: '#f44336', maxPpm: Infinity },
];
export function getCO2Color(ppm: number): string {
diff --git a/ui/src/app/interceptors/basic-auth.interceptor.ts b/ui/src/app/interceptors/basic-auth.interceptor.ts
new file mode 100644
index 0000000..8a9cf42
--- /dev/null
+++ b/ui/src/app/interceptors/basic-auth.interceptor.ts
@@ -0,0 +1,12 @@
+import { HttpHandlerFn, HttpRequest } from '@angular/common/http';
+import { environment } from '../../environments/environment';
+
+export function basicAuthInterceptor(req: HttpRequest, next: HttpHandlerFn) {
+ if (!req.url.startsWith('/api') && !req.url.includes(environment.apiUrl)) {
+ return next(req);
+ }
+ const { username, password } = environment.api;
+ if (!username && !password) return next(req);
+ const token = btoa(`${username}:${password}`);
+ return next(req.clone({ setHeaders: { Authorization: `Basic ${token}` } }));
+}
diff --git a/ui/src/app/services/room.service.spec.ts b/ui/src/app/services/room.service.spec.ts
index 1b2e56f..40d3589 100644
--- a/ui/src/app/services/room.service.spec.ts
+++ b/ui/src/app/services/room.service.spec.ts
@@ -32,12 +32,12 @@ describe('RoomService', () => {
req.flush([]);
});
- it('should return HTTP response data', () => {
- const mockData = [ROOM_LAYOUTS[0]];
+ it('should return mapped RoomLayouts for known IDs', () => {
+ const expected = ROOM_LAYOUTS.filter(r => r.id === 'A2' || r.id === 'A3');
service.getRooms().subscribe(rooms => {
- expect(rooms).toEqual(mockData);
+ expect(rooms).toEqual(expected);
});
- httpMock.expectOne(`${environment.apiUrl}/rooms`).flush(mockData);
+ httpMock.expectOne(`${environment.apiUrl}/rooms`).flush(['A2', 'A3']);
});
it('should fall back to ROOM_LAYOUTS mock on HTTP error', done => {
@@ -51,40 +51,19 @@ describe('RoomService', () => {
});
describe('getRoomById()', () => {
- it('should call GET /rooms/:id', () => {
- service.getRoomById('A1').subscribe();
- const req = httpMock.expectOne(`${environment.apiUrl}/rooms/A1`);
- expect(req.request.method).toBe('GET');
- req.flush(ROOM_LAYOUTS[0]);
- });
-
- it('should return the correct room from HTTP', () => {
- const mockRoom = ROOM_LAYOUTS.find(r => r.id === 'A1')!;
- service.getRoomById('A1').subscribe(room => {
- expect(room).toEqual(mockRoom);
- });
- httpMock.expectOne(`${environment.apiUrl}/rooms/A1`).flush(mockRoom);
- });
-
- it('should fall back to mock room on HTTP error', done => {
+ it('should return the matching room from local layout', done => {
const expected = ROOM_LAYOUTS.find(r => r.id === 'A1');
service.getRoomById('A1').subscribe(room => {
expect(room).toEqual(expected);
done();
});
- httpMock
- .expectOne(`${environment.apiUrl}/rooms/A1`)
- .error(new ProgressEvent('network error'));
});
- it('should return undefined for unknown id on HTTP error', done => {
+ it('should return undefined for unknown id', done => {
service.getRoomById('UNKNOWN').subscribe(room => {
expect(room).toBeUndefined();
done();
});
- httpMock
- .expectOne(`${environment.apiUrl}/rooms/UNKNOWN`)
- .error(new ProgressEvent('network error'));
});
});
});
diff --git a/ui/src/app/services/room.service.ts b/ui/src/app/services/room.service.ts
index 22a6e64..477aaba 100644
--- a/ui/src/app/services/room.service.ts
+++ b/ui/src/app/services/room.service.ts
@@ -1,25 +1,31 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
-import { Observable, catchError, of } from 'rxjs';
+import { Observable, catchError, map, of } from 'rxjs';
import { ROOM_LAYOUTS, RoomLayout } from '../config/rooms-layout.config';
import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' })
export class RoomService {
- private http = inject(HttpClient);
+ private http = inject(HttpClient);
private apiUrl = environment.apiUrl;
- private mockRooms: RoomLayout[] = ROOM_LAYOUTS;
-
+ /**
+ * Returns rooms that exist both in ROOM_LAYOUTS (with layout/sensor metadata)
+ * and in the backend (confirmed to have data). Falls back to all layouts.
+ */
getRooms(): Observable {
- return this.http
- .get(`${this.apiUrl}/rooms`)
- .pipe(catchError(() => of(this.mockRooms)));
+ return this.http.get(`${this.apiUrl}/rooms`).pipe(
+ map(ids => {
+ const matched = ids
+ .map(id => ROOM_LAYOUTS.find(r => r.id === id))
+ .filter((r): r is RoomLayout => r !== undefined);
+ return matched.length > 0 ? matched : ROOM_LAYOUTS;
+ }),
+ catchError(() => of(ROOM_LAYOUTS)),
+ );
}
getRoomById(id: string): Observable {
- return this.http
- .get(`${this.apiUrl}/rooms/${id}`)
- .pipe(catchError(() => of(this.mockRooms.find(r => r.id === id))));
+ return of(ROOM_LAYOUTS.find(r => r.id === id));
}
}
diff --git a/ui/src/app/services/sensor.service.spec.ts b/ui/src/app/services/sensor.service.spec.ts
index b3beec6..5c81b27 100644
--- a/ui/src/app/services/sensor.service.spec.ts
+++ b/ui/src/app/services/sensor.service.spec.ts
@@ -27,143 +27,72 @@ describe('SensorService', () => {
expect(service).toBeTruthy();
});
- describe('getLatestReadings()', () => {
- it('should call GET /sensors/latest', () => {
- service.getLatestReadings().subscribe();
- const req = httpMock.expectOne(`${environment.apiUrl}/sensors/latest`);
- expect(req.request.method).toBe('GET');
- req.flush([]);
+ describe('getLatestReadings(rooms)', () => {
+ it('should call GET /rooms/:id/current for each room', () => {
+ service.getLatestReadings(roomsWithSensors).subscribe();
+ roomsWithSensors.forEach(r =>
+ httpMock.expectOne(`${environment.apiUrl}/rooms/${r.id}/current`).flush({
+ time: new Date().toISOString(), co2_ppm: 600, temp: 21,
+ humidity: 45, window: false, room: r.id,
+ })
+ );
});
- it('should return HTTP response data', () => {
- const mockData = [
- {
- roomId: 'A1',
- roomName: 'A1',
- co2: 700,
- temperature: 21,
- humidity: 45,
- windowState: 'closed' as const,
- timestamp: new Date(),
- },
- ];
- service.getLatestReadings().subscribe(readings => {
- expect(readings).toEqual(mockData);
- });
- httpMock.expectOne(`${environment.apiUrl}/sensors/latest`).flush(mockData);
- });
-
- it('should fall back to one reading per sensor room on HTTP error', done => {
- service.getLatestReadings().subscribe(readings => {
- expect(readings.length).toBe(roomsWithSensors.length);
+ it('should fall back to mock data on HTTP error', done => {
+ service.getLatestReadings(roomsWithSensors).subscribe(readings => {
+ expect(readings.length).toBe(0);
done();
});
- httpMock
- .expectOne(`${environment.apiUrl}/sensors/latest`)
- .error(new ProgressEvent('network error'));
+ roomsWithSensors.forEach(r =>
+ httpMock.expectOne(`${environment.apiUrl}/rooms/${r.id}/current`)
+ .error(new ProgressEvent('network error'))
+ );
});
- it('fallback readings should only contain rooms with sensors', done => {
- service.getLatestReadings().subscribe(readings => {
- const ids = readings.map(r => r.roomId);
- const nonSensorRooms = ROOM_LAYOUTS.filter(r => !r.hasSensor).map(r => r.id);
- nonSensorRooms.forEach(id => expect(ids).not.toContain(id));
- done();
+ it('should only call rooms passed as argument', () => {
+ const subset = roomsWithSensors.slice(0, 1);
+ service.getLatestReadings(subset).subscribe();
+ httpMock.expectOne(`${environment.apiUrl}/rooms/${subset[0].id}/current`).flush({
+ time: new Date().toISOString(), co2_ppm: 500, temp: 20,
+ humidity: 50, window: true, room: subset[0].id,
});
- httpMock
- .expectOne(`${environment.apiUrl}/sensors/latest`)
- .error(new ProgressEvent('network error'));
});
});
describe('getLatestReadingForRoom()', () => {
- it('should call GET /sensors/:roomId/latest', () => {
+ it('should call GET /rooms/:roomId/current', () => {
service.getLatestReadingForRoom(firstRoomId).subscribe();
- const req = httpMock.expectOne(`${environment.apiUrl}/sensors/${firstRoomId}/latest`);
+ const req = httpMock.expectOne(`${environment.apiUrl}/rooms/${firstRoomId}/current`);
expect(req.request.method).toBe('GET');
- req.flush({});
+ req.flush({ time: new Date().toISOString(), co2_ppm: 600, temp: 21, humidity: 45, window: false, room: firstRoomId });
});
- it('should fall back to mock reading for given room on HTTP error', done => {
+ it('should return undefined on HTTP error', done => {
service.getLatestReadingForRoom(firstRoomId).subscribe(reading => {
- expect(reading).toBeDefined();
- expect(reading!.roomId).toBe(firstRoomId);
- done();
- });
- httpMock
- .expectOne(`${environment.apiUrl}/sensors/${firstRoomId}/latest`)
- .error(new ProgressEvent('network error'));
- });
-
- it('should return undefined for room without mock data on HTTP error', done => {
- service.getLatestReadingForRoom('UNKNOWN').subscribe(reading => {
expect(reading).toBeUndefined();
done();
});
httpMock
- .expectOne(`${environment.apiUrl}/sensors/UNKNOWN/latest`)
+ .expectOne(`${environment.apiUrl}/rooms/${firstRoomId}/current`)
.error(new ProgressEvent('network error'));
});
});
describe('getHistoryForRoom()', () => {
- it('should call GET /sensors/:roomId/history', () => {
+ it('should call GET /rooms/:roomId/history', () => {
service.getHistoryForRoom(firstRoomId).subscribe();
- const req = httpMock.expectOne(`${environment.apiUrl}/sensors/${firstRoomId}/history`);
+ const req = httpMock.expectOne(`${environment.apiUrl}/rooms/${firstRoomId}/history?window=1 day`);
expect(req.request.method).toBe('GET');
req.flush([]);
});
- it('should fall back to 24 entries per room on HTTP error', done => {
+ it('should return empty array on HTTP error', done => {
service.getHistoryForRoom(firstRoomId).subscribe(history => {
- expect(history.length).toBe(24);
- history.forEach(r => expect(r.roomId).toBe(firstRoomId));
+ expect(history).toEqual([]);
done();
});
httpMock
- .expectOne(`${environment.apiUrl}/sensors/${firstRoomId}/history`)
- .error(new ProgressEvent('network error'));
- });
-
- it('fallback history entries should have required fields', done => {
- service.getHistoryForRoom(firstRoomId).subscribe(history => {
- const entry = history[0];
- expect(entry.co2).toBeGreaterThan(0);
- expect(entry.temperature).toBeGreaterThan(0);
- expect(entry.humidity).toBeGreaterThan(0);
- expect(['open', 'closed']).toContain(entry.windowState);
- expect(entry.timestamp).toBeInstanceOf(Date);
- done();
- });
- httpMock
- .expectOne(`${environment.apiUrl}/sensors/${firstRoomId}/history`)
- .error(new ProgressEvent('network error'));
- });
-
- it('fallback latest reading equals first history entry for same room', done => {
- let latestReading: any;
- let historyEntries: any[];
-
- service.getLatestReadingForRoom(firstRoomId).subscribe(r => {
- latestReading = r;
- if (historyEntries) {
- expect(latestReading).toEqual(historyEntries[0]);
- done();
- }
- });
- httpMock
- .expectOne(`${environment.apiUrl}/sensors/${firstRoomId}/latest`)
- .error(new ProgressEvent('network error'));
-
- service.getHistoryForRoom(firstRoomId).subscribe(h => {
- historyEntries = h;
- if (latestReading) {
- expect(latestReading).toEqual(historyEntries[0]);
- done();
- }
- });
- httpMock
- .expectOne(`${environment.apiUrl}/sensors/${firstRoomId}/history`)
+ .expectOne(`${environment.apiUrl}/rooms/${firstRoomId}/history?window=1 day`)
.error(new ProgressEvent('network error'));
});
});
diff --git a/ui/src/app/services/sensor.service.ts b/ui/src/app/services/sensor.service.ts
index 42b4e5e..4830b6b 100644
--- a/ui/src/app/services/sensor.service.ts
+++ b/ui/src/app/services/sensor.service.ts
@@ -1,11 +1,35 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
-import { Observable, catchError, of } from 'rxjs';
-import { tap } from 'rxjs/operators';
+import { Observable, catchError, forkJoin, map, of } from 'rxjs';
import { SensorReading, WindowState } from '../models/sensor-reading.model';
-import { ROOM_LAYOUTS } from '../config/rooms-layout.config';
+import { ROOM_LAYOUTS, RoomLayout } from '../config/rooms-layout.config';
import { environment } from '../../environments/environment';
+// Raw shape returned by the Go API (InfluxDB row)
+interface GoReading {
+ time: string;
+ co2_ppm: number;
+ temp: number;
+ humidity: number;
+ window: boolean;
+ room: string;
+ campus?: string;
+ node?: string;
+}
+
+function toSensorReading(raw: GoReading): SensorReading {
+ const layout = ROOM_LAYOUTS.find(r => r.id === raw.room);
+ return {
+ roomId: raw.room,
+ roomName: layout?.name ?? raw.room,
+ co2: raw.co2_ppm ?? 0,
+ temperature: raw.temp ?? 0,
+ humidity: raw.humidity ?? 0,
+ windowState: (raw.window ? 'open' : 'closed') as WindowState,
+ timestamp: new Date(raw.time),
+ };
+}
+
const randomWindowState = (): WindowState => Math.random() > 0.5 ? 'open' : 'closed';
const MOCK_HISTORY: SensorReading[] = ROOM_LAYOUTS.filter(r => r.hasSensor).flatMap(room =>
@@ -23,40 +47,51 @@ const MOCK_HISTORY: SensorReading[] = ROOM_LAYOUTS.filter(r => r.hasSensor).flat
@Injectable({ providedIn: 'root' })
export class SensorService {
private http = inject(HttpClient);
- private apiUrl = environment.apiUrl ?? 'http://localhost:8080';
+ private apiUrl = environment.apiUrl;
- getLatestReadings(): Observable {
- const fallback = ROOM_LAYOUTS.filter(r => r.hasSensor)
- .map(room => MOCK_HISTORY.find(r => r.roomId === room.id))
+ /** Fetches the latest reading for the given rooms in parallel. */
+ getLatestReadings(rooms: RoomLayout[]): Observable {
+ const fallback = rooms
+ .map(r => MOCK_HISTORY.find(m => m.roomId === r.id))
.filter((r): r is SensorReading => r !== undefined);
- return this.http
- .get(`${this.apiUrl}/sensors/latest`)
- .pipe(
- tap({ error: (e) => console.error('[SensorService] getLatestReadings failed', e) }),
- catchError(() => of(fallback)),
- );
+ const requests = rooms.map(r =>
+ this.http.get(`${this.apiUrl}/rooms/${r.id}/current`).pipe(
+ map(toSensorReading),
+ catchError(() => of(null)),
+ )
+ );
+
+ return forkJoin(requests).pipe(
+ map(results => results.filter((r): r is SensorReading => r !== null)),
+ catchError((e) => {
+ console.error('[SensorService] getLatestReadings failed', e);
+ return of(fallback);
+ }),
+ );
}
getLatestReadingForRoom(roomId: string): Observable {
- const fallback = MOCK_HISTORY.find(r => r.roomId === roomId);
-
return this.http
- .get(`${this.apiUrl}/sensors/${roomId}/latest`)
+ .get(`${this.apiUrl}/rooms/${roomId}/current`)
.pipe(
- tap({ error: (e) => console.error(`[SensorService] getLatestReadingForRoom(${roomId}) failed`, e) }),
- catchError(() => of(fallback)),
+ map(toSensorReading),
+ catchError((e) => {
+ console.error(`[SensorService] getLatestReadingForRoom(${roomId}) failed`, e);
+ return of(undefined);
+ }),
);
}
getHistoryForRoom(roomId: string): Observable {
- const fallback = MOCK_HISTORY.filter(r => r.roomId === roomId);
-
return this.http
- .get(`${this.apiUrl}/sensors/${roomId}/history`)
+ .get(`${this.apiUrl}/rooms/${roomId}/history?window=1 day`)
.pipe(
- tap({ error: (e) => console.error(`[SensorService] getHistoryForRoom(${roomId}) failed`, e) }),
- catchError(() => of(fallback)),
+ map(rows => rows.map(toSensorReading)),
+ catchError((e) => {
+ console.error(`[SensorService] getHistoryForRoom(${roomId}) failed`, e);
+ return of([]);
+ }),
);
}
}
diff --git a/ui/src/environments/environment.prod.ts b/ui/src/environments/environment.prod.ts
index ea08429..24085c0 100644
--- a/ui/src/environments/environment.prod.ts
+++ b/ui/src/environments/environment.prod.ts
@@ -1,6 +1,6 @@
export const environment = {
production: true,
- apiUrl: 'https://ui.e.kb28.ch/api',
+ apiUrl: 'https://api.db.e.kb28.ch/api/v1',
wsUrl: 'wss://ui.e.kb28.ch/ws',
influxUrl: '',
logLevel: 'error',
@@ -8,4 +8,8 @@ export const environment = {
mapIntervalMs: 30_000,
detailIntervalMs: 15_000,
},
+ api: {
+ username: '__API_USERNAME__',
+ password: '__API_PASSWORD__',
+ },
};
diff --git a/ui/src/environments/environment.ts b/ui/src/environments/environment.ts
index 189a788..979acef 100644
--- a/ui/src/environments/environment.ts
+++ b/ui/src/environments/environment.ts
@@ -1,6 +1,7 @@
export const environment = {
production: false,
- apiUrl: 'http://localhost:8080',
+ // Proxied through proxy.conf.json → http://localhost:8080
+ apiUrl: '/api/v1',
wsUrl: 'ws://localhost:8080/ws',
influxUrl: '',
logLevel: 'debug',
@@ -8,4 +9,8 @@ export const environment = {
mapIntervalMs: 30_000,
detailIntervalMs: 15_000,
},
+ api: {
+ username: '',
+ password: '',
+ },
};