diff --git a/src/App.tsx b/src/App.tsx index 1ed0a7c..801a544 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,88 +1,220 @@ -import { Camera, Radio } from 'lucide-react' +import { useState, useCallback, useMemo } from 'react' +import { Camera, Play, Square, Wifi, WifiOff, AlertTriangle } from 'lucide-react' import { useSSE } from './hooks/useSSE' import { useCameraStore } from './store/useCameraStore' -import { CameraCard } from './components' +import { api } from './services/api' +import CameraCard from './components/CameraCard' +import HistoryViewer from './components/HistoryViewer' function App() { - // Connect to SSE endpoint — auto-updates the camera store - useSSE() + const [commandBusy, setCommandBusy] = useState(false) + const [commandError, setCommandError] = useState(null) + const [historyCameraId, setHistoryCameraId] = useState(null) + const [historyCameraName, setHistoryCameraName] = useState() - // Subscribe to the camera store for reactivity. - // getCameras / getOnlineCount / getRecordingCount pull from live state. - const { getCameras, getOnlineCount, getRecordingCount } = useCameraStore() - const cameras = getCameras() - const onlineCount = getOnlineCount() - const recordingCount = getRecordingCount() + // SSE connection + live store + const { connectionState } = useSSE() + + // Subscribe to full camera state — dashboard needs every change + const camerasMap = useCameraStore((s) => s.cameras) + const cameras = useMemo(() => Array.from(camerasMap.values()), [camerasMap]) + const onlineCount = useMemo(() => cameras.filter((c) => c.online).length, [cameras]) + const recordingCount = useMemo(() => cameras.filter((c) => c.recording).length, [cameras]) + + const cameraIds = cameras.map((c) => c.camera_id) + + // ── Command helpers ── + + const handleStart = useCallback(async (cameraId: string) => { + setCommandBusy(true) + setCommandError(null) + try { + await api.startRecording(cameraId) + } catch (err) { + setCommandError(err instanceof Error ? err.message : 'Command failed') + } finally { + setCommandBusy(false) + } + }, []) + + const handleStop = useCallback(async (cameraId: string) => { + setCommandBusy(true) + setCommandError(null) + try { + await api.stopRecording(cameraId) + } catch (err) { + setCommandError(err instanceof Error ? err.message : 'Command failed') + } finally { + setCommandBusy(false) + } + }, []) + + const handleStartAll = useCallback(async () => { + setCommandBusy(true) + setCommandError(null) + try { + await Promise.all(cameraIds.map((id) => api.startRecording(id))) + } catch { + // Individual failures are non-fatal — some may succeed + } finally { + setCommandBusy(false) + } + }, [cameraIds]) + + const handleStopAll = useCallback(async () => { + setCommandBusy(true) + setCommandError(null) + try { + await Promise.all(cameraIds.map((id) => api.stopRecording(id))) + } catch { + // Individual failures are non-fatal + } finally { + setCommandBusy(false) + } + }, [cameraIds]) + + const handleViewHistory = useCallback((cameraId: string) => { + const cam = useCameraStore.getState().cameras.get(cameraId) + setHistoryCameraId(cameraId) + setHistoryCameraName(cam?.friendly_name ?? cameraId) + }, []) + + const handleCloseHistory = useCallback(() => { + setHistoryCameraId(null) + }, []) + + // ── Connection badge ── + + const connectionBadge = { + connected: { icon: Wifi, label: 'Live', class: 'bg-rig-success/15 text-rig-success' }, + connecting: { icon: Wifi, label: 'Connecting...', class: 'bg-rig-warning/15 text-rig-warning' }, + disconnected: { icon: WifiOff, label: 'Disconnected', class: 'bg-rig-danger/15 text-rig-danger' }, + error: { icon: AlertTriangle, label: 'Stream Error', class: 'bg-rig-danger/15 text-rig-danger' }, + }[connectionState] ?? { + icon: WifiOff, + label: 'Disconnected', + class: 'bg-rig-danger/15 text-rig-danger', + } + + const BadgeIcon = connectionBadge.icon + + // ── Render ── return ( -
+
{/* Header */} -
-
-
- -

- RemoteRig -

- - Dashboard - - - {/* Stats badges */} -
- {/* Online count */} - - - {onlineCount} online - - - {/* Recording count */} - - - - - - {recordingCount} recording +
+
+
+
+ +

+ RemoteRig +

+ + Dashboard
+ + {/* Connection status */} +
+ {/* SSE badge */} + + + {connectionBadge.label} + + + {/* Global controls */} +
+ + +
+
+
+ + {/* Stats strip */} +
+ + {cameras.length} camera{cameras.length !== 1 ? 's' : ''} + + + {onlineCount} online + + + 0 ? 'text-rig-danger' : 'text-rig-dark-300'}> + {recordingCount} + {' '} + recording +
+ {/* Command error toast */} + {commandError && ( +
+

+ + {commandError} +

+
+ )} + {/* Main Content */} -
+
{cameras.length === 0 ? ( - /* Empty state */
- - - +

- Waiting for cameras… + No Cameras Connected

- Connect cameras to your RemoteRig server and they will appear here - automatically. + Waiting for camera nodes to connect. Ensure ESP32 bridges are powered on and connected to the network.

) : ( - /* Camera grid */ -
- {cameras.map((camera) => ( - +
+ {cameras.map((cam) => ( + ))}
)}
+ {/* History modal */} + + {/* Footer */} -