fix(ui): address code review feedback
- Fix CI: remove redundant push trigger, keep only pull_request on main - Fix CI: remove continue-on-error on lint and test steps - Fix accessibility: wrap h1 routerLink in <a> element in header - Fix reactivity: use paramMap observable instead of route.snapshot - Extract CO2 thresholds to co2-levels.config.ts - Fix environment.template.ts comment to English - Fix wsUrl in environment.ts to include /ws path - Fix production URLs: hes-so.ch -> ui.e.kb28.ch - Fix README: align Models convention with kebab-case .model.ts suffix - Remove redundant .gitkeep files from directories with README.md
This commit is contained in:
10
.github/workflows/angular-ci.yml
vendored
10
.github/workflows/angular-ci.yml
vendored
@@ -1,12 +1,6 @@
|
|||||||
name: Angular CI
|
name: Angular CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '**' # Run on all branches
|
|
||||||
paths:
|
|
||||||
- 'ui/**'
|
|
||||||
- '.github/workflows/angular-ci.yml'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -51,7 +45,6 @@ jobs:
|
|||||||
# Run linting
|
# Run linting
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
continue-on-error: true # Don't fail build on lint warnings
|
|
||||||
|
|
||||||
# Check code formatting
|
# Check code formatting
|
||||||
- name: Check Prettier formatting
|
- name: Check Prettier formatting
|
||||||
@@ -65,10 +58,9 @@ jobs:
|
|||||||
- name: Build (Production)
|
- name: Build (Production)
|
||||||
run: npm run build -- --configuration=production
|
run: npm run build -- --configuration=production
|
||||||
|
|
||||||
# Run unit tests (when available)
|
# Run unit tests
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm run test -- --watch=false --browsers=ChromeHeadless --code-coverage
|
run: npm run test -- --watch=false --browsers=ChromeHeadless --code-coverage
|
||||||
continue-on-error: true # Don't fail yet (no tests implemented)
|
|
||||||
|
|
||||||
# Upload coverage (when tests exist)
|
# Upload coverage (when tests exist)
|
||||||
- name: Upload coverage reports
|
- name: Upload coverage reports
|
||||||
|
|||||||
2
ui/.gitignore
vendored
2
ui/.gitignore
vendored
@@ -60,8 +60,6 @@ testem.log
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
#src/environments/environment.prod.ts
|
|
||||||
# Keep environment.ts as template
|
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|||||||
10
ui/README.md
10
ui/README.md
@@ -57,17 +57,17 @@ npm run e2e # E2E tests (Cypress)
|
|||||||
|
|
||||||
### Development
|
### Development
|
||||||
- API: `http://localhost:8080`
|
- API: `http://localhost:8080`
|
||||||
- WebSocket: `ws://localhost:8080`
|
- WebSocket: `ws://localhost:8080/ws`
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
- API: `https://e2eeda.hes-so.ch/api`
|
- API: `https://ui.e.kb28.ch/api`
|
||||||
- WebSocket: `wss://e2eeda.hes-so.ch/ws`
|
- WebSocket: `wss://ui.e.kb28.ch/ws`
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Components: kebab-case (`room-map`)
|
- Components: kebab-case (`room-map`)
|
||||||
- Services: camelCase with suffix (`analytics.service.ts`)
|
- Services: camelCase with suffix (`analytics.service.ts`)
|
||||||
- Models: PascalCase (`RoomData.ts`)
|
- Models: kebab-case with `.model.ts` suffix (`room-data.model.ts`)
|
||||||
- Standalone components only
|
- Standalone components only
|
||||||
- Signals for state management
|
- Signals for state management
|
||||||
|
|
||||||
@@ -112,5 +112,5 @@ docker run -p 80:80 e2eeda/ui:latest
|
|||||||
**WebSocket connection failed:**
|
**WebSocket connection failed:**
|
||||||
```typescript
|
```typescript
|
||||||
// Check URL in environment.ts
|
// Check URL in environment.ts
|
||||||
wsUrl: 'ws://localhost:8080/ws' // port can change
|
wsUrl: 'ws://localhost:8080/ws' // port can change
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1 routerLink="/dashboard">🌡️ PI_E2EEDA</h1>
|
<h1><a routerLink="/dashboard">🌡️ PI_E2EEDA</a></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
|
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ header {
|
|||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
a {
|
||||||
opacity: 0.9;
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { CO2_LEVELS, CO2Level } from '../../config/co2-levels.config';
|
||||||
interface CO2Level {
|
|
||||||
label: string;
|
|
||||||
range: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-legend',
|
selector: 'app-legend',
|
||||||
@@ -16,12 +11,5 @@ interface CO2Level {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class LegendComponent {
|
export class LegendComponent {
|
||||||
levels: CO2Level[] = [
|
levels: CO2Level[] = CO2_LEVELS;
|
||||||
{ label: 'Excellent', range: '< 800 ppm', color: '#4caf50' },
|
|
||||||
{ label: 'Good', range: '800-1000 ppm', color: '#8bc34a' },
|
|
||||||
{ label: 'Moderate', range: '1000-1200 ppm', color: '#ffc107' },
|
|
||||||
{ label: 'Poor', range: '1200-1500 ppm', color: '#ff9800' },
|
|
||||||
{ label: 'Very Poor', range: '1500-2000 ppm', color: '#ff5722' },
|
|
||||||
{ label: 'Critical', range: '> 2000 ppm', color: '#f44336' },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-room-details-panel',
|
selector: 'app-room-details-panel',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -11,5 +14,5 @@ import { ActivatedRoute, RouterModule } from '@angular/router';
|
|||||||
})
|
})
|
||||||
export class RoomDetailsPanelComponent {
|
export class RoomDetailsPanelComponent {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
roomId: string | null = this.route.snapshot.paramMap.get('id');
|
roomId = toSignal(this.route.paramMap.pipe(map((params) => params.get('id'))));
|
||||||
}
|
}
|
||||||
|
|||||||
19
ui/src/app/config/co2-levels.config.ts
Normal file
19
ui/src/app/config/co2-levels.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* CO2 Level Thresholds Configuration
|
||||||
|
* Defines air quality levels with their PPM ranges and display colors
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CO2Level {
|
||||||
|
label: string;
|
||||||
|
range: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CO2_LEVELS: CO2Level[] = [
|
||||||
|
{ label: 'Excellent', range: '< 800 ppm', color: '#4caf50' },
|
||||||
|
{ label: 'Good', range: '800-1000 ppm', color: '#8bc34a' },
|
||||||
|
{ label: 'Moderate', range: '1000-1200 ppm', color: '#ffc107' },
|
||||||
|
{ label: 'Poor', range: '1200-1500 ppm', color: '#ff9800' },
|
||||||
|
{ label: 'Very Poor', range: '1500-2000 ppm', color: '#ff5722' },
|
||||||
|
{ label: 'Critical', range: '> 2000 ppm', color: '#f44336' },
|
||||||
|
];
|
||||||
238
ui/src/app/config/rooms-layout.config.ts
Normal file
238
ui/src/app/config/rooms-layout.config.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Room Layout Configuration
|
||||||
|
* Defines all rooms in Espace A and Espace B with their SVG positions and metadata
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface RoomLayout {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
espace: 'A' | 'B';
|
||||||
|
floor: number;
|
||||||
|
type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
capacity?: number;
|
||||||
|
hasSensor: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROOM_LAYOUTS: RoomLayout[] = [
|
||||||
|
// ========================================
|
||||||
|
// ESPACE B (Left side - 4 rooms)
|
||||||
|
// ========================================
|
||||||
|
{
|
||||||
|
id: 'B1',
|
||||||
|
name: 'B1 - Meeting Room',
|
||||||
|
espace: 'B',
|
||||||
|
floor: 1,
|
||||||
|
type: 'meeting',
|
||||||
|
x: 50,
|
||||||
|
y: 80,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 12,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B2',
|
||||||
|
name: 'B2 - Lab',
|
||||||
|
espace: 'B',
|
||||||
|
floor: 1,
|
||||||
|
type: 'lab',
|
||||||
|
x: 50,
|
||||||
|
y: 240,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 20,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B3',
|
||||||
|
name: 'B3 - Office',
|
||||||
|
espace: 'B',
|
||||||
|
floor: 1,
|
||||||
|
type: 'office',
|
||||||
|
x: 50,
|
||||||
|
y: 400,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 8,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B4',
|
||||||
|
name: 'B4 - Storage',
|
||||||
|
espace: 'B',
|
||||||
|
floor: 1,
|
||||||
|
type: 'storage',
|
||||||
|
x: 50,
|
||||||
|
y: 560,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 0,
|
||||||
|
hasSensor: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ESPACE A (Right side - 9 rooms)
|
||||||
|
// ========================================
|
||||||
|
{
|
||||||
|
id: 'A1',
|
||||||
|
name: 'A1 - Auditorium',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'auditorium',
|
||||||
|
x: 410,
|
||||||
|
y: 240,
|
||||||
|
width: 220,
|
||||||
|
height: 300,
|
||||||
|
capacity: 150,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A2',
|
||||||
|
name: 'A2 - Lecture Hall',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'classroom',
|
||||||
|
x: 670,
|
||||||
|
y: 80,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 60,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A3',
|
||||||
|
name: 'A3 - Workshop',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'workshop',
|
||||||
|
x: 410,
|
||||||
|
y: 80,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 25,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A4',
|
||||||
|
name: 'A4 - Open Space',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'openspace',
|
||||||
|
x: 930,
|
||||||
|
y: 80,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 30,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A5',
|
||||||
|
name: 'A5 - Conference',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'conference',
|
||||||
|
x: 670,
|
||||||
|
y: 400,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 20,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A6',
|
||||||
|
name: 'A6 - Study Room',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'study',
|
||||||
|
x: 930,
|
||||||
|
y: 240,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 15,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A7',
|
||||||
|
name: 'A7 - Lab',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'lab',
|
||||||
|
x: 670,
|
||||||
|
y: 560,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 18,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A8',
|
||||||
|
name: 'A8 - Classroom',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'classroom',
|
||||||
|
x: 930,
|
||||||
|
y: 560,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 40,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A9',
|
||||||
|
name: 'A9 - Office',
|
||||||
|
espace: 'A',
|
||||||
|
floor: 1,
|
||||||
|
type: 'office',
|
||||||
|
x: 930,
|
||||||
|
y: 400,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
capacity: 10,
|
||||||
|
hasSensor: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room layout by ID
|
||||||
|
*/
|
||||||
|
export function getRoomById(roomId: string): RoomLayout | undefined {
|
||||||
|
return ROOM_LAYOUTS.find((room) => room.id === roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rooms by espace (A or B)
|
||||||
|
*/
|
||||||
|
export function getRoomsByEspace(espace: 'A' | 'B'): RoomLayout[] {
|
||||||
|
return ROOM_LAYOUTS.filter((room) => room.espace === espace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all rooms with sensors
|
||||||
|
*/
|
||||||
|
export function getRoomsWithSensors(): RoomLayout[] {
|
||||||
|
return ROOM_LAYOUTS.filter((room) => room.hasSensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of rooms
|
||||||
|
*/
|
||||||
|
export function getTotalRooms(): number {
|
||||||
|
return ROOM_LAYOUTS.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room center point (for tooltip positioning)
|
||||||
|
*/
|
||||||
|
export function getRoomCenter(roomId: string): { x: number; y: number } | null {
|
||||||
|
const room = getRoomById(roomId);
|
||||||
|
if (!room) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: room.x + room.width / 2,
|
||||||
|
y: room.y + room.height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
apiUrl: 'https://e2eeda.hes-so.ch/api',
|
apiUrl: 'https://ui.e.kb28.ch/api',
|
||||||
wsUrl: 'wss://e2eeda.hes-so.ch/ws',
|
wsUrl: 'wss://ui.e.kb28.ch/ws',
|
||||||
influxUrl: '',
|
influxUrl: '',
|
||||||
logLevel: 'error',
|
logLevel: 'error',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Template pour configuration environment
|
* Production environment configuration template
|
||||||
* Copier vers environment.prod.ts et remplir valeurs réelles
|
* Copy to environment.prod.ts and fill in actual values
|
||||||
*/
|
*/
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiUrl: 'http://localhost:8080',
|
apiUrl: 'http://localhost:8080',
|
||||||
wsUrl: 'ws://localhost:8080',
|
wsUrl: 'ws://localhost:8080/ws',
|
||||||
influxUrl: '',
|
influxUrl: '',
|
||||||
logLevel: 'debug',
|
logLevel: 'debug',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user