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:
khalil-bot
2026-04-15 12:11:13 +02:00
parent c5d65200ce
commit 3b803da147
19 changed files with 282 additions and 40 deletions

View File

@@ -1,12 +1,6 @@
name: Angular CI
on:
push:
branches:
- '**' # Run on all branches
paths:
- 'ui/**'
- '.github/workflows/angular-ci.yml'
pull_request:
branches:
- main
@@ -51,7 +45,6 @@ jobs:
# Run linting
- name: Run ESLint
run: npm run lint
continue-on-error: true # Don't fail build on lint warnings
# Check code formatting
- name: Check Prettier formatting
@@ -65,10 +58,9 @@ jobs:
- name: Build (Production)
run: npm run build -- --configuration=production
# Run unit tests (when available)
# Run unit tests
- name: Run unit tests
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)
- name: Upload coverage reports

2
ui/.gitignore vendored
View File

@@ -60,8 +60,6 @@ testem.log
.env
.env.local
.env.*.local
#src/environments/environment.prod.ts
# Keep environment.ts as template
# Build artifacts
*.tgz

View File

@@ -57,17 +57,17 @@ npm run e2e # E2E tests (Cypress)
### Development
- API: `http://localhost:8080`
- WebSocket: `ws://localhost:8080`
- WebSocket: `ws://localhost:8080/ws`
### Production
- API: `https://e2eeda.hes-so.ch/api`
- WebSocket: `wss://e2eeda.hes-so.ch/ws`
- API: `https://ui.e.kb28.ch/api`
- WebSocket: `wss://ui.e.kb28.ch/ws`
## Conventions
- Components: kebab-case (`room-map`)
- 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
- Signals for state management
@@ -112,5 +112,5 @@ docker run -p 80:80 e2eeda/ui:latest
**WebSocket connection failed:**
```typescript
// Check URL in environment.ts
wsUrl: 'ws://localhost:8080/ws' // port can change
wsUrl: 'ws://localhost:8080/ws' // port can change
```

View File

@@ -1,6 +1,6 @@
<header>
<div class="header-content">
<h1 routerLink="/dashboard">🌡️ PI_E2EEDA</h1>
<h1><a routerLink="/dashboard">🌡️ PI_E2EEDA</a></h1>
<nav>
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
</nav>

View File

@@ -16,10 +16,14 @@ header {
h1 {
margin: 0;
font-size: 24px;
cursor: pointer;
&:hover {
opacity: 0.9;
a {
color: inherit;
text-decoration: none;
&:hover {
opacity: 0.9;
}
}
}

View File

@@ -1,11 +1,6 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
interface CO2Level {
label: string;
range: string;
color: string;
}
import { CO2_LEVELS, CO2Level } from '../../config/co2-levels.config';
@Component({
selector: 'app-legend',
@@ -16,12 +11,5 @@ interface CO2Level {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LegendComponent {
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' },
];
levels: CO2Level[] = CO2_LEVELS;
}

View File

@@ -1,6 +1,9 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-room-details-panel',
standalone: true,
@@ -11,5 +14,5 @@ import { ActivatedRoute, RouterModule } from '@angular/router';
})
export class RoomDetailsPanelComponent {
private route = inject(ActivatedRoute);
roomId: string | null = this.route.snapshot.paramMap.get('id');
roomId = toSignal(this.route.paramMap.pipe(map((params) => params.get('id'))));
}

View 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' },
];

View 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,
};
}

View File

@@ -1,7 +1,7 @@
export const environment = {
production: true,
apiUrl: 'https://e2eeda.hes-so.ch/api',
wsUrl: 'wss://e2eeda.hes-so.ch/ws',
apiUrl: 'https://ui.e.kb28.ch/api',
wsUrl: 'wss://ui.e.kb28.ch/ws',
influxUrl: '',
logLevel: 'error',
};

View File

@@ -1,6 +1,6 @@
/**
* Template pour configuration environment
* Copier vers environment.prod.ts et remplir valeurs réelles
* Production environment configuration template
* Copy to environment.prod.ts and fill in actual values
*/
export const environment = {
production: true,

View File

@@ -1,7 +1,7 @@
export const environment = {
production: false,
apiUrl: 'http://localhost:8080',
wsUrl: 'ws://localhost:8080',
wsUrl: 'ws://localhost:8080/ws',
influxUrl: '',
logLevel: 'debug',
};