import { useState, useEffect, useRef, useCallback } 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' 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([]) 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) } } } // Only update state if data has actually changed to prevent unnecessary re-renders setSystemStatus(prevStatus => { if (JSON.stringify(prevStatus) !== JSON.stringify(statusData)) { return statusData } return prevStatus }) setStorageStats(prevStats => { if (JSON.stringify(prevStats) !== JSON.stringify(storageData)) { return storageData } return prevStats }) setRecordings(prevRecordings => { if (JSON.stringify(prevRecordings) !== JSON.stringify(recordingsData)) { return recordingsData } return prevRecordings }) setLastUpdateTime(new Date()) // 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]) 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()} {autoRefreshEnabled && !refreshing && ( Auto-refresh: {refreshInterval / 1000}s )}

)}
{/* Auto-refresh controls */}
{autoRefreshEnabled && ( )}
{/* Refresh indicator and button */}
{refreshing && (
)}
{/* System Overview */} {systemStatus && (
{systemStatus.system_started ? 'Online' : 'Offline'}
System Status
Uptime: {formatUptime(systemStatus.uptime_seconds)}
{systemStatus.mqtt_connected ? 'Connected' : 'Disconnected'}
{systemStatus.mqtt_connected && (
Live
)}
{mqttStatus && (
{mqttStatus.message_count} messages
{mqttStatus.error_count} errors
)}
MQTT
{mqttStatus ? (
Broker: {mqttStatus.broker_host}:{mqttStatus.broker_port}
Last message: {new Date(mqttStatus.last_message_time).toLocaleTimeString()}
) : (
Last message: {new Date(systemStatus.last_mqtt_message).toLocaleTimeString()}
)}
{/* MQTT Events History */} {mqttEvents.length > 0 && (

Recent Events

{mqttEvents.length} events
{mqttEvents.map((event, index) => (
{new Date(event.timestamp).toLocaleTimeString().slice(-8, -3)} {event.machine_name.replace('_', ' ')} {event.payload}
#{event.message_number}
))}
)}
{systemStatus.active_recordings}
Active Recordings
Total: {systemStatus.total_recordings}
{Object.keys(systemStatus.cameras).length}
Cameras
Machines: {Object.keys(systemStatus.machines).length}
)} {/* Cameras Status */} {systemStatus && (

Cameras

Current status of all cameras in the system

{Object.entries(systemStatus.cameras).map(([cameraName, camera]) => { // Debug logging to see what data we're getting console.log(`Camera ${cameraName} data:`, JSON.stringify(camera, null, 2)) const friendlyName = camera.device_info?.friendly_name const hasDeviceInfo = !!camera.device_info const hasSerial = !!camera.device_info?.serial_number return (

{friendlyName ? (
{friendlyName}
({cameraName})
) : (
{cameraName}
{hasDeviceInfo ? 'Device info available but no friendly name' : 'No device info available'}
)}

{camera.is_recording ? 'Recording' : camera.status}
Recording: {camera.is_recording ? 'Yes' : 'No'}
{camera.device_info?.serial_number && (
Serial: {camera.device_info.serial_number}
)} {/* Debug info - remove this after fixing */}
Debug Info:
Has device_info: {hasDeviceInfo ? 'Yes' : 'No'}
Has friendly_name: {friendlyName ? 'Yes' : 'No'}
Has serial: {hasSerial ? 'Yes' : 'No'}
Last error: {camera.last_error || 'None'}
{camera.device_info && (
Raw device_info: {JSON.stringify(camera.device_info)}
)}
Last checked: {new Date(camera.last_checked).toLocaleTimeString()}
{camera.current_recording_file && (
Recording file: {camera.current_recording_file}
)}
) })}
)} {/* 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 && (

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]) => { // Find the corresponding camera to get friendly name const camera = systemStatus?.cameras[cameraName] const displayName = camera?.device_info?.friendly_name || cameraName return (
{camera?.device_info?.friendly_name ? ( <> {displayName} ({cameraName}) ) : ( cameraName )}
Files: {stats.file_count}
Size: {formatBytes(stats.total_size_bytes)}
) })}
)}
)} {/* Recent Recordings */} {Object.keys(recordings).length > 0 && (

Recent Recordings

Latest recording sessions

{Object.entries(recordings).slice(0, 10).map(([recordingId, recording]) => { // Find the corresponding camera to get friendly name const camera = systemStatus?.cameras[recording.camera_name] const displayName = camera?.device_info?.friendly_name || recording.camera_name return ( ) })}
Camera Filename Status Duration Size Started
{camera?.device_info?.friendly_name ? (
{displayName}
({recording.camera_name})
) : ( recording.camera_name )}
{recording.filename} {recording.state} {recording.duration_seconds ? formatDuration(recording.duration_seconds) : '-'} {recording.file_size_bytes ? formatBytes(recording.file_size_bytes) : '-'} {new Date(recording.start_time).toLocaleString()}
)}
) }