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 && (
+
initializeSystem()}
+ className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
+ >
+ Retry Connection
+
+ )}
)
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',