import { useState, useEffect, useRef, useCallback, useMemo, memo, startTransition } from 'react' import { visionApi, type SystemStatus, type CameraStatus, type MachineStatus, type StorageStats, type RecordingInfo, type MqttStatus, type MqttEventsResponse, type MqttEvent, formatBytes, formatDuration, formatUptime } from '../lib/visionApi' import { useAuth } from '../hooks/useAuth' import { CameraConfigModal } from './CameraConfigModal' import { CameraPreviewModal } from './CameraPreviewModal' // Memoized components to prevent unnecessary re-renders const SystemOverview = memo(({ systemStatus }: { systemStatus: SystemStatus }) => (
{systemStatus.system_started ? 'Online' : 'Offline'}
System Status
Uptime: {formatUptime(systemStatus.uptime_seconds)}
{systemStatus.mqtt_connected ? 'Connected' : 'Disconnected'}
MQTT Status
Last message: {systemStatus.last_mqtt_message || 'Never'}
{systemStatus.active_recordings} Active
Recordings
Total: {systemStatus.total_recordings}
{Object.keys(systemStatus.cameras).length} Cameras
Devices
{Object.keys(systemStatus.machines).length} Machines
)) const StorageOverview = memo(({ storageStats }: { storageStats: StorageStats }) => (

Storage

Storage usage and file statistics

{storageStats.total_files}
Total Files
{formatBytes(storageStats.total_size_bytes)}
Total Size
{formatBytes(storageStats.disk_usage.free)}
Free Space
{/* Disk Usage Bar */}
Disk Usage {Math.round((storageStats.disk_usage.used / storageStats.disk_usage.total) * 100)}% used
{formatBytes(storageStats.disk_usage.used)} used {formatBytes(storageStats.disk_usage.total)} total
{/* Per-Camera Statistics */} {Object.keys(storageStats.cameras).length > 0 && (

Files by Camera

{Object.entries(storageStats.cameras).map(([cameraName, stats]) => (
{cameraName}
Files: {stats.file_count}
Size: {formatBytes(stats.total_size_bytes)}
))}
)}
)) const CamerasStatus = memo(({ systemStatus, onConfigureCamera, onStartRecording, onStopRecording, onPreviewCamera, onStopStreaming }: { systemStatus: SystemStatus, onConfigureCamera: (cameraName: string) => void, onStartRecording: (cameraName: string) => Promise, onStopRecording: (cameraName: string) => Promise, onPreviewCamera: (cameraName: string) => void, onStopStreaming: (cameraName: string) => Promise }) => { const { isAdmin } = useAuth() return (

Cameras

Current status of all cameras in the system

{Object.entries(systemStatus.cameras).map(([cameraName, camera]) => { const friendlyName = camera.device_info?.friendly_name const hasDeviceInfo = !!camera.device_info const hasSerial = !!camera.device_info?.serial_number // Determine if camera is connected based on status const isConnected = camera.status === 'available' || camera.status === 'connected' || camera.status === 'streaming' const hasError = camera.status === 'error' const statusText = camera.status || 'unknown' const isStreaming = camera.status === 'streaming' return (

{friendlyName || cameraName} {friendlyName && ( ({cameraName}) )}

{isStreaming ? 'Streaming' : isConnected ? 'Connected' : hasError ? 'Error' : 'Disconnected'}
Status: {isStreaming ? 'Streaming' : statusText.charAt(0).toUpperCase() + statusText.slice(1)}
{camera.is_recording && (
Recording:
Active
)} {isStreaming && (
Streaming:
Live
)} {hasDeviceInfo && ( <> {camera.device_info.model && (
Model: {camera.device_info.model}
)} {hasSerial && (
Serial: {camera.device_info.serial_number}
)} {camera.device_info.firmware_version && (
Firmware: {camera.device_info.firmware_version}
)} )} {camera.last_frame_time && (
Last Frame: {new Date(camera.last_frame_time).toLocaleTimeString()}
)} {camera.frame_rate && (
Frame Rate: {camera.frame_rate.toFixed(1)} fps
)} {camera.last_checked && (
Last Checked: {new Date(camera.last_checked).toLocaleTimeString()}
)} {camera.current_recording_file && (
Recording File: {camera.current_recording_file}
)} {camera.last_error && (
Error: {camera.last_error}
)} {/* Camera Control Buttons */}
{/* Recording Controls */}
{!camera.is_recording ? ( ) : ( )}
{/* Preview and Streaming Controls */}
{/* Admin Configuration Button */} {isAdmin() && (
)}
) })}
) }) const RecentRecordings = memo(({ recordings, systemStatus }: { recordings: Record, systemStatus: SystemStatus | null }) => (

Recent Recordings

Latest recording sessions

{Object.entries(recordings).slice(0, 10).map(([recordingId, recording]) => { const camera = systemStatus?.cameras[recording.camera_name] const displayName = camera?.device_info?.friendly_name || recording.camera_name return ( ) })}
Camera Filename Status Duration Size Started
{displayName} {camera?.device_info?.friendly_name && (
({recording.camera_name})
)}
{recording.filename} {recording.status} {recording.duration_seconds ? formatDuration(recording.duration_seconds) : '-'} {recording.file_size_bytes ? formatBytes(recording.file_size_bytes) : '-'} {new Date(recording.start_time).toLocaleString()}
)) export function VisionSystem() { const [systemStatus, setSystemStatus] = useState(null) const [storageStats, setStorageStats] = useState(null) const [recordings, setRecordings] = useState>({}) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [refreshing, setRefreshing] = useState(false) const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true) const [refreshInterval, setRefreshInterval] = useState(5000) // 5 seconds default const [lastUpdateTime, setLastUpdateTime] = useState(null) const [mqttStatus, setMqttStatus] = useState(null) const [mqttEvents, setMqttEvents] = useState([]) // Camera configuration modal state const [configModalOpen, setConfigModalOpen] = useState(false) const [selectedCamera, setSelectedCamera] = useState(null) const [notification, setNotification] = useState<{ type: 'success' | 'error', message: string } | null>(null) // Camera preview modal state const [previewModalOpen, setPreviewModalOpen] = useState(false) const [previewCamera, setPreviewCamera] = useState(null) const intervalRef = useRef(null) const clearAutoRefresh = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current) intervalRef.current = null } }, []) const startAutoRefresh = useCallback(() => { clearAutoRefresh() if (autoRefreshEnabled && refreshInterval > 0) { intervalRef.current = setInterval(fetchData, refreshInterval) } }, [autoRefreshEnabled, refreshInterval]) useEffect(() => { fetchData() startAutoRefresh() return clearAutoRefresh }, [startAutoRefresh]) useEffect(() => { startAutoRefresh() }, [autoRefreshEnabled, refreshInterval, startAutoRefresh]) const fetchData = useCallback(async (showRefreshIndicator = true) => { try { setError(null) if (!systemStatus) { setLoading(true) } else if (showRefreshIndicator) { setRefreshing(true) } const [statusData, storageData, recordingsData, mqttStatusData, mqttEventsData] = await Promise.all([ visionApi.getSystemStatus(), visionApi.getStorageStats(), visionApi.getRecordings(), visionApi.getMqttStatus().catch(err => { console.warn('Failed to fetch MQTT status:', err) return null }), visionApi.getMqttEvents(10).catch(err => { console.warn('Failed to fetch MQTT events:', err) return { events: [], total_events: 0, last_updated: '' } }) ]) // If cameras don't have device_info, try to fetch individual camera status if (statusData.cameras) { const camerasNeedingInfo = Object.entries(statusData.cameras) .filter(([_, camera]) => !camera.device_info?.friendly_name) .map(([cameraName, _]) => cameraName) if (camerasNeedingInfo.length > 0) { console.log('Fetching individual camera info for:', camerasNeedingInfo) try { const individualCameraData = await Promise.all( camerasNeedingInfo.map(cameraName => visionApi.getCameraStatus(cameraName).catch(err => { console.warn(`Failed to get individual status for ${cameraName}:`, err) return null }) ) ) // Merge the individual camera data back into statusData camerasNeedingInfo.forEach((cameraName, index) => { const individualData = individualCameraData[index] if (individualData && individualData.device_info) { statusData.cameras[cameraName] = { ...statusData.cameras[cameraName], device_info: individualData.device_info } } }) } catch (err) { console.warn('Failed to fetch individual camera data:', err) } } } // Batch state updates to minimize re-renders using startTransition for non-urgent updates const updateTime = new Date() // Use startTransition for non-urgent state updates to keep the UI responsive startTransition(() => { setSystemStatus(statusData) setStorageStats(storageData) setRecordings(recordingsData) setLastUpdateTime(updateTime) // Update MQTT status and events if (mqttStatusData) { setMqttStatus(mqttStatusData) } if (mqttEventsData && mqttEventsData.events) { setMqttEvents(mqttEventsData.events) } }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch vision system data') console.error('Vision system fetch error:', err) // Don't disable auto-refresh on errors, just log them } finally { setLoading(false) setRefreshing(false) } }, [systemStatus]) // Camera configuration handlers const handleConfigureCamera = (cameraName: string) => { setSelectedCamera(cameraName) setConfigModalOpen(true) } const handleConfigSuccess = (message: string) => { setNotification({ type: 'success', message }) setTimeout(() => setNotification(null), 5000) } const handleConfigError = (message: string) => { setNotification({ type: 'error', message }) setTimeout(() => setNotification(null), 5000) } // Recording control handlers const handleStartRecording = 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}` }) // Refresh data to update recording status fetchData(false) } else { setNotification({ type: 'error', message: `Failed to start recording: ${result.message}` }) } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' setNotification({ type: 'error', message: `Error starting recording: ${errorMessage}` }) } } const handleStopRecording = async (cameraName: string) => { try { const result = await visionApi.stopRecording(cameraName) if (result.success) { setNotification({ type: 'success', message: `Recording stopped: ${result.filename}` }) // Refresh data to update recording status fetchData(false) } else { setNotification({ type: 'error', message: `Failed to stop recording: ${result.message}` }) } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' setNotification({ type: 'error', message: `Error stopping recording: ${errorMessage}` }) } } const handlePreviewCamera = (cameraName: string) => { setPreviewCamera(cameraName) setPreviewModalOpen(true) } const handleStopStreaming = async (cameraName: string) => { try { const result = await visionApi.stopStream(cameraName) if (result.success) { setNotification({ type: 'success', message: `Streaming stopped for ${cameraName}` }) // Refresh data to update camera status fetchData(false) } else { setNotification({ type: 'error', message: `Failed to stop streaming: ${result.message}` }) } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' setNotification({ type: 'error', message: `Error stopping stream: ${errorMessage}` }) } } const getStatusColor = (status: string, isRecording: boolean = false) => { // If camera is recording, always show red regardless of status if (isRecording) { return 'text-red-600 bg-red-100' } switch (status.toLowerCase()) { case 'available': case 'connected': case 'healthy': case 'on': return 'text-green-600 bg-green-100' case 'disconnected': case 'off': case 'failed': return 'text-red-600 bg-red-100' case 'error': case 'warning': case 'degraded': return 'text-yellow-600 bg-yellow-100' default: return 'text-yellow-600 bg-yellow-100' } } const getMachineStateColor = (state: string) => { switch (state.toLowerCase()) { case 'on': case 'running': return 'text-green-600 bg-green-100' case 'off': case 'stopped': return 'text-gray-600 bg-gray-100' default: return 'text-yellow-600 bg-yellow-100' } } if (loading) { return (

Loading vision system data...

) } if (error) { return (

Error loading vision system

{error}

) } return (
{/* Header */}

Vision System

Monitor cameras, machines, and recording status

{lastUpdateTime && (

Last updated: {lastUpdateTime.toLocaleTimeString()} {refreshing && ( Updating... )} {autoRefreshEnabled && !refreshing && ( Auto-refresh: {refreshInterval / 1000}s )}

)}
{/* Auto-refresh controls */}
{autoRefreshEnabled && ( )}
{/* Refresh indicator and button */}
{refreshing && (
)}
{/* System Overview */} {systemStatus && } {/* Cameras Status */} {systemStatus && ( )} {/* Machines Status */} {systemStatus && Object.keys(systemStatus.machines).length > 0 && (

Machines

Current status of all machines in the system

{Object.entries(systemStatus.machines).map(([machineName, machine]) => (

{machineName.replace(/_/g, ' ')}

{machine.state}
Last updated: {new Date(machine.last_updated).toLocaleTimeString()}
{machine.last_message && (
Last message: {machine.last_message}
)} {machine.mqtt_topic && (
MQTT topic: {machine.mqtt_topic}
)}
))}
)} {/* Storage Statistics */} {storageStats && } {/* Recent Recordings */} {Object.keys(recordings).length > 0 && } {/* Camera Configuration Modal */} {selectedCamera && ( { setConfigModalOpen(false) setSelectedCamera(null) }} onSuccess={handleConfigSuccess} onError={handleConfigError} /> )} {/* Camera Preview Modal */} {previewCamera && ( { setPreviewModalOpen(false) setPreviewCamera(null) }} /> )} {/* Notification */} {notification && (
{notification.type === 'success' ? ( ) : ( )}

{notification.message}

)}
) }