feat: Enhance system initialization and mock mode handling with improved UI feedback and error handling

This commit is contained in:
2025-08-06 22:53:59 +02:00
parent c5074bd34d
commit 76453d6c8d
7 changed files with 265 additions and 79 deletions

View File

@@ -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 (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-primary-600 mx-auto mb-4"></div>
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-red-600 mx-auto mb-4"></div>
<h2 className="text-xl font-semibold text-gray-900">
Initializing Tempering System...
{isLoading ? 'Initializing Tempering System...' : 'System Initialization'}
</h2>
<p className="text-gray-600 mt-2">
Please wait while we connect to the hardware
{error
? `Error: ${error}`
: isLoading
? 'Connecting to backend services...'
: 'Please wait while we set up the system'
}
</p>
{error && (
<button
onClick={() => initializeSystem()}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Retry Connection
</button>
)}
</div>
</div>
)

View File

@@ -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 (
<div className="bg-yellow-100 border-l-4 border-yellow-500 p-3 mb-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<span className="text-yellow-600 text-lg"></span>
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">
<strong>Demo Mode:</strong> Running without hardware connection.
All data is simulated for testing purposes.
</p>
</div>
</div>
</div>
)
}

View File

@@ -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<LayoutProps> = ({ children, isConnected }) => {
{/* Main content area */}
<main className="flex-1 overflow-auto p-4">
<div className="max-w-none">
<MockModeIndicator />
{children}
</div>
</main>

View File

@@ -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: '⚠️',
})
}
})

View File

@@ -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 <T>(
apiCall: () => Promise<T>,
mockCall: () => Promise<T>,
operationName: string
): Promise<T> => {
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 = <T>(response: AxiosResponse<T>): T => {
return response.data
@@ -90,18 +134,36 @@ const handleResponse = <T>(response: AxiosResponse<T>): T => {
class ApiService {
// System endpoints
async getSystemInfo(): Promise<SystemInfo> {
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<SystemStatus> {
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<RecipeList> {
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<Recipe> {
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<Recipe> {
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<Recipe> {
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<void> {
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<ProcessStatus> {
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<ProcessActionResponse> {
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<ProcessActionResponse> {
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<ProcessActionResponse> {
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<ProcessActionResponse> {
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<ProcessActionResponse> {
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

View File

@@ -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<ProcessStatus> {
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<ProcessActionResponse> {
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<ProcessActionResponse> {
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<any[]> {
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<any[]> {
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<void> {
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<void> {
async acknowledgeAlarm(_alarmId: string): Promise<void> {
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<any> {
async getProcessMetrics(_days: number = 30): Promise<any> {
await delay(500)
return {
total_sessions: 42,

View File

@@ -33,29 +33,46 @@ export const useSystemStore = create<SystemState>()(
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',