From 76453d6c8dfcf0ba65565e3faf9f98305832e39e Mon Sep 17 00:00:00 2001 From: Sami Alzein Date: Wed, 6 Aug 2025 22:53:59 +0200 Subject: [PATCH] feat: Enhance system initialization and mock mode handling with improved UI feedback and error handling --- python_rewrite/frontend/src/App.tsx | 32 ++- .../components/common/MockModeIndicator.tsx | 25 +++ .../frontend/src/components/layout/Layout.tsx | 2 + .../frontend/src/hooks/useWebSocket.ts | 11 +- python_rewrite/frontend/src/services/api.ts | 182 +++++++++++++++--- .../frontend/src/services/mockApi.ts | 71 ++++--- .../frontend/src/stores/systemStore.ts | 21 +- 7 files changed, 265 insertions(+), 79 deletions(-) create mode 100644 python_rewrite/frontend/src/components/common/MockModeIndicator.tsx diff --git a/python_rewrite/frontend/src/App.tsx b/python_rewrite/frontend/src/App.tsx index 13f312f..149ffb9 100644 --- a/python_rewrite/frontend/src/App.tsx +++ b/python_rewrite/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import { useEffect } from 'react' import { Routes, Route, Navigate } from 'react-router-dom' import { useWebSocket } from './hooks/useWebSocket' import { useSystemStore } from './stores/systemStore' @@ -19,16 +19,19 @@ import ErrorBoundary from './components/common/ErrorBoundary' function App() { const { connect, disconnect, isConnected } = useWebSocket() - const { initializeSystem, isInitialized } = useSystemStore() + const { initializeSystem, isInitialized, isLoading, error } = useSystemStore() useEffect(() => { - // Initialize the system on app start + // Initialize the system on app start (this will handle mock mode automatically) initializeSystem() - // Connect to WebSocket for real-time updates - connect() + // Try to connect to WebSocket for real-time updates (optional in mock mode) + const timer = setTimeout(() => { + connect() + }, 1000) // Small delay to let system initialize first return () => { + clearTimeout(timer) disconnect() } }, [initializeSystem, connect, disconnect]) @@ -37,13 +40,26 @@ function App() { return (
-
+

- Initializing Tempering System... + {isLoading ? 'Initializing Tempering System...' : 'System Initialization'}

- Please wait while we connect to the hardware + {error + ? `Error: ${error}` + : isLoading + ? 'Connecting to backend services...' + : 'Please wait while we set up the system' + }

+ {error && ( + + )}
) diff --git a/python_rewrite/frontend/src/components/common/MockModeIndicator.tsx b/python_rewrite/frontend/src/components/common/MockModeIndicator.tsx new file mode 100644 index 0000000..3ef38e8 --- /dev/null +++ b/python_rewrite/frontend/src/components/common/MockModeIndicator.tsx @@ -0,0 +1,25 @@ +import { useSystemStore } from '../../stores/systemStore' + +export function MockModeIndicator() { + const { systemInfo, isConnected } = useSystemStore() + + const isMockMode = systemInfo?.environment === 'mock' || !isConnected + + if (!isMockMode) return null + + return ( +
+
+
+ ⚠️ +
+
+

+ Demo Mode: Running without hardware connection. + All data is simulated for testing purposes. +

+
+
+
+ ) +} diff --git a/python_rewrite/frontend/src/components/layout/Layout.tsx b/python_rewrite/frontend/src/components/layout/Layout.tsx index d1c2c46..a3274be 100644 --- a/python_rewrite/frontend/src/components/layout/Layout.tsx +++ b/python_rewrite/frontend/src/components/layout/Layout.tsx @@ -2,6 +2,7 @@ import React, { ReactNode } from 'react' import Header from './Header' import Sidebar from './Sidebar' import StatusBar from './StatusBar' +import { MockModeIndicator } from '../common/MockModeIndicator' interface LayoutProps { children: ReactNode @@ -21,6 +22,7 @@ const Layout: React.FC = ({ children, isConnected }) => { {/* Main content area */}
+ {children}
diff --git a/python_rewrite/frontend/src/hooks/useWebSocket.ts b/python_rewrite/frontend/src/hooks/useWebSocket.ts index 67a992a..3fedc53 100644 --- a/python_rewrite/frontend/src/hooks/useWebSocket.ts +++ b/python_rewrite/frontend/src/hooks/useWebSocket.ts @@ -20,7 +20,7 @@ export const useWebSocket = (): UseWebSocketReturn => { const maxReconnectAttempts = 10 // Store actions - const { updateSystemStatus, updateConnectionStatus, setError } = useSystemStore() + const { updateSystemStatus, updateConnectionStatus } = useSystemStore() // Get WebSocket URL from environment const getSocketUrl = useCallback(() => { @@ -31,8 +31,8 @@ export const useWebSocket = (): UseWebSocketReturn => { // Handle reconnection with exponential backoff const scheduleReconnect = useCallback(() => { if (reconnectAttempts.current >= maxReconnectAttempts) { - console.error('Max reconnection attempts reached') - setError('Lost connection to server. Please refresh the page.') + console.warn('Max WebSocket reconnection attempts reached - continuing in mock mode') + // Don't set error, just stop trying to reconnect return } @@ -44,7 +44,7 @@ export const useWebSocket = (): UseWebSocketReturn => { reconnectTimeoutRef.current = setTimeout(() => { connect() }, delay) - }, [setError]) + }, []) // Connect to WebSocket const connect = useCallback(() => { @@ -143,8 +143,9 @@ export const useWebSocket = (): UseWebSocketReturn => { }, }) } else if (data.overall_status === 'warning') { - toast.warning('Safety warning', { + toast('Safety warning', { duration: 5000, + icon: '⚠️', }) } }) diff --git a/python_rewrite/frontend/src/services/api.ts b/python_rewrite/frontend/src/services/api.ts index accf38e..cdcd715 100644 --- a/python_rewrite/frontend/src/services/api.ts +++ b/python_rewrite/frontend/src/services/api.ts @@ -1,5 +1,6 @@ import axios, { AxiosResponse, AxiosError } from 'axios' import toast from 'react-hot-toast' +import { mockApi } from './mockApi' import type { SystemInfo, SystemStatus, @@ -19,6 +20,15 @@ import type { ApiError, } from '../types' +// Global flag to track if we should use mock mode +let useMockMode = false +let mockModeDetected = false + +// Check if we should use mock mode +const shouldUseMockMode = () => { + return useMockMode || import.meta.env.VITE_USE_MOCK_API === 'true' +} + // Create axios instance with default configuration const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000', @@ -81,6 +91,40 @@ apiClient.interceptors.response.use( } ) +// Helper function to handle API responses with mock fallback +const handleApiCall = async ( + apiCall: () => Promise, + mockCall: () => Promise, + operationName: string +): Promise => { + if (shouldUseMockMode()) { + console.info(`[MOCK MODE] Using mock API for ${operationName}`) + return await mockCall() + } + + try { + return await apiCall() + } catch (error) { + // If we get a connection error, switch to mock mode + if (error instanceof AxiosError && (!error.response || error.code === 'ECONNREFUSED' || error.code === 'ERR_NETWORK')) { + if (!mockModeDetected) { + console.warn('Backend unavailable, switching to mock mode') + toast.error('Hardware backend unavailable - running in demo mode', { + duration: 5000, + icon: '⚠️', + }) + mockModeDetected = true + useMockMode = true + } + console.info(`[MOCK MODE] Backend unavailable, using mock API for ${operationName}`) + return await mockCall() + } + + // Re-throw other errors + throw error + } +} + // Helper function to handle API responses const handleResponse = (response: AxiosResponse): T => { return response.data @@ -90,18 +134,36 @@ const handleResponse = (response: AxiosResponse): T => { class ApiService { // System endpoints async getSystemInfo(): Promise { - const response = await apiClient.get('/api/v1/info') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.get('/api/v1/info') + return handleResponse(response) + }, + () => mockApi.getSystemInfo(), + 'getSystemInfo' + ) } async getSystemStatus(): Promise { - const response = await apiClient.get('/api/v1/system/status') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.get('/api/v1/system/status') + return handleResponse(response) + }, + () => mockApi.getSystemStatus(), + 'getSystemStatus' + ) } async getHealthCheck(): Promise<{ status: string }> { - const response = await apiClient.get('/health') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.get('/health') + return handleResponse(response) + }, + () => mockApi.getHealthCheck(), + 'getHealthCheck' + ) } // Recipe endpoints @@ -111,58 +173,124 @@ class ApiService { active_only?: boolean search?: string }): Promise { - const response = await apiClient.get('/api/v1/recipes', { params }) - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.get('/api/v1/recipes', { params }) + return handleResponse(response) + }, + () => mockApi.getRecipes(params), + 'getRecipes' + ) } async getRecipe(id: number): Promise { - const response = await apiClient.get(`/api/v1/recipes/${id}`) - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.get(`/api/v1/recipes/${id}`) + return handleResponse(response) + }, + () => mockApi.getRecipe(id), + 'getRecipe' + ) } async createRecipe(recipe: RecipeCreate): Promise { - const response = await apiClient.post('/api/v1/recipes', recipe) - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.post('/api/v1/recipes', recipe) + return handleResponse(response) + }, + () => mockApi.createRecipe(recipe), + 'createRecipe' + ) } async updateRecipe(id: number, recipe: RecipeUpdate): Promise { - const response = await apiClient.put(`/api/v1/recipes/${id}`, recipe) - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.put(`/api/v1/recipes/${id}`, recipe) + return handleResponse(response) + }, + () => mockApi.updateRecipe(id, recipe), + 'updateRecipe' + ) } async deleteRecipe(id: number): Promise { - await apiClient.delete(`/api/v1/recipes/${id}`) + return handleApiCall( + async () => { + await apiClient.delete(`/api/v1/recipes/${id}`) + }, + () => mockApi.deleteRecipe(id), + 'deleteRecipe' + ) } // Process control endpoints async getProcessStatus(): Promise { - const response = await apiClient.get('/api/v1/process/status') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.get('/api/v1/process/status') + return handleResponse(response) + }, + () => mockApi.getProcessStatus(), + 'getProcessStatus' + ) } async startProcess(request: ProcessStartRequest): Promise { - const response = await apiClient.post('/api/v1/process/start', request) - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.post('/api/v1/process/start', request) + return handleResponse(response) + }, + () => mockApi.startProcess(request), + 'startProcess' + ) } async pauseProcess(): Promise { - const response = await apiClient.post('/api/v1/process/pause') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.post('/api/v1/process/pause') + return handleResponse(response) + }, + () => mockApi.pauseProcess(), + 'pauseProcess' + ) } async resumeProcess(): Promise { - const response = await apiClient.post('/api/v1/process/resume') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.post('/api/v1/process/resume') + return handleResponse(response) + }, + () => mockApi.resumeProcess(), + 'resumeProcess' + ) } async stopProcess(): Promise { - const response = await apiClient.post('/api/v1/process/stop') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.post('/api/v1/process/stop') + return handleResponse(response) + }, + () => mockApi.stopProcess(), + 'stopProcess' + ) } async emergencyStop(): Promise { - const response = await apiClient.post('/api/v1/process/emergency-stop') - return handleResponse(response) + return handleApiCall( + async () => { + const response = await apiClient.post('/api/v1/process/emergency-stop') + return handleResponse(response) + }, + () => mockApi.emergencyStop(), + 'emergencyStop' + ) } // Hardware endpoints diff --git a/python_rewrite/frontend/src/services/mockApi.ts b/python_rewrite/frontend/src/services/mockApi.ts index a98108d..2dbd192 100644 --- a/python_rewrite/frontend/src/services/mockApi.ts +++ b/python_rewrite/frontend/src/services/mockApi.ts @@ -204,7 +204,6 @@ const delay = (ms: number = 500) => new Promise(resolve => setTimeout(resolve, m // Mock API service class class MockApiService { - private currentUser: User = mockUsers[0] private recipes: Recipe[] = [...mockRecipes] private processStatus: ProcessStatus = { ...mockProcessStatus } private hardwareStatus: HardwareStatus = { ...mockHardwareStatus } @@ -244,7 +243,7 @@ class MockApiService { const search = params.search.toLowerCase() filteredRecipes = filteredRecipes.filter(r => r.name.toLowerCase().includes(search) || - r.description.toLowerCase().includes(search) + (r.description && r.description.toLowerCase().includes(search)) ) } @@ -273,6 +272,7 @@ class MockApiService { const newRecipe: Recipe = { ...recipe, id: Math.max(...this.recipes.map(r => r.id)) + 1, + is_active: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } @@ -309,12 +309,8 @@ class MockApiService { async getProcessStatus(): Promise { await delay(200) // Simulate temperature fluctuations if process is running - if (this.processStatus.is_running) { - const temps = this.processStatus.current_temperatures - temps.tank_bottom += (Math.random() - 0.5) * 0.2 - temps.tank_wall += (Math.random() - 0.5) * 0.2 - temps.pump += (Math.random() - 0.5) * 0.1 - temps.fountain += (Math.random() - 0.5) * 0.1 + if (this.processStatus.is_running && this.processStatus.current_temperature !== null) { + this.processStatus.current_temperature += (Math.random() - 0.5) * 0.2 } return { ...this.processStatus } } @@ -329,21 +325,15 @@ class MockApiService { this.processStatus = { ...this.processStatus, is_running: true, + status: 'running', current_phase: 'heating', recipe_id: recipe.id, recipe_name: recipe.name, - progress_percentage: 0, - phase_time_remaining: recipe.heating_time, - total_time_remaining: recipe.heating_time + recipe.cooling_time + recipe.working_time, - target_temperatures: { - tank_bottom: recipe.heating_temp, - tank_wall: recipe.heating_temp, - pump: recipe.heating_temp, - fountain: recipe.heating_temp, - }, + target_temperature: recipe.target_temperature_c, + duration_seconds: 0, session_id: `mock-session-${Date.now()}`, started_at: new Date().toISOString(), - estimated_completion: new Date(Date.now() + (recipe.heating_time + recipe.cooling_time + recipe.working_time) * 1000).toISOString(), + started_by: request.user_id || 'mock-user', } return { @@ -355,7 +345,7 @@ class MockApiService { async pauseProcess(): Promise { await delay(300) - this.processStatus.current_phase = 'paused' + this.processStatus.status = 'paused' return { success: true, message: 'Process paused successfully', @@ -364,7 +354,7 @@ class MockApiService { async resumeProcess(): Promise { await delay(300) - this.processStatus.current_phase = 'heating' // or whatever phase it was in + this.processStatus.status = 'running' return { success: true, message: 'Process resumed successfully', @@ -388,7 +378,7 @@ class MockApiService { this.processStatus = { ...mockProcessStatus, session_id: null, - error: 'Emergency stop activated', + status: 'error', } return { success: true, @@ -404,27 +394,33 @@ class MockApiService { async getTemperatures(): Promise { await delay(200) - return [ - { sensor: 'tank_bottom', temperature: this.hardwareStatus.temperatures.tank_bottom, timestamp: new Date().toISOString() }, - { sensor: 'tank_wall', temperature: this.hardwareStatus.temperatures.tank_wall, timestamp: new Date().toISOString() }, - { sensor: 'pump', temperature: this.hardwareStatus.temperatures.pump, timestamp: new Date().toISOString() }, - { sensor: 'fountain', temperature: this.hardwareStatus.temperatures.fountain, timestamp: new Date().toISOString() }, - ] + return this.hardwareStatus.temperature_sensors.map(sensor => ({ + sensor: sensor.id, + temperature: sensor.current_temp_c, + timestamp: new Date().toISOString(), + })) } async getMotorStatus(): Promise { await delay(200) - return [ - { motor: 'mixer', ...this.hardwareStatus.motors.mixer }, - { motor: 'pump', ...this.hardwareStatus.motors.pump }, - ] + return this.hardwareStatus.motors.map(motor => ({ + motor: motor.id, + name: motor.name, + is_running: motor.is_running, + current_speed_rpm: motor.current_speed_rpm, + target_speed_rpm: motor.target_speed_rpm, + current_amps: motor.current_amps, + status: motor.status, + })) } async setMotorSpeed(motorId: string, speed: number): Promise { await delay(400) - if (motorId in this.hardwareStatus.motors) { - (this.hardwareStatus.motors as any)[motorId].speed = speed - ;(this.hardwareStatus.motors as any)[motorId].running = speed > 0 + const motor = this.hardwareStatus.motors.find(m => m.id === motorId) + if (motor) { + motor.target_speed_rpm = speed + motor.current_speed_rpm = speed + motor.is_running = speed > 0 } } @@ -434,7 +430,7 @@ class MockApiService { return mockSafetyStatus } - async acknowledgeAlarm(alarmId: string): Promise { + async acknowledgeAlarm(_alarmId: string): Promise { await delay(300) // Mock alarm acknowledgment } @@ -481,8 +477,9 @@ class MockApiService { const newUser: User = { ...user, id: (mockUsers.length + 1).toString(), + is_active: true, created_at: new Date().toISOString(), - last_login: null, + last_login: undefined, } mockUsers.push(newUser) return newUser @@ -558,7 +555,7 @@ class MockApiService { }) } - async getProcessMetrics(days: number = 30): Promise { + async getProcessMetrics(_days: number = 30): Promise { await delay(500) return { total_sessions: 42, diff --git a/python_rewrite/frontend/src/stores/systemStore.ts b/python_rewrite/frontend/src/stores/systemStore.ts index d1f0994..98a32fb 100644 --- a/python_rewrite/frontend/src/stores/systemStore.ts +++ b/python_rewrite/frontend/src/stores/systemStore.ts @@ -33,29 +33,46 @@ export const useSystemStore = create()( isLoading: false, error: null, - // Initialize system - called on app startup + // Initialize system - called on app startup initializeSystem: async () => { const { isInitialized } = get() if (isInitialized) return + console.log('Starting system initialization...') set({ isLoading: true, error: null }) try { // Fetch system information + console.log('Fetching system info...') const systemInfo = await api.getSystemInfo() + console.log('System info received:', systemInfo) // Fetch initial system status + console.log('Fetching system status...') const systemStatus = await api.getSystemStatus() + console.log('System status received:', systemStatus) + + // Check if we're in mock mode based on the system info + const isMockMode = systemInfo.environment === 'mock' || systemStatus.hardware_status === 'disconnected' + console.log('Mock mode detected:', isMockMode) set({ systemInfo, systemStatus, isInitialized: true, + isConnected: !isMockMode, // Set connection status based on mock mode isLoading: false, error: null, }) + + // Show mock mode warning if applicable + if (isMockMode && systemInfo.environment === 'mock') { + console.info('✅ Running in mock mode - no hardware required') + } + + console.log('✅ System initialization completed successfully') } catch (error) { - console.error('Failed to initialize system:', error) + console.error('❌ Failed to initialize system:', error) set({ isLoading: false, error: error instanceof Error ? error.message : 'Failed to initialize system',