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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user