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:
@@ -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
|
||||
|
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.62.10
|
||||
v2.65.2
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
3196
management-dashboard-web-app/supabase/seed_02_phase2_experiments.sql
Normal file
3196
management-dashboard-web-app/supabase/seed_02_phase2_experiments.sql
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user