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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user