feat: Implement Vision System API Client with comprehensive endpoints and utility functions

- Added VisionApiClient class to interact with the vision system API.
- Defined interfaces for system status, machine status, camera status, recordings, and storage stats.
- Implemented methods for health checks, system status retrieval, camera control, and storage management.
- Introduced utility functions for formatting bytes, durations, and uptime.

test: Create manual verification script for Vision API functionality

- Added a test script to verify utility functions and API endpoints.
- Included tests for health check, system status, cameras, machines, and storage stats.

feat: Create experiment repetitions system migration

- Added experiment_repetitions table to manage experiment repetitions with scheduling.
- Implemented triggers and functions for validation and timestamp management.
- Established row-level security policies for user access control.

feat: Introduce phase-specific draft management system migration

- Created experiment_phase_drafts and experiment_phase_data tables for managing phase-specific drafts and measurements.
- Added pecan_diameter_measurements table for individual diameter measurements.
- Implemented row-level security policies for user access control.

fix: Adjust draft constraints to allow multiple drafts while preventing multiple submitted drafts

- Modified constraints on experiment_phase_drafts to allow multiple drafts in 'draft' or 'withdrawn' status.
- Ensured only one 'submitted' draft per user per phase per repetition.
This commit is contained in:
Alireza Vaezi
2025-07-28 16:30:56 -04:00
parent 8a1b22234b
commit 7c7115a0f4
27 changed files with 4219 additions and 683 deletions

View File

@@ -38,15 +38,15 @@ export interface Experiment {
throughput_rate_pecans_sec: number
crush_amount_in: number
entry_exit_height_diff_in: number
schedule_status: ScheduleStatus
results_status: ResultsStatus
completion_status: boolean
scheduled_date?: string | null
created_at: string
updated_at: string
created_by: string
}
export interface CreateExperimentRequest {
experiment_number: number
reps_required: number
@@ -56,10 +56,8 @@ export interface CreateExperimentRequest {
throughput_rate_pecans_sec: number
crush_amount_in: number
entry_exit_height_diff_in: number
schedule_status?: ScheduleStatus
results_status?: ResultsStatus
completion_status?: boolean
scheduled_date?: string | null
}
export interface UpdateExperimentRequest {
@@ -71,25 +69,54 @@ export interface UpdateExperimentRequest {
throughput_rate_pecans_sec?: number
crush_amount_in?: number
entry_exit_height_diff_in?: number
schedule_status?: ScheduleStatus
results_status?: ResultsStatus
completion_status?: boolean
}
export interface CreateRepetitionRequest {
experiment_id: string
repetition_number: number
scheduled_date?: string | null
schedule_status?: ScheduleStatus
}
export interface UpdateRepetitionRequest {
scheduled_date?: string | null
schedule_status?: ScheduleStatus
completion_status?: boolean
}
// Data Entry System Interfaces
export type DataEntryStatus = 'draft' | 'submitted'
export type PhaseDraftStatus = 'draft' | 'submitted' | 'withdrawn'
export type ExperimentPhase = 'pre-soaking' | 'air-drying' | 'cracking' | 'shelling'
export interface ExperimentDataEntry {
export interface ExperimentPhaseDraft {
id: string
experiment_id: string
repetition_id: string
user_id: string
status: DataEntryStatus
entry_name?: string | null
phase_name: ExperimentPhase
status: PhaseDraftStatus
draft_name?: string | null
created_at: string
updated_at: string
submitted_at?: string | null
withdrawn_at?: string | null
}
export interface ExperimentRepetition {
id: string
experiment_id: string
repetition_number: number
scheduled_date?: string | null
schedule_status: ScheduleStatus
completion_status: boolean
is_locked: boolean
locked_at?: string | null
locked_by?: string | null
created_at: string
updated_at: string
created_by: string
}
export interface PecanDiameterMeasurement {
@@ -102,7 +129,7 @@ export interface PecanDiameterMeasurement {
export interface ExperimentPhaseData {
id: string
data_entry_id: string
phase_draft_id: string
phase_name: ExperimentPhase
// Pre-soaking phase
@@ -141,15 +168,17 @@ export interface ExperimentPhaseData {
diameter_measurements?: PecanDiameterMeasurement[]
}
export interface CreateDataEntryRequest {
export interface CreatePhaseDraftRequest {
experiment_id: string
entry_name?: string
status?: DataEntryStatus
repetition_id: string
phase_name: ExperimentPhase
draft_name?: string
status?: PhaseDraftStatus
}
export interface UpdateDataEntryRequest {
entry_name?: string
status?: DataEntryStatus
export interface UpdatePhaseDraftRequest {
draft_name?: string
status?: PhaseDraftStatus
}
export interface CreatePhaseDataRequest {
@@ -440,25 +469,7 @@ export const experimentManagement = {
return data
},
// Schedule an experiment
async scheduleExperiment(id: string, scheduledDate: string): Promise<Experiment> {
const updates: UpdateExperimentRequest = {
scheduled_date: scheduledDate,
schedule_status: 'scheduled'
}
return this.updateExperiment(id, updates)
},
// Remove experiment schedule
async removeExperimentSchedule(id: string): Promise<Experiment> {
const updates: UpdateExperimentRequest = {
scheduled_date: null,
schedule_status: 'pending schedule'
}
return this.updateExperiment(id, updates)
},
// Check if experiment number is unique
async isExperimentNumberUnique(experimentNumber: number, excludeId?: string): Promise<boolean> {
@@ -478,45 +489,237 @@ export const experimentManagement = {
}
}
// Data Entry Management
export const dataEntryManagement = {
// Get all data entries for an experiment
async getDataEntriesForExperiment(experimentId: string): Promise<ExperimentDataEntry[]> {
// Experiment Repetitions Management
export const repetitionManagement = {
// Get all repetitions for an experiment
async getExperimentRepetitions(experimentId: string): Promise<ExperimentRepetition[]> {
const { data, error } = await supabase
.from('experiment_data_entries')
.from('experiment_repetitions')
.select('*')
.eq('experiment_id', experimentId)
.order('repetition_number', { ascending: true })
if (error) throw error
return data
},
// Create a new repetition
async createRepetition(repetitionData: CreateRepetitionRequest): Promise<ExperimentRepetition> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('experiment_repetitions')
.insert({
...repetitionData,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
// Update a repetition
async updateRepetition(id: string, updates: UpdateRepetitionRequest): Promise<ExperimentRepetition> {
const { data, error } = await supabase
.from('experiment_repetitions')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
// Schedule a repetition
async scheduleRepetition(id: string, scheduledDate: string): Promise<ExperimentRepetition> {
const updates: UpdateRepetitionRequest = {
scheduled_date: scheduledDate,
schedule_status: 'scheduled'
}
return this.updateRepetition(id, updates)
},
// Remove repetition schedule
async removeRepetitionSchedule(id: string): Promise<ExperimentRepetition> {
const updates: UpdateRepetitionRequest = {
scheduled_date: null,
schedule_status: 'pending schedule'
}
return this.updateRepetition(id, updates)
},
// Delete a repetition
async deleteRepetition(id: string): Promise<void> {
const { error } = await supabase
.from('experiment_repetitions')
.delete()
.eq('id', id)
if (error) throw error
},
// Get repetitions by status
async getRepetitionsByStatus(scheduleStatus?: ScheduleStatus): Promise<ExperimentRepetition[]> {
let query = supabase.from('experiment_repetitions').select('*')
if (scheduleStatus) {
query = query.eq('schedule_status', scheduleStatus)
}
const { data, error } = await query.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get repetitions with experiment details
async getRepetitionsWithExperiments(): Promise<(ExperimentRepetition & { experiment: Experiment })[]> {
const { data, error } = await supabase
.from('experiment_repetitions')
.select(`
*,
experiment:experiments(*)
`)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get user's data entries for an experiment
async getUserDataEntriesForExperiment(experimentId: string, userId?: string): Promise<ExperimentDataEntry[]> {
// Create all repetitions for an experiment
async createAllRepetitions(experimentId: string): Promise<ExperimentRepetition[]> {
// First get the experiment to know how many reps are required
const { data: experiment, error: expError } = await supabase
.from('experiments')
.select('reps_required')
.eq('id', experimentId)
.single()
if (expError) throw expError
// Create repetitions for each required rep
const repetitions: CreateRepetitionRequest[] = []
for (let i = 1; i <= experiment.reps_required; i++) {
repetitions.push({
experiment_id: experimentId,
repetition_number: i,
schedule_status: 'pending schedule'
})
}
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const targetUserId = userId || user.id
const { data, error } = await supabase
.from('experiment_repetitions')
.insert(repetitions.map(rep => ({
...rep,
created_by: user.id
})))
.select()
if (error) throw error
return data
},
// Lock a repetition (admin only)
async lockRepetition(repetitionId: string): Promise<ExperimentRepetition> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('experiment_data_entries')
.from('experiment_repetitions')
.update({
is_locked: true,
locked_at: new Date().toISOString(),
locked_by: user.id
})
.eq('id', repetitionId)
.select()
.single()
if (error) throw error
return data
},
// Unlock a repetition (admin only)
async unlockRepetition(repetitionId: string): Promise<ExperimentRepetition> {
const { data, error } = await supabase
.from('experiment_repetitions')
.update({
is_locked: false,
locked_at: null,
locked_by: null
})
.eq('id', repetitionId)
.select()
.single()
if (error) throw error
return data
}
}
// Phase Draft Management
export const phaseDraftManagement = {
// Get all phase drafts for a repetition
async getPhaseDraftsForRepetition(repetitionId: string): Promise<ExperimentPhaseDraft[]> {
const { data, error } = await supabase
.from('experiment_phase_drafts')
.select('*')
.eq('experiment_id', experimentId)
.eq('user_id', targetUserId)
.eq('repetition_id', repetitionId)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Create a new data entry
async createDataEntry(request: CreateDataEntryRequest): Promise<ExperimentDataEntry> {
// Get user's phase drafts for a repetition
async getUserPhaseDraftsForRepetition(repetitionId: string): Promise<ExperimentPhaseDraft[]> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('experiment_data_entries')
.from('experiment_phase_drafts')
.select('*')
.eq('repetition_id', repetitionId)
.eq('user_id', user.id)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get user's phase drafts for a specific phase and repetition
async getUserPhaseDraftsForPhase(repetitionId: string, phase: ExperimentPhase): Promise<ExperimentPhaseDraft[]> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('experiment_phase_drafts')
.select('*')
.eq('repetition_id', repetitionId)
.eq('user_id', user.id)
.eq('phase_name', phase)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Create a new phase draft
async createPhaseDraft(request: CreatePhaseDraftRequest): Promise<ExperimentPhaseDraft> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('experiment_phase_drafts')
.insert({
...request,
user_id: user.id
@@ -528,10 +731,10 @@ export const dataEntryManagement = {
return data
},
// Update a data entry
async updateDataEntry(id: string, updates: UpdateDataEntryRequest): Promise<ExperimentDataEntry> {
// Update a phase draft
async updatePhaseDraft(id: string, updates: UpdatePhaseDraftRequest): Promise<ExperimentPhaseDraft> {
const { data, error } = await supabase
.from('experiment_data_entries')
.from('experiment_phase_drafts')
.update(updates)
.eq('id', id)
.select()
@@ -541,65 +744,53 @@ export const dataEntryManagement = {
return data
},
// Delete a data entry (only drafts)
async deleteDataEntry(id: string): Promise<void> {
// Delete a phase draft (only drafts)
async deletePhaseDraft(id: string): Promise<void> {
const { error } = await supabase
.from('experiment_data_entries')
.from('experiment_phase_drafts')
.delete()
.eq('id', id)
if (error) throw error
},
// Submit a data entry (change status from draft to submitted)
async submitDataEntry(id: string): Promise<ExperimentDataEntry> {
return this.updateDataEntry(id, { status: 'submitted' })
// Submit a phase draft (change status from draft to submitted)
async submitPhaseDraft(id: string): Promise<ExperimentPhaseDraft> {
return this.updatePhaseDraft(id, { status: 'submitted' })
},
// Get phase data for a data entry
async getPhaseDataForEntry(dataEntryId: string): Promise<ExperimentPhaseData[]> {
// Withdraw a phase draft (change status from submitted to withdrawn)
async withdrawPhaseDraft(id: string): Promise<ExperimentPhaseDraft> {
return this.updatePhaseDraft(id, { status: 'withdrawn' })
},
// Get phase data for a phase draft
async getPhaseDataForDraft(phaseDraftId: string): Promise<ExperimentPhaseData | null> {
const { data, error } = await supabase
.from('experiment_phase_data')
.select(`
*,
diameter_measurements:pecan_diameter_measurements(*)
`)
.eq('data_entry_id', dataEntryId)
.order('phase_name')
if (error) throw error
return data
},
// Get specific phase data
async getPhaseData(dataEntryId: string, phaseName: ExperimentPhase): Promise<ExperimentPhaseData | null> {
const { data, error } = await supabase
.from('experiment_phase_data')
.select(`
*,
diameter_measurements:pecan_diameter_measurements(*)
`)
.eq('data_entry_id', dataEntryId)
.eq('phase_name', phaseName)
.eq('phase_draft_id', phaseDraftId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
if (error.code === 'PGRST116') return null // No rows found
throw error
}
return data
},
// Create or update phase data
async upsertPhaseData(dataEntryId: string, phaseName: ExperimentPhase, phaseData: Partial<ExperimentPhaseData>): Promise<ExperimentPhaseData> {
// Create or update phase data for a draft
async upsertPhaseData(phaseDraftId: string, phaseData: Partial<ExperimentPhaseData>): Promise<ExperimentPhaseData> {
const { data, error } = await supabase
.from('experiment_phase_data')
.upsert({
data_entry_id: dataEntryId,
phase_name: phaseName,
phase_draft_id: phaseDraftId,
...phaseData
}, {
onConflict: 'data_entry_id,phase_name'
onConflict: 'phase_draft_id,phase_name'
})
.select()
.single()
@@ -641,9 +832,9 @@ export const dataEntryManagement = {
},
// Auto-save draft data (for periodic saves)
async autoSaveDraft(dataEntryId: string, phaseName: ExperimentPhase, phaseData: Partial<ExperimentPhaseData>): Promise<void> {
async autoSaveDraft(phaseDraftId: string, phaseData: Partial<ExperimentPhaseData>): Promise<void> {
try {
await this.upsertPhaseData(dataEntryId, phaseName, phaseData)
await this.upsertPhaseData(phaseDraftId, phaseData)
} catch (error) {
console.warn('Auto-save failed:', error)
// Don't throw error for auto-save failures