From e7041f2202a98261a95fed7b0cf11466731a849a Mon Sep 17 00:00:00 2001 From: damnthonyy Date: Wed, 20 May 2026 15:59:30 +0200 Subject: [PATCH 1/3] refactor: replace REST client with WebSocket implementation and enhance dashboard with robot health status ( mock env ) --- .env.example | 2 +- healhcheck.ts | 0 src/architecture/api/client.test.ts | 2 +- .../api/{client.ts => rest.client.ts} | 0 src/architecture/api/types.ts | 28 ++++++++++++ src/architecture/api/ws.client.test.ts | 12 ++--- src/architecture/api/ws.client.ts | 7 +-- src/architecture/gateways/robot.gateway.ts | 41 +++++++++++++++++ src/components/dashboard/dashboard.tsx | 10 ++++- src/components/dashboard/header.tsx | 32 ++++++++++--- src/hooks/use-robot-health.ts | 45 +++++++++++++++++++ 11 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 healhcheck.ts rename src/architecture/api/{client.ts => rest.client.ts} (100%) create mode 100644 src/architecture/gateways/robot.gateway.ts create mode 100644 src/hooks/use-robot-health.ts diff --git a/.env.example b/.env.example index 13bb897..a063446 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ # FRONT END NEXT_PUBLIC_API_URL=http://localhost:3001/api/ -NEXT_PUBLIC_FRONT_URL= \ No newline at end of file +NEXT_PUBLIC_ROSBRIDGE_URL=ws://localhost:9090 \ No newline at end of file diff --git a/healhcheck.ts b/healhcheck.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/architecture/api/client.test.ts b/src/architecture/api/client.test.ts index f18705a..2982786 100644 --- a/src/architecture/api/client.test.ts +++ b/src/architecture/api/client.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { HttpError, apiClient } from './client'; +import { HttpError, apiClient } from './rest.client'; const baseUrl = 'http://localhost:3001/api/'; let mockFetch: ReturnType; diff --git a/src/architecture/api/client.ts b/src/architecture/api/rest.client.ts similarity index 100% rename from src/architecture/api/client.ts rename to src/architecture/api/rest.client.ts diff --git a/src/architecture/api/types.ts b/src/architecture/api/types.ts index 092e7cf..d31c2e0 100644 --- a/src/architecture/api/types.ts +++ b/src/architecture/api/types.ts @@ -46,3 +46,31 @@ export interface WebSocketClientCallbacks { onClose?: (event: CloseEvent) => void; onError?: (event: Event) => void; } + +// --- Robot WebSocket Messages --- + +export interface RobotState { + is_connected: boolean; + battery_level: number; + linear_velocity: number; + ping_ms: number; + last_updated: string; +} + +export interface HealthData { + status: string; + adapter: string; + environment: string; + robot_state: RobotState; +} + +export type WsIncomingMessage = + | { type: 'health_response'; data: HealthData } + | { type: 'initial_state'; data: RobotState } + | { type: 'robot_state_updated'; data: RobotState } + | { type: 'pong' }; + +export type WsOutgoingMessage = + | { type: 'get_health' } + | { type: 'ping' } + | { type: 'get_state' }; diff --git a/src/architecture/api/ws.client.test.ts b/src/architecture/api/ws.client.test.ts index c5907de..6a624a5 100644 --- a/src/architecture/api/ws.client.test.ts +++ b/src/architecture/api/ws.client.test.ts @@ -88,19 +88,19 @@ afterEach(() => { }); describe('getWsBaseUrl via env override', () => { - const originalEnv = process.env.NEXT_PUBLIC_API_URL; + const originalEnv = process.env.NEXT_PUBLIC_ROSBRIDGE_URL; afterEach(() => { if (originalEnv === undefined) { - delete process.env.NEXT_PUBLIC_API_URL; + delete process.env.NEXT_PUBLIC_ROSBRIDGE_URL; } else { - process.env.NEXT_PUBLIC_API_URL = originalEnv; + process.env.NEXT_PUBLIC_ROSBRIDGE_URL = originalEnv; } }); it('converts https to wss', async () => { vi.resetModules(); - process.env.NEXT_PUBLIC_API_URL = 'https://robot.example.com/api/'; + process.env.NEXT_PUBLIC_ROSBRIDGE_URL = 'https://robot.example.com:9090'; const { WebSocketClient: WsClient } = await import('./ws.client'); const client = new WsClient({ path: 'status' }); client.connect(); @@ -110,11 +110,11 @@ describe('getWsBaseUrl via env override', () => { it('prepends ws:// when URL has no protocol', async () => { vi.resetModules(); - process.env.NEXT_PUBLIC_API_URL = 'robot.local:9090/api/'; + process.env.NEXT_PUBLIC_ROSBRIDGE_URL = 'robot.local:9090'; const { WebSocketClient: WsClient } = await import('./ws.client'); const client = new WsClient({ path: 'feed' }); client.connect(); - expect(capturedUrl).toBe('ws://robot.local:9090/api/feed'); + expect(capturedUrl).toBe('ws://robot.local:9090/feed'); }); }); diff --git a/src/architecture/api/ws.client.ts b/src/architecture/api/ws.client.ts index 990445d..98f0b6d 100644 --- a/src/architecture/api/ws.client.ts +++ b/src/architecture/api/ws.client.ts @@ -9,14 +9,15 @@ import { * Connection lifecycle, optional reconnect, typed message handling. */ -const API_BASE_URL = - process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/'; +const ROSBRIDGE_URL = + process.env.NEXT_PUBLIC_ROSBRIDGE_URL || 'ws://localhost:8765'; /** Builds WebSocket URL from HTTP base (http(s) -> ws(s)). */ function getWsBaseUrl(): string { - const url = API_BASE_URL.trim().replace(/\/$/, ''); + const url = ROSBRIDGE_URL.trim().replace(/\/$/, ''); if (url.startsWith('https://')) return url.replace('https://', 'wss://'); if (url.startsWith('http://')) return url.replace('http://', 'ws://'); + if (url.startsWith('wss://') || url.startsWith('ws://')) return url; return `ws://${url}`; } diff --git a/src/architecture/gateways/robot.gateway.ts b/src/architecture/gateways/robot.gateway.ts new file mode 100644 index 0000000..98fd712 --- /dev/null +++ b/src/architecture/gateways/robot.gateway.ts @@ -0,0 +1,41 @@ +import { WebSocketClient } from '../api/ws.client'; +import type { + WsIncomingMessage, + WebSocketClientCallbacks, + WebSocketConnectionState, +} from '../api/types'; + +export class RobotGateway { + private client: WebSocketClient; + + constructor() { + this.client = new WebSocketClient({ path: '' }); + } + + connect(callbacks: WebSocketClientCallbacks): void { + this.client.setCallbacks(callbacks); + this.client.connect(); + } + + disconnect(): void { + this.client.disconnect(); + } + + get connectionState(): WebSocketConnectionState { + return this.client.connectionState; + } + + getHealth(): void { + this.client.send({ type: 'get_health' }); + } + + ping(): void { + this.client.send({ type: 'ping' }); + } + + getState(): void { + this.client.send({ type: 'get_state' }); + } +} + +export const robotGateway = new RobotGateway(); diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index 68bb9cb..c061f2b 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -1,12 +1,13 @@ 'use client'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { Toaster, toast } from 'sonner'; import { Header } from '@/components/dashboard/header'; import { CameraFeed } from '@/components/dashboard/camera-feed'; import { ManualControl } from '@/components/dashboard/manual-control'; import { TaskManager } from '@/components/dashboard/task-manager'; import { MetricsAndAlerts } from '@/components/dashboard/metrics-and-alerts'; +import { useRobotHealth } from '@/hooks/use-robot-health'; const vitalsData = [ { time: '10:00', heartRate: 72, battery: 95 }, @@ -22,6 +23,7 @@ export function Dashboard() { const [isAutonomous, setIsAutonomous] = useState(true); const [speed, setSpeed] = useState([50]); const [activeTask, setActiveTask] = useState(null); + const { connectionState, health } = useRobotHealth(); const handleStartTask = (taskName: string) => { setActiveTask(taskName); @@ -45,7 +47,11 @@ export function Dashboard() {
-
+
diff --git a/src/components/dashboard/header.tsx b/src/components/dashboard/header.tsx index 428d0a0..b40191b 100644 --- a/src/components/dashboard/header.tsx +++ b/src/components/dashboard/header.tsx @@ -1,14 +1,28 @@ 'use client'; -import React from 'react'; import Image from 'next/image'; import { Battery, Wifi } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; +import type { + WebSocketConnectionState, + HealthData, +} from '@/architecture/api/types'; + interface HeaderProps { isAutonomous: boolean; + connectionState?: WebSocketConnectionState; + health?: HealthData | null; } -export function Header({ isAutonomous }: Readonly) { +export function Header({ + isAutonomous, + connectionState = 'closed', + health, +}: Readonly) { + const isConnected = connectionState === 'open'; + const robotState = health?.robot_state; + const battery = robotState?.battery_level ?? 0; + const ping = robotState?.ping_ms ?? 0; return (
@@ -31,14 +45,20 @@ export function Header({ isAutonomous }: Readonly) {
- + - Connecté (Ping: 12ms) + {isConnected ? `Connecté (Ping: ${ping}ms)` : 'Déconnecté'}
- - 60% + 50 ? 'text-green-500' : battery > 20 ? 'text-yellow-500' : 'text-red-500'}`} + /> + + {battery.toFixed(1)}% +
('closed'); + const [health, setHealth] = useState(null); + + useEffect(() => { + robotGateway.connect({ + onOpen: () => { + setConnectionState('open'); + robotGateway.getHealth(); + }, + onMessage: (msg: WsIncomingMessage) => { + if (msg.type === 'health_response') { + setHealth(msg.data); + } + }, + onClose: () => { + setConnectionState('closed'); + }, + onError: () => { + setConnectionState('closed'); + }, + }); + + return () => { + robotGateway.disconnect(); + }; + }, []); + + return { + connectionState, + health, + checkHealth: () => robotGateway.getHealth(), + }; +} From 9f5e860be0ae2db6bb3798a4b8ea18abadf6dcb4 Mon Sep 17 00:00:00 2001 From: damnthonyy Date: Wed, 20 May 2026 16:02:06 +0200 Subject: [PATCH 2/3] chore: pr-description-template --- .github/PULL_REQUEST_TEMPLATE.md | 36 +++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4045f45..0501c81 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ -## PR Type +## TroubleShooting - + ## Summary @@ -8,22 +8,38 @@ ## Description - + ## What's Changed -- [ ] -- [ ] -- [ ] + + +**Affected areas:** + +- User authentication & validation layer +- Email handling utilities +- Related middleware + + + +**Key files:** `UserService.ts`, `AuthMiddleware.ts`, `EmailValidator.ts` (new) + + + +**Details:** See commits or ask in thread for specific file breakdown + +## How to Test + + ## Screen - + -## Will +## Related - + --- - +Closes #XX or Fixes #XX From 3033cdb632d10e3333646bfb22a2e271a9ad280f Mon Sep 17 00:00:00 2001 From: damnthonyy Date: Wed, 20 May 2026 16:41:02 +0200 Subject: [PATCH 3/3] chore : delete empty file --- healhcheck.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 healhcheck.ts diff --git a/healhcheck.ts b/healhcheck.ts deleted file mode 100644 index e69de29..0000000