Enhance Docker Compose configuration and improve camera manager error handling

- Added container names for better identification of services in docker-compose.yml.
- Refactored CameraManager to include error handling during initialization of camera recorders and streamers, ensuring the system remains operational even if some components fail.
- Updated frontend components to support new MQTT Debug Panel functionality, enhancing monitoring capabilities.
This commit is contained in:
salirezav
2025-12-01 15:30:10 -05:00
parent 73849b40a8
commit b3a94d2d4f
14 changed files with 940 additions and 67 deletions

View File

@@ -13,13 +13,13 @@ 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`
@@ -33,7 +33,7 @@ export default function App() {
const [error, setError] = useState<string | null>(null)
const [lastUpdate, setLastUpdate] = useState<Date | null>(null)
const [notification, setNotification] = useState<{ type: 'success' | 'error', message: string } | null>(null)
// Modal states
const [previewModalOpen, setPreviewModalOpen] = useState(false)
const [previewCamera, setPreviewCamera] = useState<string | null>(null)
@@ -100,13 +100,13 @@ export default function App() {
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())
})
)
@@ -122,7 +122,7 @@ export default function App() {
is_recording: false,
},
}))
// Refresh recordings and system status
Promise.all([
visionApi.getRecordings(),
@@ -131,7 +131,7 @@ export default function App() {
setRecordings(recordingsData)
setSystemStatus(statusData)
}).catch(console.error)
setLastUpdate(new Date())
})
)
@@ -171,7 +171,7 @@ export default function App() {
if (result.success) {
setNotification({ type: 'success', message: `Recording started: ${result.filename}` })
// Immediately update state optimistically (UI updates instantly)
setCameras((prev) => ({
...prev,
@@ -181,7 +181,7 @@ export default function App() {
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)
@@ -199,7 +199,7 @@ export default function App() {
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,
@@ -209,7 +209,7 @@ export default function App() {
current_recording_file: null,
},
}))
// Refresh camera status from API as backup (in case WebSocket is delayed)
setTimeout(() => {
visionApi.getCameras().then(setCameras).catch(console.error)
@@ -241,7 +241,7 @@ export default function App() {
status: 'streaming',
},
}))
// Open camera stream in new window/tab
const streamUrl = visionApi.getStreamUrl(cameraName)
window.open(streamUrl, '_blank')
@@ -262,7 +262,7 @@ export default function App() {
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
@@ -283,7 +283,7 @@ export default function App() {
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,
@@ -292,7 +292,7 @@ export default function App() {
status: 'available',
},
}))
// Refresh camera status from API as backup
setTimeout(() => {
visionApi.getCameras().then(setCameras).catch(console.error)
@@ -390,7 +390,10 @@ export default function App() {
{/* Status Widgets */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<SystemHealthWidget systemStatus={systemStatus} />
<MqttStatusWidget systemStatus={systemStatus} />
<MqttStatusWidget
systemStatus={systemStatus}
onDebugClick={() => setDebugPanelOpen(true)}
/>
<RecordingsCountWidget active={activeRecordings} total={totalRecordings} />
<CameraCountWidget cameraCount={cameraCount} machineCount={machineCount} />
</div>
@@ -426,11 +429,10 @@ export default function App() {
{/* Notification */}
{notification && (
<div
className={`fixed top-4 right-4 z-[999999] p-4 rounded-md shadow-lg ${
notification.type === 'success'
className={`fixed top-4 right-4 z-[999999] p-4 rounded-md shadow-lg ${notification.type === 'success'
? 'bg-green-50 border border-green-200 text-green-800'
: 'bg-red-50 border border-red-200 text-red-800'
}`}
}`}
>
<div className="flex items-center">
<div className="flex-shrink-0">
@@ -450,11 +452,10 @@ export default function App() {
<div className="ml-auto pl-3">
<button
onClick={() => setNotification(null)}
className={`inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2 ${
notification.type === 'success'
className={`inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2 ${notification.type === 'success'
? 'text-green-500 hover:bg-green-100 focus:ring-green-600'
: 'text-red-500 hover:bg-red-100 focus:ring-red-600'
}`}
}`}
>
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
@@ -515,25 +516,13 @@ export default function App() {
setNotification({ type: 'error', message: error })
}}
/>
)}
{/* MQTT Debug Panel */}
<MqttDebugPanel
isOpen={debugPanelOpen}
onClose={() => setDebugPanelOpen(false)}
/>
{/* Debug Button - Bottom Right */}
<button
onClick={() => setDebugPanelOpen(true)}
className="fixed bottom-6 right-6 z-40 bg-purple-600 text-white px-4 py-2 rounded-lg shadow-lg hover:bg-purple-700 transition-colors flex items-center gap-2"
title="Open MQTT Debug Panel"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Debug
</button>
</div>
)
}

View File

@@ -4,9 +4,10 @@ import { visionApi, type SystemStatus, type MqttEvent } from '../services/api'
interface MqttStatusWidgetProps {
systemStatus: SystemStatus | null
onDebugClick?: () => void
}
export const MqttStatusWidget: React.FC<MqttStatusWidgetProps> = ({ systemStatus }) => {
export const MqttStatusWidget: React.FC<MqttStatusWidgetProps> = ({ systemStatus, onDebugClick }) => {
const [lastEvent, setLastEvent] = useState<MqttEvent | null>(null)
const isConnected = systemStatus?.mqtt_connected ?? false
const lastMessage = systemStatus?.last_mqtt_message
@@ -44,14 +45,27 @@ export const MqttStatusWidget: React.FC<MqttStatusWidgetProps> = ({ systemStatus
: 'No messages'
return (
<StatusWidget
title="MQTT Status"
status={isConnected}
statusText={isConnected ? 'Connected' : 'Disconnected'}
subtitle={subtitle || undefined}
icon={
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500 animate-pulse' : 'bg-red-500'}`} />
}
/>
<div className="relative">
<StatusWidget
title="MQTT Status"
status={isConnected}
statusText={isConnected ? 'Connected' : 'Disconnected'}
subtitle={subtitle || undefined}
icon={
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500 animate-pulse' : 'bg-red-500'}`} />
}
/>
{onDebugClick && (
<button
onClick={onDebugClick}
className="absolute top-2 right-2 text-gray-400 hover:text-purple-600 transition-colors p-1"
title="MQTT Debug Panel"
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fillRule="evenodd" clipRule="evenodd" />
</svg>
</button>
)}
</div>
)
}

View File

@@ -27,6 +27,14 @@ export default defineConfig({
},
build: {
target: 'esnext',
rollupOptions: {
output: {
// Add hash to filenames for cache busting
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
})