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

@@ -1,61 +1,61 @@
phase_name,machine_type,run_id,experiment_number,soaking_duration_hr,air_drying_duration_min,plate_contact_frequency_hz,throughput_rate_pecans_sec,crush_amount_in,entry_exit_height_diff_in,reps,rep
"Phase 2 of JC Experiments","JC Cracker",1,0,34,19,53,28,0.05,-0.09,3,1
"Phase 2 of JC Experiments","JC Cracker",2,1,24,27,34,29,0.03,0.01,3,3
"Phase 2 of JC Experiments","JC Cracker",3,12,28,59,37,23,0.06,-0.08,3,1
"Phase 2 of JC Experiments","JC Cracker",4,15,16,60,30,24,0.07,0.02,3,1
"Phase 2 of JC Experiments","JC Cracker",5,4,13,41,41,38,0.05,0.03,3,2
"Phase 2 of JC Experiments","JC Cracker",6,18,18,49,38,35,0.07,-0.08,3,1
"Phase 2 of JC Experiments","JC Cracker",7,11,24,59,42,25,0.07,-0.05,3,1
"Phase 2 of JC Experiments","JC Cracker",8,16,20,59,41,14,0.07,0.04,3,1
"Phase 2 of JC Experiments","JC Cracker",9,4,13,41,41,38,0.05,0.03,3,1
"Phase 2 of JC Experiments","JC Cracker",10,19,11,25,56,34,0.06,-0.09,3,1
"Phase 2 of JC Experiments","JC Cracker",11,15,16,60,30,24,0.07,0.02,3,2
"Phase 2 of JC Experiments","JC Cracker",12,16,20,59,41,14,0.07,0.04,3,3
"Phase 2 of JC Experiments","JC Cracker",13,10,26,60,44,12,0.08,-0.1,3,2
"Phase 2 of JC Experiments","JC Cracker",14,1,24,27,34,29,0.03,0.01,3,1
"Phase 2 of JC Experiments","JC Cracker",15,17,34,60,34,29,0.07,-0.09,3,2
"Phase 2 of JC Experiments","JC Cracker",16,5,30,33,30,36,0.05,-0.04,3,3
"Phase 2 of JC Experiments","JC Cracker",17,2,38,10,60,28,0.06,-0.1,3,3
"Phase 2 of JC Experiments","JC Cracker",18,2,38,10,60,28,0.06,-0.1,3,1
"Phase 2 of JC Experiments","JC Cracker",19,13,21,59,41,21,0.06,-0.09,3,2
"Phase 2 of JC Experiments","JC Cracker",20,1,24,27,34,29,0.03,0.01,3,2
"Phase 2 of JC Experiments","JC Cracker",21,14,22,59,45,17,0.07,-0.08,3,2
"Phase 2 of JC Experiments","JC Cracker",22,6,10,22,37,30,0.06,0.02,3,2
"Phase 2 of JC Experiments","JC Cracker",23,11,24,59,42,25,0.07,-0.05,3,2
"Phase 2 of JC Experiments","JC Cracker",24,19,11,25,56,34,0.06,-0.09,3,2
"Phase 2 of JC Experiments","JC Cracker",25,8,27,12,55,24,0.04,0.04,3,2
"Phase 2 of JC Experiments","JC Cracker",26,18,18,49,38,35,0.07,-0.08,3,3
"Phase 2 of JC Experiments","JC Cracker",27,5,30,33,30,36,0.05,-0.04,3,1
"Phase 2 of JC Experiments","JC Cracker",28,9,32,26,47,26,0.07,0.03,3,1
"Phase 2 of JC Experiments","JC Cracker",29,3,11,36,42,13,0.07,-0.07,3,1
"Phase 2 of JC Experiments","JC Cracker",30,10,26,60,44,12,0.08,-0.1,3,1
"Phase 2 of JC Experiments","JC Cracker",31,8,27,12,55,24,0.04,0.04,3,3
"Phase 2 of JC Experiments","JC Cracker",32,5,30,33,30,36,0.05,-0.04,3,2
"Phase 2 of JC Experiments","JC Cracker",33,8,27,12,55,24,0.04,0.04,3,1
"Phase 2 of JC Experiments","JC Cracker",34,18,18,49,38,35,0.07,-0.08,3,2
"Phase 2 of JC Experiments","JC Cracker",35,3,11,36,42,13,0.07,-0.07,3,3
"Phase 2 of JC Experiments","JC Cracker",36,10,26,60,44,12,0.08,-0.1,3,3
"Phase 2 of JC Experiments","JC Cracker",37,17,34,60,34,29,0.07,-0.09,3,3
"Phase 2 of JC Experiments","JC Cracker",38,13,21,59,41,21,0.06,-0.09,3,3
"Phase 2 of JC Experiments","JC Cracker",39,12,28,59,37,23,0.06,-0.08,3,2
"Phase 2 of JC Experiments","JC Cracker",40,9,32,26,47,26,0.07,0.03,3,3
"Phase 2 of JC Experiments","JC Cracker",41,14,22,59,45,17,0.07,-0.08,3,3
"Phase 2 of JC Experiments","JC Cracker",42,0,34,19,53,28,0.05,-0.09,3,2
"Phase 2 of JC Experiments","JC Cracker",43,7,15,30,35,32,0.05,-0.07,3,1
"Phase 2 of JC Experiments","JC Cracker",44,0,34,19,53,28,0.05,-0.09,3,3
"Phase 2 of JC Experiments","JC Cracker",45,15,16,60,30,24,0.07,0.02,3,3
"Phase 2 of JC Experiments","JC Cracker",46,13,21,59,41,21,0.06,-0.09,3,1
"Phase 2 of JC Experiments","JC Cracker",47,11,24,59,42,25,0.07,-0.05,3,3
"Phase 2 of JC Experiments","JC Cracker",48,7,15,30,35,32,0.05,-0.07,3,3
"Phase 2 of JC Experiments","JC Cracker",49,16,20,59,41,14,0.07,0.04,3,2
"Phase 2 of JC Experiments","JC Cracker",50,3,11,36,42,13,0.07,-0.07,3,2
"Phase 2 of JC Experiments","JC Cracker",51,7,15,30,35,32,0.05,-0.07,3,2
"Phase 2 of JC Experiments","JC Cracker",52,6,10,22,37,30,0.06,0.02,3,1
"Phase 2 of JC Experiments","JC Cracker",53,19,11,25,56,34,0.06,-0.09,3,3
"Phase 2 of JC Experiments","JC Cracker",54,6,10,22,37,30,0.06,0.02,3,3
"Phase 2 of JC Experiments","JC Cracker",55,2,38,10,60,28,0.06,-0.1,3,2
"Phase 2 of JC Experiments","JC Cracker",56,14,22,59,45,17,0.07,-0.08,3,1
"Phase 2 of JC Experiments","JC Cracker",57,4,13,41,41,38,0.05,0.03,3,3
"Phase 2 of JC Experiments","JC Cracker",58,9,32,26,47,26,0.07,0.03,3,2
"Phase 2 of JC Experiments","JC Cracker",59,17,34,60,34,29,0.07,-0.09,3,1
"Phase 2 of JC Experiments","JC Cracker",60,12,28,59,37,23,0.06,-0.08,3,3
experiment_number,soaking_duration_hr,air_drying_duration_min,plate_contact_frequency_hz,throughput_rate_pecans_sec,crush_amount_in,entry_exit_height_diff_in
0,34,19,53,28,0.05,-0.09
1,24,27,34,29,0.03,0.01
12,28,59,37,23,0.06,-0.08
15,16,60,30,24,0.07,0.02
4,13,41,41,38,0.05,0.03
18,18,49,38,35,0.07,-0.08
11,24,59,42,25,0.07,-0.05
16,20,59,41,14,0.07,0.04
4,13,41,41,38,0.05,0.03
19,11,25,56,34,0.06,-0.09
15,16,60,30,24,0.07,0.02
16,20,59,41,14,0.07,0.04
10,26,60,44,12,0.08,-0.1
1,24,27,34,29,0.03,0.01
17,34,60,34,29,0.07,-0.09
5,30,33,30,36,0.05,-0.04
2,38,10,60,28,0.06,-0.1
2,38,10,60,28,0.06,-0.1
13,21,59,41,21,0.06,-0.09
1,24,27,34,29,0.03,0.01
14,22,59,45,17,0.07,-0.08
6,10,22,37,30,0.06,0.02
11,24,59,42,25,0.07,-0.05
19,11,25,56,34,0.06,-0.09
8,27,12,55,24,0.04,0.04
18,18,49,38,35,0.07,-0.08
5,30,33,30,36,0.05,-0.04
9,32,26,47,26,0.07,0.03
3,11,36,42,13,0.07,-0.07
10,26,60,44,12,0.08,-0.1
8,27,12,55,24,0.04,0.04
5,30,33,30,36,0.05,-0.04
8,27,12,55,24,0.04,0.04
18,18,49,38,35,0.07,-0.08
3,11,36,42,13,0.07,-0.07
10,26,60,44,12,0.08,-0.1
17,34,60,34,29,0.07,-0.09
13,21,59,41,21,0.06,-0.09
12,28,59,37,23,0.06,-0.08
9,32,26,47,26,0.07,0.03
14,22,59,45,17,0.07,-0.08
0,34,19,53,28,0.05,-0.09
7,15,30,35,32,0.05,-0.07
0,34,19,53,28,0.05,-0.09
15,16,60,30,24,0.07,0.02
13,21,59,41,21,0.06,-0.09
11,24,59,42,25,0.07,-0.05
7,15,30,35,32,0.05,-0.07
16,20,59,41,14,0.07,0.04
3,11,36,42,13,0.07,-0.07
7,15,30,35,32,0.05,-0.07
6,10,22,37,30,0.06,0.02
19,11,25,56,34,0.06,-0.09
6,10,22,37,30,0.06,0.02
2,38,10,60,28,0.06,-0.1
14,22,59,45,17,0.07,-0.08
4,13,41,41,38,0.05,0.03
9,32,26,47,26,0.07,0.03
17,34,60,34,29,0.07,-0.09
12,28,59,37,23,0.06,-0.08
1 phase_name experiment_number machine_type soaking_duration_hr run_id air_drying_duration_min plate_contact_frequency_hz throughput_rate_pecans_sec crush_amount_in entry_exit_height_diff_in reps rep
2 Phase 2 of JC Experiments 0 JC Cracker 34 1 19 53 28 0.05 -0.09 3 1
3 Phase 2 of JC Experiments 1 JC Cracker 24 2 27 34 29 0.03 0.01 3 3
4 Phase 2 of JC Experiments 12 JC Cracker 28 3 59 37 23 0.06 -0.08 3 1
5 Phase 2 of JC Experiments 15 JC Cracker 16 4 60 30 24 0.07 0.02 3 1
6 Phase 2 of JC Experiments 4 JC Cracker 13 5 41 41 38 0.05 0.03 3 2
7 Phase 2 of JC Experiments 18 JC Cracker 18 6 49 38 35 0.07 -0.08 3 1
8 Phase 2 of JC Experiments 11 JC Cracker 24 7 59 42 25 0.07 -0.05 3 1
9 Phase 2 of JC Experiments 16 JC Cracker 20 8 59 41 14 0.07 0.04 3 1
10 Phase 2 of JC Experiments 4 JC Cracker 13 9 41 41 38 0.05 0.03 3 1
11 Phase 2 of JC Experiments 19 JC Cracker 11 10 25 56 34 0.06 -0.09 3 1
12 Phase 2 of JC Experiments 15 JC Cracker 16 11 60 30 24 0.07 0.02 3 2
13 Phase 2 of JC Experiments 16 JC Cracker 20 12 59 41 14 0.07 0.04 3 3
14 Phase 2 of JC Experiments 10 JC Cracker 26 13 60 44 12 0.08 -0.1 3 2
15 Phase 2 of JC Experiments 1 JC Cracker 24 14 27 34 29 0.03 0.01 3 1
16 Phase 2 of JC Experiments 17 JC Cracker 34 15 60 34 29 0.07 -0.09 3 2
17 Phase 2 of JC Experiments 5 JC Cracker 30 16 33 30 36 0.05 -0.04 3 3
18 Phase 2 of JC Experiments 2 JC Cracker 38 17 10 60 28 0.06 -0.1 3 3
19 Phase 2 of JC Experiments 2 JC Cracker 38 18 10 60 28 0.06 -0.1 3 1
20 Phase 2 of JC Experiments 13 JC Cracker 21 19 59 41 21 0.06 -0.09 3 2
21 Phase 2 of JC Experiments 1 JC Cracker 24 20 27 34 29 0.03 0.01 3 2
22 Phase 2 of JC Experiments 14 JC Cracker 22 21 59 45 17 0.07 -0.08 3 2
23 Phase 2 of JC Experiments 6 JC Cracker 10 22 22 37 30 0.06 0.02 3 2
24 Phase 2 of JC Experiments 11 JC Cracker 24 23 59 42 25 0.07 -0.05 3 2
25 Phase 2 of JC Experiments 19 JC Cracker 11 24 25 56 34 0.06 -0.09 3 2
26 Phase 2 of JC Experiments 8 JC Cracker 27 25 12 55 24 0.04 0.04 3 2
27 Phase 2 of JC Experiments 18 JC Cracker 18 26 49 38 35 0.07 -0.08 3 3
28 Phase 2 of JC Experiments 5 JC Cracker 30 27 33 30 36 0.05 -0.04 3 1
29 Phase 2 of JC Experiments 9 JC Cracker 32 28 26 47 26 0.07 0.03 3 1
30 Phase 2 of JC Experiments 3 JC Cracker 11 29 36 42 13 0.07 -0.07 3 1
31 Phase 2 of JC Experiments 10 JC Cracker 26 30 60 44 12 0.08 -0.1 3 1
32 Phase 2 of JC Experiments 8 JC Cracker 27 31 12 55 24 0.04 0.04 3 3
33 Phase 2 of JC Experiments 5 JC Cracker 30 32 33 30 36 0.05 -0.04 3 2
34 Phase 2 of JC Experiments 8 JC Cracker 27 33 12 55 24 0.04 0.04 3 1
35 Phase 2 of JC Experiments 18 JC Cracker 18 34 49 38 35 0.07 -0.08 3 2
36 Phase 2 of JC Experiments 3 JC Cracker 11 35 36 42 13 0.07 -0.07 3 3
37 Phase 2 of JC Experiments 10 JC Cracker 26 36 60 44 12 0.08 -0.1 3 3
38 Phase 2 of JC Experiments 17 JC Cracker 34 37 60 34 29 0.07 -0.09 3 3
39 Phase 2 of JC Experiments 13 JC Cracker 21 38 59 41 21 0.06 -0.09 3 3
40 Phase 2 of JC Experiments 12 JC Cracker 28 39 59 37 23 0.06 -0.08 3 2
41 Phase 2 of JC Experiments 9 JC Cracker 32 40 26 47 26 0.07 0.03 3 3
42 Phase 2 of JC Experiments 14 JC Cracker 22 41 59 45 17 0.07 -0.08 3 3
43 Phase 2 of JC Experiments 0 JC Cracker 34 42 19 53 28 0.05 -0.09 3 2
44 Phase 2 of JC Experiments 7 JC Cracker 15 43 30 35 32 0.05 -0.07 3 1
45 Phase 2 of JC Experiments 0 JC Cracker 34 44 19 53 28 0.05 -0.09 3 3
46 Phase 2 of JC Experiments 15 JC Cracker 16 45 60 30 24 0.07 0.02 3 3
47 Phase 2 of JC Experiments 13 JC Cracker 21 46 59 41 21 0.06 -0.09 3 1
48 Phase 2 of JC Experiments 11 JC Cracker 24 47 59 42 25 0.07 -0.05 3 3
49 Phase 2 of JC Experiments 7 JC Cracker 15 48 30 35 32 0.05 -0.07 3 3
50 Phase 2 of JC Experiments 16 JC Cracker 20 49 59 41 14 0.07 0.04 3 2
51 Phase 2 of JC Experiments 3 JC Cracker 11 50 36 42 13 0.07 -0.07 3 2
52 Phase 2 of JC Experiments 7 JC Cracker 15 51 30 35 32 0.05 -0.07 3 2
53 Phase 2 of JC Experiments 6 JC Cracker 10 52 22 37 30 0.06 0.02 3 1
54 Phase 2 of JC Experiments 19 JC Cracker 11 53 25 56 34 0.06 -0.09 3 3
55 Phase 2 of JC Experiments 6 JC Cracker 10 54 22 37 30 0.06 0.02 3 3
56 Phase 2 of JC Experiments 2 JC Cracker 38 55 10 60 28 0.06 -0.1 3 2
57 Phase 2 of JC Experiments 14 JC Cracker 22 56 59 45 17 0.07 -0.08 3 1
58 Phase 2 of JC Experiments 4 JC Cracker 13 57 41 41 38 0.05 0.03 3 3
59 Phase 2 of JC Experiments 9 JC Cracker 32 58 26 47 26 0.07 0.03 3 2
60 Phase 2 of JC Experiments 17 JC Cracker 34 59 60 34 29 0.07 -0.09 3 1
61 Phase 2 of JC Experiments 12 JC Cracker 28 60 59 37 23 0.06 -0.08 3 3

View File

@@ -14,6 +14,8 @@ export function CameraPage({ cameraName }: CameraPageProps) {
const [mqttEvents, setMqttEvents] = useState<MqttEvent[]>([])
const [autoRecordingError, setAutoRecordingError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [retrying, setRetrying] = useState(false)
const imgRef = useRef<HTMLImageElement>(null)
const statusIntervalRef = useRef<number | null>(null)
const mqttEventsIntervalRef = useRef<number | null>(null)
@@ -121,19 +123,39 @@ export function CameraPage({ cameraName }: CameraPageProps) {
const loadCameraData = async () => {
try {
setLoading(true)
setError(null)
await Promise.all([loadCameraStatus(), loadCameraConfig()])
} catch (error) {
console.error('Error loading camera data:', error)
const errorMessage = error instanceof Error ? error.message : 'Failed to load camera data'
setError(errorMessage)
} finally {
setLoading(false)
}
}
const handleRetry = async () => {
setRetrying(true)
setError(null)
try {
await loadCameraData()
// Also retry streaming if it was previously active
if (streamStatus === 'error') {
await startStreaming()
}
} catch (error) {
console.error('Error retrying:', error)
} finally {
setRetrying(false)
}
}
const loadCameraStatus = async () => {
try {
const status = await visionApi.getCameraStatus(cameraName)
setCameraStatus(status)
setIsRecording(status.is_recording)
setError(null) // Clear error on successful load
// Update stream status based on camera status
if (status.status === 'streaming' || status.status === 'available') {
@@ -144,6 +166,11 @@ export function CameraPage({ cameraName }: CameraPageProps) {
}
} catch (error) {
console.error('Error loading camera status:', error)
// Only set error if we don't have status data at all
if (!cameraStatus) {
const errorMessage = error instanceof Error ? error.message : 'Failed to load camera status'
setError(errorMessage)
}
}
}
@@ -151,8 +178,14 @@ export function CameraPage({ cameraName }: CameraPageProps) {
try {
const config = await visionApi.getCameraConfig(cameraName)
setCameraConfig(config)
setError(null) // Clear error on successful load
} catch (error) {
console.error('Error loading camera config:', error)
// Only set error if we don't have config data at all
if (!cameraConfig) {
const errorMessage = error instanceof Error ? error.message : 'Failed to load camera configuration'
setError(errorMessage)
}
}
}
@@ -277,7 +310,7 @@ export function CameraPage({ cameraName }: CameraPageProps) {
}
}
if (loading) {
if (loading && !cameraStatus && !cameraConfig) {
return (
<div className="h-screen flex items-center justify-center bg-gray-900">
<div className="text-center text-white">
@@ -288,6 +321,51 @@ export function CameraPage({ cameraName }: CameraPageProps) {
)
}
if (error && !cameraStatus && !cameraConfig) {
return (
<div className="h-screen flex items-center justify-center bg-gray-900">
<div className="max-w-md w-full mx-4">
<div className="bg-red-50 border border-red-200 rounded-md p-6">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800">Error loading camera</h3>
<div className="mt-2 text-sm text-red-700">
<p>{error}</p>
</div>
<div className="mt-4">
<button
onClick={handleRetry}
disabled={retrying}
className="bg-red-100 px-4 py-2 rounded-md text-sm font-medium text-red-800 hover:bg-red-200 disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 flex items-center space-x-2"
>
{retrying ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-800"></div>
<span>Retrying...</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Reload Module</span>
</>
)}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
const healthStatus = getHealthStatus()
return (

View File

@@ -217,7 +217,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
return <DataEntry />
case 'vision-system':
return (
<ErrorBoundary fallback={<div className="p-6">Failed to load vision system module. Please try again.</div>}>
<ErrorBoundary>
<Suspense fallback={<div className="p-6">Loading vision system module...</div>}>
<RemoteVisionSystem />
</Suspense>
@@ -225,7 +225,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
)
case 'scheduling':
return (
<ErrorBoundary fallback={<div className="p-6">Failed to load scheduling module. Please try again.</div>}>
<ErrorBoundary>
<Suspense fallback={<div className="p-6">Loading scheduling module...</div>}>
<RemoteScheduling user={user} currentRoute={currentRoute} />
</Suspense>
@@ -233,7 +233,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
)
case 'video-library':
return (
<ErrorBoundary fallback={<div className="p-6">Failed to load video module. Please try again.</div>}>
<ErrorBoundary>
<Suspense fallback={<div className="p-6">Loading video module...</div>}>
<RemoteVideoLibrary />
</Suspense>
@@ -312,7 +312,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
)}
</div>
<div
className={`flex-1 transition-all duration-300 ease-in-out bg-gray-50 dark:bg-gray-900 ${isExpanded || isHovered ? "lg:ml-[290px]" : "lg:ml-[90px]"
className={`flex-1 transition-all duration-300 ease-in-out bg-gray-50 dark:bg-gray-900 flex flex-col min-h-0 ${isExpanded || isHovered ? "lg:ml-[290px]" : "lg:ml-[90px]"
} ${isMobileOpen ? "ml-0" : ""}`}
>
<TopNavbar
@@ -323,7 +323,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
isSidebarOpen={isMobileOpen}
onNavigateToProfile={() => handleViewChange('profile')}
/>
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">
<div className="flex-1 min-h-0 overflow-hidden p-4 md:p-6">
{renderCurrentView()}
</div>
</div>

View File

@@ -109,7 +109,40 @@ export function DataEntry() {
return (
<div className="p-6">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800 dark:text-red-400">Error loading data</h3>
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
<p>{error}</p>
</div>
<div className="mt-4">
<button
onClick={loadData}
disabled={loading}
className="bg-red-100 dark:bg-red-900/30 px-4 py-2 rounded-md text-sm font-medium text-red-800 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/50 disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 flex items-center space-x-2"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-800 dark:border-red-300"></div>
<span>Retrying...</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Reload Module</span>
</>
)}
</button>
</div>
</div>
</div>
</div>
</div>
)

View File

@@ -1,6 +1,11 @@
import { Component, ReactNode } from 'react'
type Props = { children: ReactNode, fallback?: ReactNode }
type Props = {
children: ReactNode
fallback?: ReactNode
onRetry?: () => void
showRetry?: boolean
}
type State = { hasError: boolean }
export class ErrorBoundary extends Component<Props, State> {
@@ -12,9 +17,48 @@ export class ErrorBoundary extends Component<Props, State> {
componentDidCatch() {}
handleRetry = () => {
this.setState({ hasError: false })
if (this.props.onRetry) {
this.props.onRetry()
}
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <div className="p-6">Something went wrong loading this section.</div>
if (this.props.fallback) {
return this.props.fallback
}
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800">Something went wrong loading this section</h3>
<div className="mt-2 text-sm text-red-700">
<p>An error occurred while loading this component. Please try reloading it.</p>
</div>
{(this.props.showRetry !== false) && (
<div className="mt-4">
<button
onClick={this.handleRetry}
className="bg-red-100 px-4 py-2 rounded-md text-sm font-medium text-red-800 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
Reload Module
</button>
</div>
)}
</div>
</div>
</div>
</div>
)
}
return this.props.children
}

View File

@@ -936,7 +936,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
@@ -944,7 +943,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
@@ -957,7 +955,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()
@@ -999,8 +996,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
return (
<div className="p-6">
<div className="mb-6">
<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"
@@ -1018,7 +1015,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">
<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>
)}
@@ -1033,7 +1031,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">Fetching conductors, phases, and experiments.</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 flex-shrink-0">
{/* Left: Conductors with future availability */}
<div>
<div className="flex items-center justify-between mb-3">
@@ -1254,8 +1252,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</div>
)}
{/* Week Calendar for selected conductors' availability */}
<div className="mt-6">
<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>
@@ -1297,7 +1295,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</button>
</div>
</div>
<div ref={calendarRef} className="h-[80vh] 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}
@@ -1387,6 +1385,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</div>
</div>
</div>
</div>
</div>
)

View File

@@ -80,8 +80,7 @@ export interface MachineType {
// Phase-specific interfaces
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
@@ -94,8 +93,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
@@ -307,8 +305,7 @@ export interface UpdatePhaseDataRequest {
// Phase creation request interfaces
export interface CreateSoakingRequest {
experiment_id: string
repetition_id?: string
repetition_id: string
scheduled_start_time: string
soaking_duration_minutes: number
actual_start_time?: string
@@ -316,19 +313,17 @@ export interface CreateSoakingRequest {
}
export interface CreateAirdryingRequest {
experiment_id: string
repetition_id?: string
scheduled_start_time?: string // Will be auto-calculated from soaking if not provided
repetition_id: string
scheduled_start_time: string
duration_minutes: number
actual_start_time?: string
actual_end_time?: string
}
export interface CreateCrackingRequest {
experiment_id: string
repetition_id?: string
repetition_id: string
machine_type_id: string
scheduled_start_time?: string // Will be auto-calculated from airdrying if not provided
scheduled_start_time: string
actual_start_time?: string
actual_end_time?: string
}
@@ -798,11 +793,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')
.insert({
...request,
.upsert({
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: 'repetition_id'
})
.select()
.single()
@@ -824,10 +830,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)
.eq('repetition_id', repetitions[0].id)
.single()
if (error) {
@@ -856,11 +875,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')
.insert({
...request,
.upsert({
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: 'repetition_id'
})
.select()
.single()
@@ -882,10 +916,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)
.eq('repetition_id', repetitions[0].id)
.single()
if (error) {
@@ -914,11 +961,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')
.insert({
...request,
.upsert({
repetition_id: request.repetition_id,
machine_type_id: request.machine_type_id,
scheduled_start_time: request.scheduled_start_time,
created_by: user.id
}, {
onConflict: 'repetition_id'
})
.select()
.single()

View File

@@ -1 +1 @@
v2.62.10
v2.65.2

View File

@@ -57,7 +57,7 @@ schema_paths = []
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed_01_users.sql"]
sql_paths = ["./seed_01_users.sql", "./seed_02_phase2_experiments.sql"]
# , "./seed_04_phase2_jc_experiments.sql", "./seed_05_meyer_experiments.sql"]
[db.network_restrictions]

View File

@@ -15,7 +15,10 @@ CREATE TABLE IF NOT EXISTS public.experiments (
phase_id UUID NOT NULL REFERENCES public.experiment_phases(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id)
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure unique combination of experiment_number and phase_id
CONSTRAINT unique_experiment_number_phase UNIQUE (experiment_number, phase_id)
);
-- =============================================

View File

@@ -7,8 +7,7 @@
CREATE TABLE IF NOT EXISTS public.soaking (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
actual_start_time TIMESTAMP WITH TIME ZONE,
soaking_duration_minutes INTEGER NOT NULL CHECK (soaking_duration_minutes > 0),
@@ -18,8 +17,7 @@ CREATE TABLE IF NOT EXISTS public.soaking (
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure only one soaking per experiment or repetition
CONSTRAINT unique_soaking_per_experiment UNIQUE (experiment_id),
-- Ensure only one soaking per repetition
CONSTRAINT unique_soaking_per_repetition UNIQUE (repetition_id)
);
@@ -29,8 +27,7 @@ CREATE TABLE IF NOT EXISTS public.soaking (
CREATE TABLE IF NOT EXISTS public.airdrying (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
actual_start_time TIMESTAMP WITH TIME ZONE,
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0),
@@ -40,8 +37,7 @@ CREATE TABLE IF NOT EXISTS public.airdrying (
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure only one airdrying per experiment or repetition
CONSTRAINT unique_airdrying_per_experiment UNIQUE (experiment_id),
-- Ensure only one airdrying per repetition
CONSTRAINT unique_airdrying_per_repetition UNIQUE (repetition_id)
);
@@ -51,8 +47,7 @@ CREATE TABLE IF NOT EXISTS public.airdrying (
CREATE TABLE IF NOT EXISTS public.cracking (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
machine_type_id UUID NOT NULL REFERENCES public.machine_types(id),
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
actual_start_time TIMESTAMP WITH TIME ZONE,
@@ -61,8 +56,7 @@ CREATE TABLE IF NOT EXISTS public.cracking (
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure only one cracking per experiment or repetition
CONSTRAINT unique_cracking_per_experiment UNIQUE (experiment_id),
-- Ensure only one cracking per repetition
CONSTRAINT unique_cracking_per_repetition UNIQUE (repetition_id)
);
@@ -72,8 +66,7 @@ CREATE TABLE IF NOT EXISTS public.cracking (
CREATE TABLE IF NOT EXISTS public.shelling (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
actual_start_time TIMESTAMP WITH TIME ZONE,
actual_end_time TIMESTAMP WITH TIME ZONE,
@@ -81,8 +74,7 @@ CREATE TABLE IF NOT EXISTS public.shelling (
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure only one shelling per experiment or repetition
CONSTRAINT unique_shelling_per_experiment UNIQUE (experiment_id),
-- Ensure only one shelling per repetition
CONSTRAINT unique_shelling_per_repetition UNIQUE (repetition_id)
);
@@ -90,12 +82,6 @@ CREATE TABLE IF NOT EXISTS public.shelling (
-- 5. INDEXES FOR PERFORMANCE
-- =============================================
-- Create indexes for experiment_id references
CREATE INDEX IF NOT EXISTS idx_soaking_experiment_id ON public.soaking(experiment_id);
CREATE INDEX IF NOT EXISTS idx_airdrying_experiment_id ON public.airdrying(experiment_id);
CREATE INDEX IF NOT EXISTS idx_cracking_experiment_id ON public.cracking(experiment_id);
CREATE INDEX IF NOT EXISTS idx_shelling_experiment_id ON public.shelling(experiment_id);
-- Create indexes for repetition references
CREATE INDEX IF NOT EXISTS idx_soaking_repetition_id ON public.soaking(repetition_id);
CREATE INDEX IF NOT EXISTS idx_airdrying_repetition_id ON public.airdrying(repetition_id);
@@ -138,11 +124,11 @@ CREATE OR REPLACE FUNCTION set_airdrying_scheduled_start_time()
RETURNS TRIGGER AS $$
BEGIN
-- If this is a new airdrying record and no scheduled_start_time is provided,
-- try to get it from the associated soaking's scheduled_end_time
-- try to get it from the associated soaking's scheduled_end_time for the same repetition
IF NEW.scheduled_start_time IS NULL THEN
SELECT s.scheduled_end_time INTO NEW.scheduled_start_time
FROM public.soaking s
WHERE s.experiment_id = NEW.experiment_id
WHERE s.repetition_id = NEW.repetition_id
LIMIT 1;
END IF;
RETURN NEW;
@@ -154,11 +140,11 @@ CREATE OR REPLACE FUNCTION set_cracking_scheduled_start_time()
RETURNS TRIGGER AS $$
BEGIN
-- If this is a new cracking record and no scheduled_start_time is provided,
-- try to get it from the associated airdrying's scheduled_end_time
-- try to get it from the associated airdrying's scheduled_end_time for the same repetition
IF NEW.scheduled_start_time IS NULL THEN
SELECT a.scheduled_end_time INTO NEW.scheduled_start_time
FROM public.airdrying a
WHERE a.experiment_id = NEW.experiment_id
WHERE a.repetition_id = NEW.repetition_id
LIMIT 1;
END IF;
RETURN NEW;

View File

@@ -6,6 +6,7 @@
-- =============================================
-- View for experiments with all phase information
-- Note: Since phases are now per-repetition, this view shows phase data from the first repetition
CREATE OR REPLACE VIEW public.experiments_with_phases AS
SELECT
e.id,
@@ -24,6 +25,8 @@ SELECT
ep.has_airdrying,
ep.has_cracking,
ep.has_shelling,
er.id as first_repetition_id,
er.repetition_number as first_repetition_number,
s.id as soaking_id,
s.scheduled_start_time as soaking_scheduled_start,
s.actual_start_time as soaking_actual_start,
@@ -47,11 +50,18 @@ SELECT
sh.actual_end_time as shelling_actual_end
FROM public.experiments e
LEFT JOIN public.experiment_phases ep ON e.phase_id = ep.id
LEFT JOIN public.soaking s ON s.experiment_id = e.id
LEFT JOIN public.airdrying ad ON ad.experiment_id = e.id
LEFT JOIN public.cracking c ON c.experiment_id = e.id
LEFT JOIN LATERAL (
SELECT id, repetition_number
FROM public.experiment_repetitions
WHERE experiment_id = e.id
ORDER BY repetition_number
LIMIT 1
) er ON true
LEFT JOIN public.soaking s ON s.repetition_id = er.id
LEFT JOIN public.airdrying ad ON ad.repetition_id = er.id
LEFT JOIN public.cracking c ON c.repetition_id = er.id
LEFT JOIN public.machine_types mt ON c.machine_type_id = mt.id
LEFT JOIN public.shelling sh ON sh.experiment_id = e.id;
LEFT JOIN public.shelling sh ON sh.repetition_id = er.id;
-- View for repetitions with phase information
CREATE OR REPLACE VIEW public.repetitions_with_phases AS

View File

@@ -0,0 +1,35 @@
-- Add repetition_id foreign key to cracker parameters tables
-- This migration adds a foreign key to link cracker parameters to their repetitions
-- =============================================
-- 1. ADD REPETITION_ID TO JC CRACKER PARAMETERS
-- =============================================
ALTER TABLE public.jc_cracker_parameters
ADD COLUMN IF NOT EXISTS repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE;
-- Add index for performance
CREATE INDEX IF NOT EXISTS idx_jc_cracker_parameters_repetition_id
ON public.jc_cracker_parameters(repetition_id);
-- Add unique constraint to ensure one parameter set per repetition
ALTER TABLE public.jc_cracker_parameters
ADD CONSTRAINT unique_jc_cracker_parameters_per_repetition
UNIQUE (repetition_id);
-- =============================================
-- 2. ADD REPETITION_ID TO MEYER CRACKER PARAMETERS
-- =============================================
ALTER TABLE public.meyer_cracker_parameters
ADD COLUMN IF NOT EXISTS repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE;
-- Add index for performance
CREATE INDEX IF NOT EXISTS idx_meyer_cracker_parameters_repetition_id
ON public.meyer_cracker_parameters(repetition_id);
-- Add unique constraint to ensure one parameter set per repetition
ALTER TABLE public.meyer_cracker_parameters
ADD CONSTRAINT unique_meyer_cracker_parameters_per_repetition
UNIQUE (repetition_id);

File diff suppressed because it is too large Load Diff