Add scheduling-remote service to docker-compose and enhance camera error handling
- Introduced a new service for scheduling-remote in docker-compose.yml, allowing for better management of scheduling functionalities. - Enhanced error handling in CameraMonitor and CameraStreamer classes to improve robustness during camera initialization and streaming processes. - Updated various components in the management dashboard to support dark mode and improve user experience with consistent styling. - Implemented feature flags for enabling/disabling modules, including the new scheduling module.
This commit is contained in:
@@ -57,8 +57,10 @@ export default function App() {
|
||||
setRecordings(recordingsData)
|
||||
setLastUpdate(new Date())
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch data')
|
||||
// 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)
|
||||
}
|
||||
@@ -221,12 +223,32 @@ export default function App() {
|
||||
const handlePreviewModal = useCallback((cameraName: string) => {
|
||||
setPreviewCamera(cameraName)
|
||||
setPreviewModalOpen(true)
|
||||
// The modal will start streaming and notify us via onStreamStarted callback
|
||||
}, [])
|
||||
|
||||
const handlePreviewNewWindow = useCallback((cameraName: string) => {
|
||||
// Open camera stream in new window/tab
|
||||
const streamUrl = visionApi.getStreamUrl(cameraName)
|
||||
window.open(streamUrl, '_blank')
|
||||
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) => {
|
||||
@@ -259,8 +281,20 @@ export default function App() {
|
||||
const result = await visionApi.stopStream(cameraName)
|
||||
if (result.success) {
|
||||
setNotification({ type: 'success', message: 'Streaming stopped' })
|
||||
// Refresh camera status
|
||||
visionApi.getCameras().then(setCameras).catch(console.error)
|
||||
|
||||
// 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}` })
|
||||
}
|
||||
@@ -438,6 +472,26 @@ export default function App() {
|
||||
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',
|
||||
},
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -195,11 +195,11 @@ export const CameraCard: React.FC<CameraCardProps> = ({
|
||||
{isStreaming && (
|
||||
<button
|
||||
onClick={() => onStopStreaming(cameraName)}
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-orange-600 rounded-lg hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 transition-colors"
|
||||
className="flex items-center justify-center px-3 py-2 text-sm font-medium text-white bg-orange-600 rounded-lg hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 transition-colors"
|
||||
title="Stop streaming"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,8 @@ interface CameraPreviewModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onError?: (error: string) => void
|
||||
onStreamStarted?: () => void
|
||||
onStreamStopped?: () => void
|
||||
}
|
||||
|
||||
export const CameraPreviewModal: React.FC<CameraPreviewModalProps> = ({
|
||||
@@ -13,6 +15,8 @@ export const CameraPreviewModal: React.FC<CameraPreviewModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onError,
|
||||
onStreamStarted,
|
||||
onStreamStopped,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [streaming, setStreaming] = useState(false)
|
||||
@@ -46,6 +50,9 @@ export const CameraPreviewModal: React.FC<CameraPreviewModalProps> = ({
|
||||
if (imgRef.current) {
|
||||
imgRef.current.src = `${streamUrl}?t=${Date.now()}`
|
||||
}
|
||||
|
||||
// Notify parent that streaming started
|
||||
onStreamStarted?.()
|
||||
} else {
|
||||
throw new Error(result.message)
|
||||
}
|
||||
@@ -68,6 +75,9 @@ export const CameraPreviewModal: React.FC<CameraPreviewModalProps> = ({
|
||||
if (imgRef.current) {
|
||||
imgRef.current.src = ''
|
||||
}
|
||||
|
||||
// Notify parent that streaming stopped
|
||||
onStreamStopped?.()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error stopping stream:', err)
|
||||
@@ -82,12 +92,13 @@ export const CameraPreviewModal: React.FC<CameraPreviewModalProps> = ({
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[999999] flex items-center justify-center overflow-y-auto">
|
||||
<div className="fixed inset-0 z-[999999] flex items-center justify-center overflow-y-auto p-4">
|
||||
<div
|
||||
className="fixed inset-0 h-full w-full bg-gray-900/60 backdrop-blur-sm"
|
||||
className="fixed inset-0 h-full w-full bg-gray-900/70 backdrop-blur-md"
|
||||
onClick={handleClose}
|
||||
style={{ backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)' }}
|
||||
/>
|
||||
<div className="relative w-11/12 max-w-5xl rounded-xl bg-white shadow-2xl dark:bg-gray-800 p-6" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="relative w-full max-w-4xl rounded-xl bg-white shadow-2xl dark:bg-gray-800 p-6" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
@@ -148,7 +159,7 @@ export const CameraPreviewModal: React.FC<CameraPreviewModalProps> = ({
|
||||
<img
|
||||
ref={imgRef}
|
||||
alt={`Live stream from ${cameraName}`}
|
||||
className="w-full h-auto max-h-[70vh] object-contain"
|
||||
className="w-full h-auto max-h-[60vh] object-contain"
|
||||
onError={() => setError('Failed to load camera stream')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user