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: '', + }, };