Update Docker configuration, enhance error handling, and improve logging

- Added health check to the camera management API service in docker-compose.yml for better container reliability.
- Updated installation scripts in Dockerfile to check for existing dependencies before installation, improving efficiency.
- Enhanced error handling in the USDAVisionSystem class to allow partial operation if some components fail to start, preventing immediate shutdown.
- Improved logging throughout the application, including more detailed error messages and critical error handling in the main loop.
- Refactored WebSocketManager and CameraMonitor classes to use debug logging for connection events, reducing log noise.
This commit is contained in:
salirezav
2025-12-03 17:23:31 -05:00
parent 2bce817b4e
commit 933d4417a5
30 changed files with 4314 additions and 220 deletions

View File

@@ -932,7 +932,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
// Create/update soaking record with repetition_id
await phaseManagement.createSoaking({
experiment_id: experimentId,
repetition_id: repId,
scheduled_start_time: soakingStart.toISOString(),
soaking_duration_minutes: soaking.soaking_duration_minutes
@@ -940,7 +939,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
// Create/update airdrying record with repetition_id
await phaseManagement.createAirdrying({
experiment_id: experimentId,
repetition_id: repId,
scheduled_start_time: airdryingStart.toISOString(),
duration_minutes: airdrying.duration_minutes
@@ -953,7 +951,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
if (phase?.cracking_machine_type_id) {
await phaseManagement.createCracking({
experiment_id: experimentId,
repetition_id: repId,
machine_type_id: phase.cracking_machine_type_id,
scheduled_start_time: crackingStart.toISOString()
@@ -995,8 +992,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
return (
<div className="p-6 flex flex-col" style={{ height: 'calc(100vh - 48px)' }}>
<div className="mb-6 flex-shrink-0">
<div className="h-full flex flex-col overflow-hidden -m-4 md:-m-6">
<div className="p-6 flex-shrink-0">
<button
onClick={onBack}
className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mb-4"
@@ -1014,7 +1011,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6 flex flex-col flex-1 min-h-0">
<div className="px-6 pb-6 flex-1 min-h-0 overflow-hidden">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6 h-full flex flex-col min-h-0 overflow-hidden">
{error && (
<div className="mb-4 text-sm text-red-600 dark:text-red-400">{error}</div>
)}
@@ -1250,8 +1248,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</div>
)}
{/* Week Calendar for selected conductors' availability */}
<div className="mt-6 flex flex-col flex-1 min-h-0">
<div className="flex justify-between items-center mb-3">
<div className="mt-6 flex flex-col flex-1 min-h-0 overflow-hidden">
<div className="flex justify-between items-center mb-3 flex-shrink-0">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Selected Conductors' Availability & Experiment Scheduling</h3>
<div className="flex gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">Marker Style:</span>
@@ -1293,7 +1291,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</button>
</div>
</div>
<div ref={calendarRef} className="flex-1 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
<div ref={calendarRef} className="flex-1 min-h-0 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
<DndProvider backend={HTML5Backend}>
<DnDCalendar
localizer={localizer}
@@ -1383,6 +1381,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</div>
</div>
</div>
</div>
</div>
)

View File

@@ -63,8 +63,7 @@ export interface ExperimentRepetition {
export interface Soaking {
id: string
experiment_id: string
repetition_id?: string | null
repetition_id: string
scheduled_start_time: string
actual_start_time?: string | null
soaking_duration_minutes: number
@@ -77,8 +76,7 @@ export interface Soaking {
export interface Airdrying {
id: string
experiment_id: string
repetition_id?: string | null
repetition_id: string
scheduled_start_time: string
actual_start_time?: string | null
duration_minutes: number
@@ -119,22 +117,19 @@ export interface UpdateRepetitionRequest {
}
export interface CreateSoakingRequest {
experiment_id: string
repetition_id?: string | null
repetition_id: string
scheduled_start_time: string
soaking_duration_minutes: number
}
export interface CreateAirdryingRequest {
experiment_id: string
repetition_id?: string | null
repetition_id: string
scheduled_start_time: string
duration_minutes: number
}
export interface CreateCrackingRequest {
experiment_id: string
repetition_id?: string | null
repetition_id: string
machine_type_id: string
scheduled_start_time: string
}
@@ -277,16 +272,22 @@ export const phaseManagement = {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
if (!request.repetition_id) {
throw new Error('repetition_id is required')
}
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.soaking_duration_minutes * 60000).toISOString()
const { data, error } = await supabase
.from('soaking')
.upsert({
...request,
repetition_id: request.repetition_id,
scheduled_start_time: request.scheduled_start_time,
soaking_duration_minutes: request.soaking_duration_minutes,
scheduled_end_time: scheduledEndTime,
created_by: user.id
}, {
onConflict: 'experiment_id,repetition_id'
onConflict: 'repetition_id'
})
.select()
.single()
@@ -299,16 +300,26 @@ export const phaseManagement = {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
if (!request.repetition_id) {
throw new Error('repetition_id is required')
}
if (!request.scheduled_start_time) {
throw new Error('scheduled_start_time is required')
}
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.duration_minutes * 60000).toISOString()
const { data, error } = await supabase
.from('airdrying')
.upsert({
...request,
repetition_id: request.repetition_id,
scheduled_start_time: request.scheduled_start_time,
duration_minutes: request.duration_minutes,
scheduled_end_time: scheduledEndTime,
created_by: user.id
}, {
onConflict: 'experiment_id,repetition_id'
onConflict: 'repetition_id'
})
.select()
.single()
@@ -321,13 +332,23 @@ export const phaseManagement = {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
if (!request.repetition_id) {
throw new Error('repetition_id is required')
}
if (!request.scheduled_start_time) {
throw new Error('scheduled_start_time is required')
}
const { data, error } = await supabase
.from('cracking')
.upsert({
...request,
repetition_id: request.repetition_id,
machine_type_id: request.machine_type_id,
scheduled_start_time: request.scheduled_start_time,
created_by: user.id
}, {
onConflict: 'experiment_id,repetition_id'
onConflict: 'repetition_id'
})
.select()
.single()
@@ -337,11 +358,23 @@ export const phaseManagement = {
},
async getSoakingByExperimentId(experimentId: string): Promise<Soaking | null> {
// Get the first repetition for this experiment
const { data: repetitions, error: repsError } = await supabase
.from('experiment_repetitions')
.select('id')
.eq('experiment_id', experimentId)
.order('repetition_number', { ascending: true })
.limit(1)
if (repsError || !repetitions || repetitions.length === 0) {
return null
}
// Get soaking for the first repetition
const { data, error } = await supabase
.from('soaking')
.select('*')
.eq('experiment_id', experimentId)
.is('repetition_id', null)
.eq('repetition_id', repetitions[0].id)
.single()
if (error) {
@@ -352,11 +385,23 @@ export const phaseManagement = {
},
async getAirdryingByExperimentId(experimentId: string): Promise<Airdrying | null> {
// Get the first repetition for this experiment
const { data: repetitions, error: repsError } = await supabase
.from('experiment_repetitions')
.select('id')
.eq('experiment_id', experimentId)
.order('repetition_number', { ascending: true })
.limit(1)
if (repsError || !repetitions || repetitions.length === 0) {
return null
}
// Get airdrying for the first repetition
const { data, error } = await supabase
.from('airdrying')
.select('*')
.eq('experiment_id', experimentId)
.is('repetition_id', null)
.eq('repetition_id', repetitions[0].id)
.single()
if (error) {

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]',
},
},
},
})