From acea4de5997f0c6841f17065f05be72ddb653226 Mon Sep 17 00:00:00 2001 From: khalil-bot Date: Thu, 21 May 2026 09:18:27 +0200 Subject: [PATCH] style(ui): apply Prettier formatting Closes #30 Assisted-by: Claude:claude-sonnet-4-6 --- .../room-details-panel.component.html | 107 ++++++--- .../room-details-panel.component.scss | 214 +++++++++++++----- .../room-details-panel.component.ts | 60 ++--- .../room-map/room-map.component.html | 183 ++++++++++----- .../room-map/room-map.component.scss | 186 ++++++++++----- .../components/room-map/room-map.component.ts | 149 ++++++------ ui/src/app/config/co2-levels.config.ts | 12 +- ui/src/app/config/rooms-layout.config.ts | 169 +++++++++++--- ui/src/app/services/room.service.ts | 4 +- ui/src/app/services/sensor.service.spec.ts | 32 ++- ui/src/app/services/sensor.service.ts | 78 +++---- ui/src/environments/environment.prod.ts | 2 +- ui/src/environments/environment.template.ts | 2 +- ui/src/environments/environment.ts | 2 +- 14 files changed, 831 insertions(+), 369 deletions(-) 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 e3c16dc..ca0c969 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 @@ -1,10 +1,16 @@
-
@if (latestReading(); as reading) { - {{ getCO2Level(reading.co2).label }} · {{ reading.co2 }} ppm + {{ getCO2Level(reading.co2).label }} · {{ reading.co2 }} ppm } } @@ -29,10 +34,17 @@ @if (lastUpdated(); as lu) { - - + + + - Last updated {{ lu | date:'HH:mm:ss' }} + Last updated {{ lu | date: 'HH:mm:ss' }} } @@ -40,11 +52,9 @@ @if (room(); as r) {
-
diff --git a/ui/src/app/components/room-details-panel/room-details-panel.component.scss b/ui/src/app/components/room-details-panel/room-details-panel.component.scss index f97a505..64cf88d 100644 --- a/ui/src/app/components/room-details-panel/room-details-panel.component.scss +++ b/ui/src/app/components/room-details-panel/room-details-panel.component.scss @@ -1,19 +1,19 @@ // ── Tokens (aligned with room-map) ─────────────────────────────────────────── -$navy-deep: #0d1b2a; -$navy: #1a3a6b; -$navy-mid: #1f4e8c; -$navy-light: #cfe0f2; -$accent: #00b4d8; -$surface: #f4f7fb; -$border: #d1dce8; -$text-main: #0d1b2a; -$text-muted: #5a7694; -$success: #22c55e; +$navy-deep: #0d1b2a; +$navy: #1a3a6b; +$navy-mid: #1f4e8c; +$navy-light: #cfe0f2; +$accent: #00b4d8; +$surface: #f4f7fb; +$border: #d1dce8; +$text-main: #0d1b2a; +$text-muted: #5a7694; +$success: #22c55e; $success-border: #4ade80; -$success-bg: #f0fdf4; -$radius-lg: 10px; -$radius-md: 7px; -$radius-sm: 5px; +$success-bg: #f0fdf4; +$radius-lg: 10px; +$radius-md: 7px; +$radius-sm: 5px; // Breakpoints (identical to room-map) $bp-md: 768px; @@ -72,13 +72,21 @@ $t-fast: 0.15s ease; gap: 6px; color: $text-muted; text-decoration: none; - font: 500 13px/1 ui-sans-serif, system-ui, sans-serif; + font: + 500 13px/1 ui-sans-serif, + system-ui, + sans-serif; transition: color 0.15s; white-space: nowrap; - svg { width: 14px; height: 14px; } + svg { + width: 14px; + height: 14px; + } - &:hover { color: $navy; } + &:hover { + color: $navy; + } } .header-sep { @@ -87,7 +95,9 @@ $t-fast: 0.15s ease; background: $border; flex-shrink: 0; - @media (max-width: #{$bp-sm}) { display: none; } + @media (max-width: #{$bp-sm}) { + display: none; + } } .room-identity { @@ -97,7 +107,10 @@ $t-fast: 0.15s ease; } .room-eyebrow { - font: 600 9px/1 ui-sans-serif, system-ui, sans-serif; + font: + 600 9px/1 ui-sans-serif, + system-ui, + sans-serif; letter-spacing: 0.14em; text-transform: uppercase; color: $accent; @@ -105,7 +118,10 @@ $t-fast: 0.15s ease; .room-name { margin: 0; - font: 700 16px/1.2 ui-sans-serif, system-ui, sans-serif; + font: + 700 16px/1.2 ui-sans-serif, + system-ui, + sans-serif; color: $navy; white-space: nowrap; } @@ -115,33 +131,53 @@ $t-fast: 0.15s ease; align-items: center; padding: 4px 12px; border-radius: 20px; - font: 600 12px/1 ui-sans-serif, system-ui, sans-serif; + font: + 600 12px/1 ui-sans-serif, + system-ui, + sans-serif; color: rgba(0, 0, 0, 0.72); white-space: nowrap; letter-spacing: 0.01em; // La pill est redondante sur mobile (visible dans panel-left) - @media (max-width: #{$bp-sm}) { display: none; } + @media (max-width: #{$bp-sm}) { + display: none; + } } .header-spacer { flex: 1; - @media (max-width: #{$bp-md}) { display: none; } + @media (max-width: #{$bp-md}) { + display: none; + } } .last-updated { display: flex; align-items: center; gap: 5px; - font: 400 12px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 12px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; white-space: nowrap; - svg { width: 12px; height: 12px; flex-shrink: 0; color: $accent; } - strong { font-weight: 600; color: $text-main; } + svg { + width: 12px; + height: 12px; + flex-shrink: 0; + color: $accent; + } + strong { + font-weight: 600; + color: $text-main; + } - @media (max-width: #{$bp-sm}) { font-size: 11px; } + @media (max-width: #{$bp-sm}) { + font-size: 11px; + } } // ── Body ────────────────────────────────────────────────────────────────────── @@ -200,7 +236,9 @@ $t-fast: 0.15s ease; &::before { content: ''; position: absolute; - top: 0; left: 0; right: 0; + top: 0; + left: 0; + right: 0; height: 4px; background: var(--co2, #{$accent}); border-radius: $radius-lg $radius-lg 0 0; @@ -209,7 +247,10 @@ $t-fast: 0.15s ease; .co2-hero-label { display: block; - font: 600 10px/1 ui-sans-serif, system-ui, sans-serif; + font: + 600 10px/1 ui-sans-serif, + system-ui, + sans-serif; letter-spacing: 0.14em; text-transform: uppercase; color: $text-muted; @@ -226,7 +267,10 @@ $t-fast: 0.15s ease; } .co2-hero-unit { - font: 500 14px/1 ui-sans-serif, system-ui, sans-serif; + font: + 500 14px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; margin-top: 4px; } @@ -237,8 +281,11 @@ $t-fast: 0.15s ease; padding: 3px 12px; border-radius: 20px; background: var(--co2, #{$accent}); - color: rgba(0,0,0,0.72); - font: 600 11px/1.6 ui-sans-serif, system-ui, sans-serif; + color: rgba(0, 0, 0, 0.72); + font: + 600 11px/1.6 ui-sans-serif, + system-ui, + sans-serif; letter-spacing: 0.03em; } @@ -269,19 +316,27 @@ $t-fast: 0.15s ease; } .metric-label { - font: 400 11px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 11px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; text-transform: uppercase; letter-spacing: 0.08em; } .metric-value { - font: 700 26px/1.1 'Courier New', monospace; + font: + 700 26px/1.1 'Courier New', + monospace; color: $navy; } .metric-unit { - font: 500 13px/1 ui-sans-serif, system-ui, sans-serif; + font: + 500 13px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; margin-left: 2px; font-family: ui-sans-serif, system-ui, sans-serif; @@ -296,7 +351,9 @@ $t-fast: 0.15s ease; display: flex; align-items: center; justify-content: space-between; - transition: border-color $t-fast, background $t-fast; + transition: + border-color $t-fast, + background $t-fast; &.open { border-color: $success-border; @@ -318,14 +375,20 @@ $t-fast: 0.15s ease; } .window-label { - font: 400 11px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 11px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; text-transform: uppercase; letter-spacing: 0.08em; } .window-state { - font: 600 15px/1.4 ui-sans-serif, system-ui, sans-serif; + font: + 600 15px/1.4 ui-sans-serif, + system-ui, + sans-serif; color: $navy; text-transform: capitalize; } @@ -337,7 +400,9 @@ $t-fast: 0.15s ease; background: $border; transition: background $t-fast; - &.open { background: $success; } + &.open { + background: $success; + } } // ── Right panel ─────────────────────────────────────────────────────────────── @@ -373,12 +438,18 @@ $t-fast: 0.15s ease; .history-title { margin: 0; - font: 700 15px/1.2 ui-sans-serif, system-ui, sans-serif; + font: + 700 15px/1.2 ui-sans-serif, + system-ui, + sans-serif; color: $navy; } .history-sub { - font: 400 12px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 12px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; } @@ -400,9 +471,15 @@ $t-fast: 0.15s ease; background: #fff; color: $text-muted; cursor: pointer; - transition: background $t-fast, color $t-fast, border-color $t-fast; + transition: + background $t-fast, + color $t-fast, + border-color $t-fast; - svg { width: 12px; height: 12px; } + svg { + width: 12px; + height: 12px; + } &:hover:not(:disabled) { background: $navy; @@ -417,7 +494,9 @@ $t-fast: 0.15s ease; } .page-info { - font: 600 12px/1 'Courier New', monospace; + font: + 600 12px/1 'Courier New', + monospace; color: $text-muted; min-width: 44px; text-align: center; @@ -443,20 +522,28 @@ $t-fast: 0.15s ease; .history-table { width: 100%; border-collapse: collapse; - font: 400 13px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 13px/1 ui-sans-serif, + system-ui, + sans-serif; thead { position: sticky; top: 0; z-index: 1; - @media (max-width: #{$bp-md}) { position: static; } + @media (max-width: #{$bp-md}) { + position: static; + } } th { background: $surface; color: $text-muted; - font: 600 11px/1 ui-sans-serif, system-ui, sans-serif; + font: + 600 11px/1 ui-sans-serif, + system-ui, + sans-serif; text-transform: uppercase; letter-spacing: 0.07em; padding: 11px 16px; @@ -464,7 +551,10 @@ $t-fast: 0.15s ease; white-space: nowrap; border-bottom: 1px solid $border; - @media (max-width: #{$bp-sm}) { padding: 9px 10px; font-size: 10px; } + @media (max-width: #{$bp-sm}) { + padding: 9px 10px; + font-size: 10px; + } } td { @@ -474,7 +564,10 @@ $t-fast: 0.15s ease; vertical-align: middle; white-space: nowrap; - @media (max-width: #{$bp-sm}) { padding: 8px 10px; font-size: 12px; } + @media (max-width: #{$bp-sm}) { + padding: 8px 10px; + font-size: 12px; + } } tr:hover td { @@ -483,7 +576,9 @@ $t-fast: 0.15s ease; } .td-time { - font: 500 13px/1 'Courier New', monospace; + font: + 500 13px/1 'Courier New', + monospace; color: $text-muted; } @@ -491,17 +586,25 @@ $t-fast: 0.15s ease; display: inline-block; padding: 3px 10px; border-radius: 20px; - font: 600 12px/1.6 'Courier New', monospace; + font: + 600 12px/1.6 'Courier New', + monospace; color: rgba(0, 0, 0, 0.72); white-space: nowrap; } .win-state { - font: 500 12px/1 ui-sans-serif, system-ui, sans-serif; + font: + 500 12px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; text-transform: capitalize; - &.open { color: #16a34a; font-weight: 600; } + &.open { + color: #16a34a; + font-weight: 600; + } } // ── States ──────────────────────────────────────────────────────────────────── @@ -512,7 +615,10 @@ $t-fast: 0.15s ease; padding: 40px 24px; text-align: center; color: $text-muted; - font: 400 14px/1.5 ui-sans-serif, system-ui, sans-serif; + font: + 400 14px/1.5 ui-sans-serif, + system-ui, + sans-serif; font-style: italic; background: #fff; border-radius: $radius-lg; 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 ba13c64..d6e8569 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 @@ -30,19 +30,19 @@ const PAGE_SIZE = 8; changeDetection: ChangeDetectionStrategy.OnPush, }) export class RoomDetailsPanelComponent implements OnInit { - private route = inject(ActivatedRoute); - private roomService = inject(RoomService); + private route = inject(ActivatedRoute); + private roomService = inject(RoomService); private sensorService = inject(SensorService); - private destroyRef = inject(DestroyRef); + private destroyRef = inject(DestroyRef); - room = signal(undefined); + room = signal(undefined); latestReading = signal(undefined); - history = signal([]); - lastUpdated = signal(null); + history = signal([]); + lastUpdated = signal(null); currentPage = signal(0); - totalPages = computed(() => Math.ceil(this.history().length / PAGE_SIZE)); + totalPages = computed(() => Math.ceil(this.history().length / PAGE_SIZE)); pagedHistory = computed(() => { const start = this.currentPage() * PAGE_SIZE; return this.history().slice(start, start + PAGE_SIZE); @@ -54,26 +54,34 @@ export class RoomDetailsPanelComponent implements OnInit { ngOnInit(): void { const interval = environment.polling.detailIntervalMs; - this.route.paramMap.pipe( - map(params => params.get('id') ?? ''), - switchMap(id => combineLatest([ - this.roomService.getRoomById(id), - concat( - this.sensorService.getLatestReadingForRoom(id), - timer(interval, interval).pipe(switchMap(() => this.sensorService.getLatestReadingForRoom(id))), + this.route.paramMap + .pipe( + map(params => params.get('id') ?? ''), + switchMap(id => + combineLatest([ + this.roomService.getRoomById(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)) + ) + ), + ]) ), - concat( - this.sensorService.getHistoryForRoom(id), - timer(interval, interval).pipe(switchMap(() => this.sensorService.getHistoryForRoom(id))), - ), - ])), - takeUntilDestroyed(this.destroyRef), - ).subscribe(([room, reading, history]) => { - this.room.set(room); - this.latestReading.set(reading); - this.history.set(history ?? []); - if (reading) this.lastUpdated.set(new Date()); - }); + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(([room, reading, history]) => { + this.room.set(room); + this.latestReading.set(reading); + this.history.set(history ?? []); + if (reading) this.lastUpdated.set(new Date()); + }); } prevPage(): void { 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 97e344b..ef70a3a 100644 --- a/ui/src/app/components/room-map/room-map.component.html +++ b/ui/src/app/components/room-map/room-map.component.html @@ -1,10 +1,8 @@
-
-
@@ -14,42 +12,93 @@ @if (lastUpdatedStr(); as ts) { - - - + + + Last updated {{ ts }} } @@ -66,7 +115,6 @@ (pointercancel)="onPointerUp()" (pointerleave)="onPointerUp(); hideTooltip()" > - @if (isLoading()) {
@@ -80,7 +128,7 @@

- Fichier introuvable.
+ Fichier introuvable.
Copiez plan.svg dans le dossier public/ du projet Angular.

@@ -94,7 +142,6 @@ [style.width.px]="CANVAS_W" [style.height.px]="CANVAS_H" > -
@@ -110,14 +157,24 @@ - + - + @@ -134,7 +191,9 @@ [class.no-sensor]="!room.layout.hasSensor" [attr.transform]="'translate(' + pos.cx + ' ' + pos.cy + ')'" role="button" - [attr.aria-label]="room.layout.name + ' — CO2 : ' + (room.reading?.co2 ?? 'N/A') + ' ppm'" + [attr.aria-label]=" + room.layout.name + ' — CO2 : ' + (room.reading?.co2 ?? 'N/A') + ' ppm' + " (click)="navigateToRoom(room.layout.id)" (mouseenter)="showTooltip($event, room)" (mousemove)="showTooltip($event, room)" @@ -142,9 +201,12 @@ > @if (room.reading) { - + } {{ room.layout.id }} + > + {{ room.layout.id }} + - @if (room.reading) { {{ room.reading.co2 }} ppm } - @else { — } + @if (room.reading) { + {{ room.reading.co2 }} ppm + } @else { + — + } } } -
} @if (tooltip(); as t) { - - -
+
+ +
+ \ No newline at end of file +
+ diff --git a/ui/src/app/components/room-map/room-map.component.scss b/ui/src/app/components/room-map/room-map.component.scss index 92e3824..fbcfb49 100644 --- a/ui/src/app/components/room-map/room-map.component.scss +++ b/ui/src/app/components/room-map/room-map.component.scss @@ -5,28 +5,28 @@ // ── Tokens ─────────────────────────────────────────────────────────────────── -$navy-deep: #0d1b2a; -$navy: #1a3a6b; -$navy-mid: #1f4e8c; -$navy-light: #cfe0f2; -$accent: #00b4d8; -$surface: #f4f7fb; -$border: #d1dce8; -$text-main: #0d1b2a; -$text-muted: #5a7694; -$radius-lg: 10px; -$radius-md: 7px; -$radius-sm: 5px; -$ctrl-h: 34px; +$navy-deep: #0d1b2a; +$navy: #1a3a6b; +$navy-mid: #1f4e8c; +$navy-light: #cfe0f2; +$accent: #00b4d8; +$surface: #f4f7fb; +$border: #d1dce8; +$text-main: #0d1b2a; +$text-muted: #5a7694; +$radius-lg: 10px; +$radius-md: 7px; +$radius-sm: 5px; +$ctrl-h: 34px; // Breakpoints $bp-lg: 1100px; -$bp-md: 768px; -$bp-sm: 480px; +$bp-md: 768px; +$bp-sm: 480px; // Transitions -$t-fast: 0.12s ease; -$t-normal: 0.20s ease; +$t-fast: 0.12s ease; +$t-normal: 0.2s ease; // ── Layout global ───────────────────────────────────────────────────────────── @@ -84,7 +84,10 @@ $t-normal: 0.20s ease; } .sidebar-eyebrow { - font: 600 10px/1 ui-sans-serif, system-ui, sans-serif; + font: + 600 10px/1 ui-sans-serif, + system-ui, + sans-serif; letter-spacing: 0.14em; text-transform: uppercase; color: $accent; @@ -92,7 +95,10 @@ $t-normal: 0.20s ease; .sidebar-title { margin: 0; - font: 700 15px/1.2 ui-sans-serif, system-ui, sans-serif; + font: + 700 15px/1.2 ui-sans-serif, + system-ui, + sans-serif; color: $navy; } @@ -146,7 +152,10 @@ $t-normal: 0.20s ease; } .title-eyebrow { - font: 600 10px/1 ui-sans-serif, system-ui, sans-serif; + font: + 600 10px/1 ui-sans-serif, + system-ui, + sans-serif; letter-spacing: 0.14em; text-transform: uppercase; color: $accent; @@ -154,7 +163,10 @@ $t-normal: 0.20s ease; .title { margin: 0; - font: 700 18px/1.2 ui-sans-serif, system-ui, sans-serif; + font: + 700 18px/1.2 ui-sans-serif, + system-ui, + sans-serif; color: $navy; } @@ -164,13 +176,23 @@ $t-normal: 0.20s ease; display: flex; align-items: center; gap: 5px; - font: 500 11px/1 ui-sans-serif, system-ui, sans-serif; + font: + 500 11px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; letter-spacing: 0.02em; - svg { width: 12px; height: 12px; flex-shrink: 0; color: $accent; } + svg { + width: 12px; + height: 12px; + flex-shrink: 0; + color: $accent; + } - @media (max-width: 480px) { display: none; } + @media (max-width: 480px) { + display: none; + } } // ── Barre de contrôles ──────────────────────────────────────────────────────── @@ -195,28 +217,40 @@ $t-normal: 0.20s ease; background: transparent; color: #8ab0cc; cursor: pointer; - transition: background $t-fast, color $t-fast; + transition: + background $t-fast, + color $t-fast; &:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } - &:active { transform: scale(0.93); } + &:active { + transform: scale(0.93); + } - svg { display: block; } + svg { + display: block; + } } .ctrl-zoom { width: $ctrl-h; - svg { width: 14px; height: 14px; } + svg { + width: 14px; + height: 14px; + } } .ctrl-action { width: $ctrl-h; - svg { width: 15px; height: 15px; } + svg { + width: 15px; + height: 15px; + } } .ctrl-divider { @@ -227,7 +261,9 @@ $t-normal: 0.20s ease; } .scale-badge { - font: 600 12px/1 'Courier New', monospace; + font: + 600 12px/1 'Courier New', + monospace; color: $accent; min-width: 42px; text-align: center; @@ -259,19 +295,29 @@ $t-normal: 0.20s ease; linear-gradient(45deg, transparent 75%, #e2eaf4 75%), linear-gradient(-45deg, transparent 75%, #e2eaf4 75%); background-size: 24px 24px; - background-position: 0 0, 0 12px, 12px -12px, -12px 0; + background-position: + 0 0, + 0 12px, + 12px -12px, + -12px 0; - &.panning { cursor: grabbing; } + &.panning { + cursor: grabbing; + } // Hauteur explicite sur mobile (flex:1 ne suffit pas en colonne avec height:auto) - @media (max-width: 1100px) { min-height: 55vh; } - @media (max-width: 480px) { min-height: 50vh; border-radius: $radius-md; } + @media (max-width: 1100px) { + min-height: 55vh; + } + @media (max-width: 480px) { + min-height: 50vh; + border-radius: $radius-md; + } } // ── Canvas transformable ────────────────────────────────────────────────────── .canvas { - position: absolute; top: 0; left: 0; @@ -279,7 +325,6 @@ $t-normal: 0.20s ease; will-change: transform; // Ombre extérieure pour matérialiser les bords du plan filter: drop-shadow(0 4px 24px rgba($navy-deep, 0.18)); - } // ── Hôte du SVG injecté ─────────────────────────────────────────────────────── @@ -322,7 +367,9 @@ $t-normal: 0.20s ease; cursor: pointer; .badge-bg { - transition: filter 0.15s ease, fill-opacity 0.15s ease; + transition: + filter 0.15s ease, + fill-opacity 0.15s ease; } &:hover .badge-bg { @@ -357,7 +404,10 @@ $t-normal: 0.20s ease; p { margin: 0; - font: 400 14px/1.5 ui-sans-serif, system-ui, sans-serif; + font: + 400 14px/1.5 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; text-align: center; } @@ -392,7 +442,9 @@ $t-normal: 0.20s ease; } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } // ── Hint viewport ───────────────────────────────────────────────────────────── @@ -408,7 +460,10 @@ $t-normal: 0.20s ease; background: rgba($navy-deep, 0.62); backdrop-filter: blur(4px); color: rgba(255, 255, 255, 0.7); - font: 400 10px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 10px/1 ui-sans-serif, + system-ui, + sans-serif; letter-spacing: 0.04em; padding: 5px 12px; border-radius: 20px; @@ -423,7 +478,11 @@ $t-normal: 0.20s ease; // Cache les spans "Mouse wheel" et "Drag" sur mobile, garde juste "Click" @media (max-width: 480px) { - .hint-mouse, .hint-drag, .hint-sep:not(:last-of-type) { display: none; } + .hint-mouse, + .hint-drag, + .hint-sep:not(:last-of-type) { + display: none; + } } } @@ -446,19 +505,31 @@ $t-normal: 0.20s ease; animation: tt-in 0.1s ease; @keyframes tt-in { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } } } .tt-name { - font: 700 13px/1.2 ui-sans-serif, system-ui, sans-serif; + font: + 700 13px/1.2 ui-sans-serif, + system-ui, + sans-serif; color: $navy-light; margin-bottom: 2px; } .tt-type { - font: 400 10px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 10px/1 ui-sans-serif, + system-ui, + sans-serif; color: $text-muted; text-transform: capitalize; margin-bottom: 8px; @@ -475,17 +546,30 @@ $t-normal: 0.20s ease; justify-content: space-between; align-items: center; gap: 16px; - font: 400 11px/1.8 ui-sans-serif, system-ui, sans-serif; + font: + 400 11px/1.8 ui-sans-serif, + system-ui, + sans-serif; - span { color: #6a90b0; } - strong { color: #fff; font-weight: 600; } + span { + color: #6a90b0; + } + strong { + color: #fff; + font-weight: 600; + } - strong.open { color: #69e09a; } + strong.open { + color: #69e09a; + } } .tt-empty { - font: 400 11px/1 ui-sans-serif, system-ui, sans-serif; + font: + 400 11px/1 ui-sans-serif, + system-ui, + sans-serif; color: #5a7694; font-style: italic; margin-top: 4px; -} \ No newline at end of file +} 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 a6514f7..d644917 100644 --- a/ui/src/app/components/room-map/room-map.component.ts +++ b/ui/src/app/components/room-map/room-map.component.ts @@ -40,9 +40,9 @@ export interface RoomMapEntry { // Les badges se chevauchent légèrement à l'échelle globale ; zoomer > 3× les sépare. // ───────────────────────────────────────────────────────────────────────────── const SVG_ROOM_CENTERS: Record = { - A8: { cx: 155500, cy: 9000 }, - A9: { cx: 177200, cy: 57000 }, - A7: { cx: 134800, cy: 84700 }, + A8: { cx: 155500, cy: 9000 }, + A9: { cx: 177200, cy: 57000 }, + A7: { cx: 134800, cy: 84700 }, A6: { cx: 166500, cy: 81200 }, A5: { cx: 134800, cy: 130200 }, A4: { cx: 166500, cy: 141500 }, @@ -51,10 +51,10 @@ const SVG_ROOM_CENTERS: Record = { A1: { cx: 110000, cy: 225000 }, // B rooms : labels hors de la zone visible analysée du SVG (section tronquée). // Décommentez et ajustez après inspection du fichier complet. - B4: { cx: 17000, cy: 138000 }, - B3: { cx: 17000, cy: 170000 }, - B2: { cx: 17000, cy: 205000 }, - B1: { cx: 52000, cy: 225000 }, + B4: { cx: 17000, cy: 138000 }, + B3: { cx: 17000, cy: 170000 }, + B2: { cx: 17000, cy: 205000 }, + B1: { cx: 52000, cy: 225000 }, }; // Dimensions du SVG original (attributs width/height du fichier Inkscape) @@ -74,21 +74,21 @@ const SCALE = 100; changeDetection: ChangeDetectionStrategy.OnPush, }) export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { - private http = inject(HttpClient); - private sanitizer = inject(DomSanitizer); - private router = inject(Router); - private roomService = inject(RoomService); + private http = inject(HttpClient); + private sanitizer = inject(DomSanitizer); + private router = inject(Router); + private roomService = inject(RoomService); private sensorService = inject(SensorService); - private zone = inject(NgZone); + private zone = inject(NgZone); @ViewChild('viewport') viewportRef!: ElementRef; // ── Données ────────────────────────────────────────────────────────────── - rooms = signal([]); - svgHtml = signal(null); - isLoading = signal(true); - svgError = signal(false); - tooltip = signal<{ room: RoomMapEntry; x: number; y: number } | null>(null); + rooms = signal([]); + svgHtml = signal(null); + isLoading = signal(true); + svgError = signal(false); + tooltip = signal<{ room: RoomMapEntry; x: number; y: number } | null>(null); lastUpdated = signal(null); lastUpdatedStr = computed(() => { @@ -98,36 +98,36 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { }); private pollSub?: Subscription; - private svgSub?: Subscription; + private svgSub?: Subscription; private fitTimer?: ReturnType; private roomLayouts: RoomLayout[] = []; // ── État pan / zoom ────────────────────────────────────────────────────── // Variables privées pour les mutations hors zone (wheel) - private _s = 1; + private _s = 1; private _tx = 0; private _ty = 0; - scale = signal(1); - tx = signal(0); - ty = signal(0); + scale = signal(1); + tx = signal(0); + ty = signal(0); isPanning = signal(false); - scalePct = computed(() => Math.round(this.scale() * 100)); + scalePct = computed(() => Math.round(this.scale() * 100)); transform = computed(() => `translate(${this.tx()}px,${this.ty()}px) scale(${this.scale()})`); // ── Constantes exposées au template ────────────────────────────────────── - readonly SVG_W = SVG_W; - readonly SVG_H = SVG_H; - readonly CANVAS_W = Math.round(SVG_W / SCALE); // 1843 px - readonly CANVAS_H = Math.round(SVG_H / SCALE); // 2457 px + readonly SVG_W = SVG_W; + readonly SVG_H = SVG_H; + readonly CANVAS_W = Math.round(SVG_W / SCALE); // 1843 px + readonly CANVAS_H = Math.round(SVG_H / SCALE); // 2457 px readonly MIN_SCALE = 0.04; readonly MAX_SCALE = 25; readonly NO_SENSOR_COLOR = '#94a3b8'; // ── Pan ────────────────────────────────────────────────────────────────── - private panStart = { x: 0, y: 0 }; + private panStart = { x: 0, y: 0 }; private panOrigin = { tx: 0, ty: 0 }; // Référence stable pour removeEventListener @@ -140,7 +140,7 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit(): void { // Chargement du plan SVG depuis public/ this.svgSub = this.http.get('/plan.svg', { responseType: 'text' }).subscribe({ - next: (raw) => { + next: raw => { const cleaned = raw .replace(/(]*?)\s+width="[^"]*"/, '$1') .replace(/(]*?)\s+height="[^"]*"/, '$1'); @@ -154,27 +154,34 @@ 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((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) => { - const reading = readings.find((r) => r.roomId === layout.id); - return { - layout, - reading, - color: reading ? getCO2Color(reading.co2) : this.NO_SENSOR_COLOR, - }; - }) - ); - this.lastUpdated.set(new Date()); - }); + this.pollSub = this.roomService + .getRooms() + .pipe( + tap(layouts => { + this.roomLayouts = layouts; + }), + 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 => { + const reading = readings.find(r => r.roomId === layout.id); + return { + layout, + reading, + color: reading ? getCO2Color(reading.co2) : this.NO_SENSOR_COLOR, + }; + }) + ); + this.lastUpdated.set(new Date()); + }); } ngAfterViewInit(): void { @@ -213,7 +220,7 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { const factor = newScale / this._s; this._tx = pivotX - factor * (pivotX - this._tx); this._ty = pivotY - factor * (pivotY - this._ty); - this._s = newScale; + this._s = newScale; // Re-entre dans la zone Angular pour déclencher la détection de changements this.zone.run(() => { this.scale.set(this._s); @@ -224,21 +231,25 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { onWheel(e: WheelEvent): void { e.preventDefault(); - const rect = this.viewportRef.nativeElement.getBoundingClientRect(); + const rect = this.viewportRef.nativeElement.getBoundingClientRect(); // Zoom progressif : ~12% par cran de molette const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12; this.applyZoom(this._s * factor, e.clientX - rect.left, e.clientY - rect.top); } - zoomIn(): void { this.applyZoom(this._s * 1.5, ...this.vpCenter()); } - zoomOut(): void { this.applyZoom(this._s / 1.5, ...this.vpCenter()); } + zoomIn(): void { + this.applyZoom(this._s * 1.5, ...this.vpCenter()); + } + zoomOut(): void { + this.applyZoom(this._s / 1.5, ...this.vpCenter()); + } fitToView(): void { const vp = this.viewportRef?.nativeElement; if (!vp) return; - const s = Math.min(vp.clientWidth / this.CANVAS_W, vp.clientHeight / this.CANVAS_H) * 0.90; - this._s = s; - this._tx = (vp.clientWidth - this.CANVAS_W * s) / 2; + const s = Math.min(vp.clientWidth / this.CANVAS_W, vp.clientHeight / this.CANVAS_H) * 0.9; + this._s = s; + this._tx = (vp.clientWidth - this.CANVAS_W * s) / 2; this._ty = (vp.clientHeight - this.CANVAS_H * s) / 2; this.scale.set(this._s); this.tx.set(this._tx); @@ -246,8 +257,12 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { } reset(): void { - this._s = 1; this._tx = 0; this._ty = 0; - this.scale.set(1); this.tx.set(0); this.ty.set(0); + this._s = 1; + this._tx = 0; + this._ty = 0; + this.scale.set(1); + this.tx.set(0); + this.ty.set(0); } private vpCenter(): [number, number] { @@ -262,7 +277,7 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { onPointerDown(e: PointerEvent): void { if (e.button !== 0) return; this.isPanning.set(true); - this.panStart = { x: e.clientX, y: e.clientY }; + this.panStart = { x: e.clientX, y: e.clientY }; this.panOrigin = { tx: this._tx, ty: this._ty }; (e.target as HTMLElement).setPointerCapture(e.pointerId); } @@ -275,7 +290,9 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { this.ty.set(this._ty); } - onPointerUp(): void { this.isPanning.set(false); } + onPointerUp(): void { + this.isPanning.set(false); + } // ═══════════════════════════════════════════════════════════════════════ // Tooltip & Navigation @@ -286,6 +303,10 @@ export class RoomMapComponent implements OnInit, AfterViewInit, OnDestroy { this.tooltip.set({ room, x: e.clientX - rect.left + 18, y: e.clientY - rect.top - 12 }); } - hideTooltip(): void { this.tooltip.set(null); } - navigateToRoom(id: string): void { this.router.navigate(['/room', id]); } -} \ No newline at end of file + hideTooltip(): void { + this.tooltip.set(null); + } + navigateToRoom(id: string): void { + this.router.navigate(['/room', id]); + } +} diff --git a/ui/src/app/config/co2-levels.config.ts b/ui/src/app/config/co2-levels.config.ts index 26b63c0..e2203fc 100644 --- a/ui/src/app/config/co2-levels.config.ts +++ b/ui/src/app/config/co2-levels.config.ts @@ -11,12 +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-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 }, + { 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/config/rooms-layout.config.ts b/ui/src/app/config/rooms-layout.config.ts index f87706b..b47d319 100644 --- a/ui/src/app/config/rooms-layout.config.ts +++ b/ui/src/app/config/rooms-layout.config.ts @@ -32,61 +32,178 @@ export interface RoomLayout { export const ROOM_LAYOUTS: RoomLayout[] = [ // ── Espace B (lower-left, x=29–361) ────────────────────────────────────── { - id: 'B4', name: 'B4', espace: 'B', floor: 1, type: 'meeting', - x: 29, y: 445, width: 332, height: 60, capacity: 12, hasSensor: true, + id: 'B4', + name: 'B4', + espace: 'B', + floor: 1, + type: 'meeting', + x: 29, + y: 445, + width: 332, + height: 60, + capacity: 12, + hasSensor: true, }, { - id: 'B3', name: 'B3', espace: 'B', floor: 1, type: 'office', - x: 29, y: 505, width: 332, height: 60, capacity: 8, hasSensor: true, + id: 'B3', + name: 'B3', + espace: 'B', + floor: 1, + type: 'office', + x: 29, + y: 505, + width: 332, + height: 60, + capacity: 8, + hasSensor: true, }, { - id: 'B2', name: 'B2', espace: 'B', floor: 1, type: 'classroom', - x: 29, y: 565, width: 332, height: 80, capacity: 20, hasSensor: true, + id: 'B2', + name: 'B2', + espace: 'B', + floor: 1, + type: 'classroom', + x: 29, + y: 565, + width: 332, + height: 80, + capacity: 20, + hasSensor: true, }, { - id: 'B1', name: 'B1', espace: 'B', floor: 1, type: 'lab', - x: 29, y: 645, width: 332, height: 50, capacity: 0, hasSensor: false, + id: 'B1', + name: 'B1', + espace: 'B', + floor: 1, + type: 'lab', + x: 29, + y: 645, + width: 332, + height: 50, + capacity: 0, + hasSensor: false, }, // ── Espace A — upper-right column (x=811–1095) ─────────────────────────── { - id: 'A8', name: 'A8', espace: 'A', floor: 1, type: 'office', - x: 811, y: 15, width: 142, height: 90, capacity: 10, hasSensor: true, + id: 'A8', + name: 'A8', + espace: 'A', + floor: 1, + type: 'office', + x: 811, + y: 15, + width: 142, + height: 90, + capacity: 10, + hasSensor: true, }, { - id: 'A9', name: 'A9', espace: 'A', floor: 1, type: 'classroom', - x: 953, y: 15, width: 142, height: 90, capacity: 40, hasSensor: true, + id: 'A9', + name: 'A9', + espace: 'A', + floor: 1, + type: 'classroom', + x: 953, + y: 15, + width: 142, + height: 90, + capacity: 40, + hasSensor: true, }, { - id: 'A7', name: 'A7', espace: 'A', floor: 1, type: 'classroom', - x: 811, y: 105, width: 284, height: 60, capacity: 40, hasSensor: true, + id: 'A7', + name: 'A7', + espace: 'A', + floor: 1, + type: 'classroom', + x: 811, + y: 105, + width: 284, + height: 60, + capacity: 40, + hasSensor: true, }, { - id: 'A6', name: 'A6', espace: 'A', floor: 1, type: 'classroom', - x: 811, y: 165, width: 284, height: 80, capacity: 30, hasSensor: true, + id: 'A6', + name: 'A6', + espace: 'A', + floor: 1, + type: 'classroom', + x: 811, + y: 165, + width: 284, + height: 80, + capacity: 30, + hasSensor: true, }, // ── Espace A — centre-right (A5 left of A4, same y-band) ───────────────── { - id: 'A5', name: 'A5', espace: 'A', floor: 1, type: 'conference', - x: 598, y: 245, width: 213, height: 80, capacity: 20, hasSensor: true, + id: 'A5', + name: 'A5', + espace: 'A', + floor: 1, + type: 'conference', + x: 598, + y: 245, + width: 213, + height: 80, + capacity: 20, + hasSensor: true, }, { - id: 'A4', name: 'A4', espace: 'A', floor: 1, type: 'classroom', - x: 811, y: 245, width: 284, height: 95, capacity: 60, hasSensor: true, + id: 'A4', + name: 'A4', + espace: 'A', + floor: 1, + type: 'classroom', + x: 811, + y: 245, + width: 284, + height: 95, + capacity: 60, + hasSensor: true, }, { - id: 'A2', name: 'A2', espace: 'A', floor: 1, type: 'classroom', - x: 811, y: 340, width: 284, height: 95, capacity: 60, hasSensor: true, + id: 'A2', + name: 'A2', + espace: 'A', + floor: 1, + type: 'classroom', + x: 811, + y: 340, + width: 284, + height: 95, + capacity: 60, + hasSensor: true, }, // ── Espace A — lower centre ─────────────────────────────────────────────── { - id: 'A3', name: 'A3', espace: 'A', floor: 1, type: 'lab', - x: 408, y: 445, width: 403, height: 60, capacity: 25, hasSensor: true, + id: 'A3', + name: 'A3', + espace: 'A', + floor: 1, + type: 'lab', + x: 408, + y: 445, + width: 403, + height: 60, + capacity: 25, + hasSensor: true, }, { - id: 'A1', name: 'A1', espace: 'A', floor: 1, type: 'meeting', - x: 598, y: 595, width: 261, height: 100, capacity: 15, hasSensor: true, + id: 'A1', + name: 'A1', + espace: 'A', + floor: 1, + type: 'meeting', + x: 598, + y: 595, + width: 261, + height: 100, + capacity: 15, + hasSensor: true, }, ]; diff --git a/ui/src/app/services/room.service.ts b/ui/src/app/services/room.service.ts index 477aaba..ccb6ed7 100644 --- a/ui/src/app/services/room.service.ts +++ b/ui/src/app/services/room.service.ts @@ -6,7 +6,7 @@ import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class RoomService { - private http = inject(HttpClient); + private http = inject(HttpClient); private apiUrl = environment.apiUrl; /** @@ -21,7 +21,7 @@ export class RoomService { .filter((r): r is RoomLayout => r !== undefined); return matched.length > 0 ? matched : ROOM_LAYOUTS; }), - catchError(() => of(ROOM_LAYOUTS)), + catchError(() => of(ROOM_LAYOUTS)) ); } diff --git a/ui/src/app/services/sensor.service.spec.ts b/ui/src/app/services/sensor.service.spec.ts index 5c81b27..829aa2f 100644 --- a/ui/src/app/services/sensor.service.spec.ts +++ b/ui/src/app/services/sensor.service.spec.ts @@ -32,8 +32,12 @@ describe('SensorService', () => { 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, + time: new Date().toISOString(), + co2_ppm: 600, + temp: 21, + humidity: 45, + window: false, + room: r.id, }) ); }); @@ -44,7 +48,8 @@ describe('SensorService', () => { done(); }); roomsWithSensors.forEach(r => - httpMock.expectOne(`${environment.apiUrl}/rooms/${r.id}/current`) + httpMock + .expectOne(`${environment.apiUrl}/rooms/${r.id}/current`) .error(new ProgressEvent('network error')) ); }); @@ -53,8 +58,12 @@ describe('SensorService', () => { 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, + time: new Date().toISOString(), + co2_ppm: 500, + temp: 20, + humidity: 50, + window: true, + room: subset[0].id, }); }); }); @@ -64,7 +73,14 @@ describe('SensorService', () => { service.getLatestReadingForRoom(firstRoomId).subscribe(); const req = httpMock.expectOne(`${environment.apiUrl}/rooms/${firstRoomId}/current`); expect(req.request.method).toBe('GET'); - req.flush({ time: new Date().toISOString(), co2_ppm: 600, temp: 21, humidity: 45, window: false, room: firstRoomId }); + req.flush({ + time: new Date().toISOString(), + co2_ppm: 600, + temp: 21, + humidity: 45, + window: false, + room: firstRoomId, + }); }); it('should return undefined on HTTP error', done => { @@ -81,7 +97,9 @@ describe('SensorService', () => { describe('getHistoryForRoom()', () => { it('should call GET /rooms/:roomId/history', () => { service.getHistoryForRoom(firstRoomId).subscribe(); - const req = httpMock.expectOne(`${environment.apiUrl}/rooms/${firstRoomId}/history?window=1 day`); + const req = httpMock.expectOne( + `${environment.apiUrl}/rooms/${firstRoomId}/history?window=1 day` + ); expect(req.request.method).toBe('GET'); req.flush([]); }); diff --git a/ui/src/app/services/sensor.service.ts b/ui/src/app/services/sensor.service.ts index 4830b6b..b558a2a 100644 --- a/ui/src/app/services/sensor.service.ts +++ b/ui/src/app/services/sensor.service.ts @@ -7,46 +7,46 @@ import { environment } from '../../environments/environment'; // Raw shape returned by the Go API (InfluxDB row) interface GoReading { - time: string; - co2_ppm: number; - temp: number; + time: string; + co2_ppm: number; + temp: number; humidity: number; - window: boolean; - room: string; - campus?: string; - node?: string; + 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, + 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), + timestamp: new Date(raw.time), }; } -const randomWindowState = (): WindowState => Math.random() > 0.5 ? 'open' : 'closed'; +const randomWindowState = (): WindowState => (Math.random() > 0.5 ? 'open' : 'closed'); const MOCK_HISTORY: SensorReading[] = ROOM_LAYOUTS.filter(r => r.hasSensor).flatMap(room => Array.from({ length: 24 }, (_, i) => ({ - roomId: room.id, - roomName: room.name, - co2: Math.floor(Math.random() * 800) + 300, + roomId: room.id, + roomName: room.name, + co2: Math.floor(Math.random() * 800) + 300, temperature: Math.round((Math.random() * 8 + 18) * 10) / 10, - humidity: Math.floor(Math.random() * 30 + 35), + humidity: Math.floor(Math.random() * 30 + 35), windowState: randomWindowState(), - timestamp: new Date(Date.now() - i * 60 * 60 * 1000), + timestamp: new Date(Date.now() - i * 60 * 60 * 1000), })) ); @Injectable({ providedIn: 'root' }) export class SensorService { - private http = inject(HttpClient); + private http = inject(HttpClient); private apiUrl = environment.apiUrl; /** Fetches the latest reading for the given rooms in parallel. */ @@ -58,40 +58,36 @@ export class SensorService { const requests = rooms.map(r => this.http.get(`${this.apiUrl}/rooms/${r.id}/current`).pipe( map(toSensorReading), - catchError(() => of(null)), + catchError(() => of(null)) ) ); return forkJoin(requests).pipe( map(results => results.filter((r): r is SensorReading => r !== null)), - catchError((e) => { + catchError(e => { console.error('[SensorService] getLatestReadings failed', e); return of(fallback); - }), + }) ); } getLatestReadingForRoom(roomId: string): Observable { - return this.http - .get(`${this.apiUrl}/rooms/${roomId}/current`) - .pipe( - map(toSensorReading), - catchError((e) => { - console.error(`[SensorService] getLatestReadingForRoom(${roomId}) failed`, e); - return of(undefined); - }), - ); + return this.http.get(`${this.apiUrl}/rooms/${roomId}/current`).pipe( + map(toSensorReading), + catchError(e => { + console.error(`[SensorService] getLatestReadingForRoom(${roomId}) failed`, e); + return of(undefined); + }) + ); } getHistoryForRoom(roomId: string): Observable { - return this.http - .get(`${this.apiUrl}/rooms/${roomId}/history?window=1 day`) - .pipe( - map(rows => rows.map(toSensorReading)), - catchError((e) => { - console.error(`[SensorService] getHistoryForRoom(${roomId}) failed`, e); - return of([]); - }), - ); + return this.http.get(`${this.apiUrl}/rooms/${roomId}/history?window=1 day`).pipe( + 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 24085c0..acd3e57 100644 --- a/ui/src/environments/environment.prod.ts +++ b/ui/src/environments/environment.prod.ts @@ -5,7 +5,7 @@ export const environment = { influxUrl: '', logLevel: 'error', polling: { - mapIntervalMs: 30_000, + mapIntervalMs: 30_000, detailIntervalMs: 15_000, }, api: { diff --git a/ui/src/environments/environment.template.ts b/ui/src/environments/environment.template.ts index ef9735a..2edbdd3 100644 --- a/ui/src/environments/environment.template.ts +++ b/ui/src/environments/environment.template.ts @@ -9,7 +9,7 @@ export const environment = { influxUrl: '', logLevel: 'error', polling: { - mapIntervalMs: 30_000, + mapIntervalMs: 30_000, detailIntervalMs: 15_000, }, }; diff --git a/ui/src/environments/environment.ts b/ui/src/environments/environment.ts index 979acef..4cdcb08 100644 --- a/ui/src/environments/environment.ts +++ b/ui/src/environments/environment.ts @@ -6,7 +6,7 @@ export const environment = { influxUrl: '', logLevel: 'debug', polling: { - mapIntervalMs: 30_000, + mapIntervalMs: 30_000, detailIntervalMs: 15_000, }, api: {