import React, { useState, useEffect, useCallback } from 'react' import { useWebSocket } from './hooks/useWebSocket' import { visionApi, type SystemStatus, type CameraStatus, type RecordingInfo } from './services/api' import { SystemHealthWidget } from './widgets/SystemHealthWidget' import { MqttStatusWidget } from './widgets/MqttStatusWidget' import { RecordingsCountWidget } from './widgets/RecordingsCountWidget' import { CameraCountWidget } from './widgets/CameraCountWidget' import { CameraCard } from './components/CameraCard' import { CameraPreviewModal } from './components/CameraPreviewModal' import { CameraConfigModal } from './components/CameraConfigModal' import { MqttDebugPanel } from './components/MqttDebugPanel' // Get WebSocket URL from environment or construct it const getWebSocketUrl = () => { const apiUrl = import.meta.env.VITE_VISION_API_URL || '/api' // If it's a relative path, use relative WebSocket URL if (apiUrl.startsWith('/')) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' return `${protocol}//${window.location.host}${apiUrl.replace(/\/api$/, '')}/ws` } // Convert http(s):// to ws(s):// const wsUrl = apiUrl.replace(/^http/, 'ws') return `${wsUrl.replace(/\/api$/, '')}/ws` } export default function App() { const [systemStatus, setSystemStatus] = useState(null) const [cameras, setCameras] = useState>({}) const [recordings, setRecordings] = useState>({}) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [lastUpdate, setLastUpdate] = useState(null) const [notification, setNotification] = useState<{ type: 'success' | 'error', message: string } | null>(null) // Modal states const [previewModalOpen, setPreviewModalOpen] = useState(false) const [previewCamera, setPreviewCamera] = useState(null) const [configModalOpen, setConfigModalOpen] = useState(false) const [selectedCamera, setSelectedCamera] = useState(null) const [debugPanelOpen, setDebugPanelOpen] = useState(false) // WebSocket connection const { isConnected, subscribe } = useWebSocket(getWebSocketUrl()) // Fetch initial data const fetchInitialData = useCallback(async () => { try { setError(null) const [status, camerasData, recordingsData] = await Promise.all([ visionApi.getSystemStatus(), visionApi.getCameras(), visionApi.getRecordings(), ]) setSystemStatus(status) setCameras(camerasData) setRecordings(recordingsData) setLastUpdate(new Date()) } catch (err) { // Don't set error state - let widgets show API is unavailable // Keep existing state so UI can still render console.error('Failed to fetch initial data:', err) setLastUpdate(new Date()) // Update timestamp even on error to show we tried } finally { setLoading(false) } }, []) // Set up WebSocket subscriptions for real-time updates useEffect(() => { const unsubscribeFunctions: Array<() => void> = [] // Subscribe to camera status changes unsubscribeFunctions.push( subscribe('camera_status_changed', (event) => { const { camera_name, status, is_recording } = event.data setCameras((prev) => ({ ...prev, [camera_name]: { ...prev[camera_name], status, is_recording, last_checked: new Date().toISOString(), }, })) setLastUpdate(new Date()) }) ) // Subscribe to recording started events unsubscribeFunctions.push( subscribe('recording_started', (event) => { const { camera_name } = event.data setCameras((prev) => ({ ...prev, [camera_name]: { ...prev[camera_name], is_recording: true, }, })) // Refresh recordings to get accurate count visionApi.getRecordings().then(setRecordings).catch(console.error) // Refresh system status to update counts visionApi.getSystemStatus().then(setSystemStatus).catch(console.error) setLastUpdate(new Date()) }) ) // Subscribe to recording stopped events unsubscribeFunctions.push( subscribe('recording_stopped', (event) => { const { camera_name } = event.data setCameras((prev) => ({ ...prev, [camera_name]: { ...prev[camera_name], is_recording: false, }, })) // Refresh recordings and system status Promise.all([ visionApi.getRecordings(), visionApi.getSystemStatus(), ]).then(([recordingsData, statusData]) => { setRecordings(recordingsData) setSystemStatus(statusData) }).catch(console.error) setLastUpdate(new Date()) }) ) // Subscribe to system status changes unsubscribeFunctions.push( subscribe('system_status_changed', () => { visionApi.getSystemStatus().then(setSystemStatus).catch(console.error) setLastUpdate(new Date()) }) ) // Subscribe to MQTT status changes unsubscribeFunctions.push( subscribe('mqtt_status_changed', () => { visionApi.getSystemStatus().then(setSystemStatus).catch(console.error) setLastUpdate(new Date()) }) ) return () => { unsubscribeFunctions.forEach((unsub) => unsub()) } }, [subscribe]) // Fetch initial data on mount useEffect(() => { fetchInitialData() }, [fetchInitialData]) // Camera action handlers const handleStartRecording = useCallback(async (cameraName: string) => { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const filename = `manual_${cameraName}_${timestamp}.mp4` const result = await visionApi.startRecording(cameraName, filename) if (result.success) { setNotification({ type: 'success', message: `Recording started: ${result.filename}` }) // Immediately update state optimistically (UI updates instantly) setCameras((prev) => ({ ...prev, [cameraName]: { ...prev[cameraName], is_recording: true, current_recording_file: result.filename, }, })) // Refresh camera status from API as backup (in case WebSocket is delayed) setTimeout(() => { visionApi.getCameras().then(setCameras).catch(console.error) }, 500) } else { setNotification({ type: 'error', message: `Failed: ${result.message}` }) } } catch (err) { setNotification({ type: 'error', message: err instanceof Error ? err.message : 'Unknown error' }) } }, []) const handleStopRecording = useCallback(async (cameraName: string) => { try { const result = await visionApi.stopRecording(cameraName) if (result.success) { setNotification({ type: 'success', message: 'Recording stopped' }) // Immediately update state optimistically (UI updates instantly) setCameras((prev) => ({ ...prev, [cameraName]: { ...prev[cameraName], is_recording: false, current_recording_file: null, }, })) // Refresh camera status from API as backup (in case WebSocket is delayed) setTimeout(() => { visionApi.getCameras().then(setCameras).catch(console.error) }, 500) } else { setNotification({ type: 'error', message: `Failed: ${result.message}` }) } } catch (err) { setNotification({ type: 'error', message: err instanceof Error ? err.message : 'Unknown error' }) } }, []) const handlePreviewModal = useCallback((cameraName: string) => { setPreviewCamera(cameraName) setPreviewModalOpen(true) // The modal will start streaming and notify us via onStreamStarted callback }, []) const handlePreviewNewWindow = useCallback(async (cameraName: string) => { try { // Start streaming before opening new window const result = await visionApi.startStream(cameraName) if (result.success) { // Immediately update camera status to show "Stop Streaming" button setCameras((prev) => ({ ...prev, [cameraName]: { ...prev[cameraName], status: 'streaming', }, })) // Open camera stream in new window/tab const streamUrl = visionApi.getStreamUrl(cameraName) window.open(streamUrl, '_blank') } else { setNotification({ type: 'error', message: `Failed to start stream: ${result.message}` }) } } catch (err) { setNotification({ type: 'error', message: err instanceof Error ? err.message : 'Failed to start stream' }) } }, []) const handleConfigure = useCallback((cameraName: string) => { setSelectedCamera(cameraName) setConfigModalOpen(true) }, []) const handleRestart = useCallback(async (cameraName: string) => { try { setNotification({ type: 'success', message: `Restarting camera ${cameraName}...` }) const result = await visionApi.reinitializeCamera(cameraName) if (result.success) { setNotification({ type: 'success', message: `Camera ${cameraName} restarted successfully` }) // Refresh camera status setTimeout(() => { visionApi.getCameras().then(setCameras).catch(console.error) visionApi.getSystemStatus().then(setSystemStatus).catch(console.error) }, 2000) // Wait 2 seconds for camera to reinitialize } else { setNotification({ type: 'error', message: `Failed: ${result.message}` }) } } catch (err) { setNotification({ type: 'error', message: err instanceof Error ? err.message : 'Unknown error' }) } }, []) const handleStopStreaming = useCallback(async (cameraName: string) => { try { const result = await visionApi.stopStream(cameraName) if (result.success) { setNotification({ type: 'success', message: 'Streaming stopped' }) // Immediately update camera status (UI updates instantly) setCameras((prev) => ({ ...prev, [cameraName]: { ...prev[cameraName], status: 'available', }, })) // Refresh camera status from API as backup setTimeout(() => { visionApi.getCameras().then(setCameras).catch(console.error) }, 500) } else { setNotification({ type: 'error', message: `Failed: ${result.message}` }) } } catch (err) { setNotification({ type: 'error', message: err instanceof Error ? err.message : 'Unknown error' }) } }, []) // Auto-hide notifications useEffect(() => { if (notification) { const timer = setTimeout(() => setNotification(null), 5000) return () => clearTimeout(timer) } }, [notification]) if (loading) { return (

Loading vision system...

) } if (error) { return (

Error loading vision system

{error}

) } const cameraCount = Object.keys(cameras).length const machineCount = systemStatus ? Object.keys(systemStatus.machines).length : 0 const activeRecordings = systemStatus?.active_recordings ?? 0 // Fix: Use recordings object length instead of total_recordings (which may be incorrect) const totalRecordings = Object.keys(recordings).length return (
{/* Header */}

Vision System

Monitor cameras, machines, and recording status

{lastUpdate && (

Last updated: {lastUpdate.toLocaleTimeString()} {isConnected ? ( Live Updates ) : ( Polling Mode )}

)}
{/* Status Widgets */}
setDebugPanelOpen(true)} />
{/* Cameras Grid */}

Cameras

Current status of all cameras in the system

{Object.entries(cameras).map(([cameraName, camera]) => ( ))}
{/* Notification */} {notification && (
{notification.type === 'success' ? ( ) : ( )}

{notification.message}

)} {/* Camera Preview Modal */} {previewCamera && ( { setPreviewModalOpen(false) setPreviewCamera(null) }} onStreamStarted={() => { // Update camera status when streaming starts setCameras((prev) => ({ ...prev, [previewCamera]: { ...prev[previewCamera], status: 'streaming', }, })) }} onStreamStopped={() => { // Update camera status when streaming stops setCameras((prev) => ({ ...prev, [previewCamera]: { ...prev[previewCamera], status: 'available', }, })) }} /> )} {/* Camera Configuration Modal */} {selectedCamera && ( { setConfigModalOpen(false) setSelectedCamera(null) }} onSuccess={(message) => { setNotification({ type: 'success', message }) // Refresh camera status visionApi.getCameras().then(setCameras).catch(console.error) }} onError={(error) => { setNotification({ type: 'error', message: error }) }} /> )} {/* MQTT Debug Panel */} setDebugPanelOpen(false)} />
) }