Refactor docker-compose setup and enhance scheduling components

- Re-enabled Vision API and Media API services in docker-compose.yml, providing necessary configurations for development.
- Improved scheduling logic in HorizontalTimelineCalendar and Scheduling components to better manage repetition visibility and database scheduling status.
- Updated docker-compose-reset.sh to conditionally wait for Supabase services, enhancing the setup process for local development.
- Added isScheduledInDb prop to manage UI states for scheduled repetitions, improving user experience in the scheduling interface.
This commit is contained in:
salirezav
2026-02-02 11:25:37 -05:00
parent 780a95549b
commit 49ddcfd002
4 changed files with 284 additions and 212 deletions

View File

@@ -299,74 +299,71 @@ services:
# - usda-vision-network # - usda-vision-network
# restart: unless-stopped # restart: unless-stopped
# #
# ============================================================================ api:
# Vision API Service - DISABLED FOR DEVELOPMENT container_name: usda-vision-api
# ============================================================================ build:
# api: context: ./camera-management-api
# container_name: usda-vision-api dockerfile: Dockerfile
# build: working_dir: /app
# context: ./camera-management-api restart: unless-stopped # Automatically restart container if it fails or exits
# dockerfile: Dockerfile healthcheck:
# working_dir: /app test: ["CMD-SHELL", "python3 -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\").read()' || exit 1"]
# restart: unless-stopped # Automatically restart container if it fails or exits interval: 30s
# healthcheck: timeout: 10s
# test: ["CMD-SHELL", "python3 -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\").read()' || exit 1"] retries: 3
# interval: 30s start_period: 60s
# timeout: 10s volumes:
# retries: 3 - ./camera-management-api:/app
# start_period: 60s - /mnt/nfs_share:/mnt/nfs_share
# volumes: - /etc/localtime:/etc/localtime:ro
# - ./camera-management-api:/app - /etc/timezone:/etc/timezone:ro
# - /mnt/nfs_share:/mnt/nfs_share environment:
# - /etc/localtime:/etc/localtime:ro - PYTHONUNBUFFERED=1
# - /etc/timezone:/etc/timezone:ro - LD_LIBRARY_PATH=/usr/local/lib:/lib:/usr/lib
# environment: - PYTHONPATH=/app:/app/camera_sdk
# - PYTHONUNBUFFERED=1 - TZ=America/New_York
# - LD_LIBRARY_PATH=/usr/local/lib:/lib:/usr/lib - MEDIAMTX_HOST=localhost
# - PYTHONPATH=/app:/app/camera_sdk - MEDIAMTX_RTSP_PORT=8554
# - TZ=America/New_York command: >
# - MEDIAMTX_HOST=localhost sh -lc "
# - MEDIAMTX_RTSP_PORT=8554 set -e # Exit on error
# command: >
# sh -lc " # Only install system packages if not already installed (check for ffmpeg)
# set -e # Exit on error if ! command -v ffmpeg &> /dev/null; then
# echo 'Installing system dependencies...';
# # Only install system packages if not already installed (check for ffmpeg) apt-get update && apt-get install -y --no-install-recommends libusb-1.0-0-dev ffmpeg;
# if ! command -v ffmpeg &> /dev/null; then else
# echo 'Installing system dependencies...'; echo 'System dependencies already installed';
# apt-get update && apt-get install -y --no-install-recommends libusb-1.0-0-dev ffmpeg; fi
# else
# echo 'System dependencies already installed';
# fi
# # Install camera SDK if not already installed # Install camera SDK if not already installed
# if [ ! -f /lib/libMVSDK.so ] && [ -f 'camera_sdk/linuxSDK_V2.1.0.49(250108)/install.sh' ]; then if [ ! -f /lib/libMVSDK.so ] && [ -f 'camera_sdk/linuxSDK_V2.1.0.49(250108)/install.sh' ]; then
# echo 'Installing camera SDK...'; echo 'Installing camera SDK...';
# cd 'camera_sdk/linuxSDK_V2.1.0.49(250108)'; cd 'camera_sdk/linuxSDK_V2.1.0.49(250108)';
# chmod +x install.sh; chmod +x install.sh;
# ./install.sh || echo 'Warning: Camera SDK installation may have failed'; ./install.sh || echo 'Warning: Camera SDK installation may have failed';
# cd /app; cd /app;
# else else
# echo 'Camera SDK already installed or install script not found'; echo 'Camera SDK already installed or install script not found';
# fi; fi;
# # Install Python dependencies (only if requirements.txt changed or packages missing) # Install Python dependencies (only if requirements.txt changed or packages missing)
# if [ -f requirements.txt ]; then if [ -f requirements.txt ]; then
# pip install --no-cache-dir -r requirements.txt || echo 'Warning: Some Python packages may have failed to install'; pip install --no-cache-dir -r requirements.txt || echo 'Warning: Some Python packages may have failed to install';
# else else
# pip install --no-cache-dir -e . || echo 'Warning: Package installation may have failed'; pip install --no-cache-dir -e . || echo 'Warning: Package installation may have failed';
# fi; fi;
# # Start the application with error handling # Start the application with error handling
# echo 'Starting USDA Vision Camera System...'; echo 'Starting USDA Vision Camera System...';
# python main.py --config config.compose.json || { python main.py --config config.compose.json || {
# echo 'Application exited with error code: $?'; echo 'Application exited with error code: $?';
# echo 'Waiting 5 seconds before exit...'; echo 'Waiting 5 seconds before exit...';
# sleep 5; sleep 5;
# exit 1; exit 1;
# } }
# " "
# network_mode: host network_mode: host
web: web:
container_name: usda-vision-web container_name: usda-vision-web
@@ -424,31 +421,28 @@ services:
networks: networks:
- usda-vision-network - usda-vision-network
# ============================================================================ vision-system-remote:
# Vision System Remote - DISABLED FOR DEVELOPMENT container_name: usda-vision-vision-system-remote
# ============================================================================ image: node:20-alpine
# vision-system-remote: working_dir: /app
# container_name: usda-vision-vision-system-remote environment:
# image: node:20-alpine - CHOKIDAR_USEPOLLING=true
# working_dir: /app - TZ=America/New_York
# environment: # Use environment variable with fallback to localhost
# - CHOKIDAR_USEPOLLING=true - VITE_VISION_API_URL=${VITE_VISION_API_URL:-http://localhost:8000}
# - TZ=America/New_York volumes:
# # Use environment variable with fallback to localhost - ./vision-system-remote:/app
# - VITE_VISION_API_URL=${VITE_VISION_API_URL:-http://localhost:8000} command: >
# volumes: sh -lc "
# - ./vision-system-remote:/app npm install;
# command: > npm run dev:watch
# sh -lc " "
# npm install; extra_hosts:
# npm run dev:watch - "host.docker.internal:host-gateway"
# " ports:
# extra_hosts: - "3002:3002"
# - "host.docker.internal:host-gateway" networks:
# ports: - usda-vision-network
# - "3002:3002"
# networks:
# - usda-vision-network
scheduling-remote: scheduling-remote:
container_name: usda-vision-scheduling-remote container_name: usda-vision-scheduling-remote
@@ -473,36 +467,33 @@ services:
networks: networks:
- usda-vision-network - usda-vision-network
# ============================================================================ media-api:
# Media API Service - DISABLED FOR DEVELOPMENT container_name: usda-vision-media-api
# ============================================================================ build:
# media-api: context: ./media-api
# container_name: usda-vision-media-api dockerfile: Dockerfile
# build: environment:
# context: ./media-api - MEDIA_VIDEOS_DIR=/mnt/nfs_share
# dockerfile: Dockerfile - MEDIA_THUMBS_DIR=/mnt/nfs_share/.thumbnails
# environment: - MAX_CONCURRENT_TRANSCODING=2 # Limit concurrent transcoding operations
# - MEDIA_VIDEOS_DIR=/mnt/nfs_share volumes:
# - MEDIA_THUMBS_DIR=/mnt/nfs_share/.thumbnails - /mnt/nfs_share:/mnt/nfs_share
# - MAX_CONCURRENT_TRANSCODING=2 # Limit concurrent transcoding operations ports:
# volumes: - "8090:8090"
# - /mnt/nfs_share:/mnt/nfs_share networks:
# ports: - usda-vision-network
# - "8090:8090" deploy:
# networks: resources:
# - usda-vision-network limits:
# deploy: cpus: '4' # Limit to 4 CPU cores (adjust based on your system)
# resources: memory: 2G # Limit to 2GB RAM per container
# limits: reservations:
# cpus: '4' # Limit to 4 CPU cores (adjust based on your system) cpus: '1' # Reserve at least 1 CPU core
# memory: 2G # Limit to 2GB RAM per container memory: 512M # Reserve at least 512MB RAM
# reservations: # Alternative syntax for older Docker Compose versions:
# cpus: '1' # Reserve at least 1 CPU core # cpus: '4'
# memory: 512M # Reserve at least 512MB RAM # mem_limit: 2g
# # Alternative syntax for older Docker Compose versions: # mem_reservation: 512m
# # cpus: '4'
# # mem_limit: 2g
# # mem_reservation: 512m
mediamtx: mediamtx:
container_name: usda-vision-mediamtx container_name: usda-vision-mediamtx

View File

@@ -67,6 +67,7 @@ function RepetitionBorder({
onScheduleRepetition, onScheduleRepetition,
visibleMarkers, visibleMarkers,
getTimePosition, getTimePosition,
isScheduledInDb = false,
children children
}: { }: {
left: number left: number
@@ -91,6 +92,7 @@ function RepetitionBorder({
onScheduleRepetition?: (repId: string, experimentId: string) => void onScheduleRepetition?: (repId: string, experimentId: string) => void
visibleMarkers: Array<{ id: string; startTime: Date; assignedConductors: string[] }> visibleMarkers: Array<{ id: string; startTime: Date; assignedConductors: string[] }>
getTimePosition: (time: Date) => number getTimePosition: (time: Date) => number
isScheduledInDb?: boolean
children?: React.ReactNode children?: React.ReactNode
}) { }) {
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false)
@@ -148,6 +150,9 @@ function RepetitionBorder({
? '8px 0 0 8px' // No radius on right side (markers extend to future) ? '8px 0 0 8px' // No radius on right side (markers extend to future)
: '8px' // Full radius (default) : '8px' // Full radius (default)
// Muted styling for repetitions that have been fully scheduled in DB (gray out, but don't collapse)
const isMuted = isScheduledInDb && !isHovered && !isDragging
return ( return (
<div <div
className={`absolute transition-all duration-200 ease-in-out ${isLocked ? 'cursor-not-allowed' : 'cursor-grab'} active:cursor-grabbing`} className={`absolute transition-all duration-200 ease-in-out ${isLocked ? 'cursor-not-allowed' : 'cursor-grab'} active:cursor-grabbing`}
@@ -159,11 +164,13 @@ function RepetitionBorder({
border: '2px solid', border: '2px solid',
borderLeft: extendLeft ? 'none' : '2px solid', borderLeft: extendLeft ? 'none' : '2px solid',
borderRight: extendRight ? 'none' : '2px solid', borderRight: extendRight ? 'none' : '2px solid',
borderColor: isHovered borderColor: isMuted
? 'rgba(156, 163, 175, 0.4)'
: isHovered
? (isLocked ? 'rgba(156, 163, 175, 0.9)' : 'rgba(59, 130, 246, 0.9)') ? (isLocked ? 'rgba(156, 163, 175, 0.9)' : 'rgba(59, 130, 246, 0.9)')
: (isLocked ? 'rgba(156, 163, 175, 0.3)' : 'rgba(59, 130, 246, 0.3)'), : (isLocked ? 'rgba(156, 163, 175, 0.3)' : 'rgba(59, 130, 246, 0.3)'),
borderRadius: borderRadius, borderRadius: borderRadius,
backgroundColor: 'transparent', backgroundColor: isMuted ? 'rgba(249, 250, 251, 0.6)' : 'transparent',
opacity: isDragging ? 0.7 : 1, opacity: isDragging ? 0.7 : 1,
transition: isDragging ? 'none' : 'left 0.2s ease-out, border-color 0.2s ease-in-out', transition: isDragging ? 'none' : 'left 0.2s ease-out, border-color 0.2s ease-in-out',
pointerEvents: 'auto', pointerEvents: 'auto',
@@ -183,20 +190,30 @@ function RepetitionBorder({
style={{ style={{
left: `${textContainerLeft}px`, left: `${textContainerLeft}px`,
width: `${textContainerWidth}px`, width: `${textContainerWidth}px`,
height: `${height - 2}px` height: `${height - 2}px`,
opacity: isMuted ? 0.5 : 1
}} }}
> >
{/* Text content (non-clickable) */} {/* Text content (non-clickable) */}
<div <div
className="text-xs font-medium text-gray-700 dark:text-gray-300 flex-shrink-0" className="text-xs font-medium text-gray-700 dark:text-gray-300 flex-shrink-0"
style={{ style={{
lineHeight: '1.2', lineHeight: isScheduledInDb ? '1' : '1.2',
whiteSpace: 'pre-line' whiteSpace: 'pre-line'
}} }}
> >
{phaseName && <div>{phaseName}</div>} {isScheduledInDb ? (
{experimentNumber !== undefined && <div>Exp {experimentNumber}</div>} <>
{repetitionNumber !== undefined && <div>Rep {repetitionNumber}</div>} {experimentNumber !== undefined && <div>{`Exp ${experimentNumber}`}</div>}
{repetitionNumber !== undefined && <div>{`Rep ${repetitionNumber}`}</div>}
</>
) : (
<>
{phaseName && <div>{phaseName}</div>}
{experimentNumber !== undefined && <div>Exp {experimentNumber}</div>}
{repetitionNumber !== undefined && <div>Rep {repetitionNumber}</div>}
</>
)}
</div> </div>
{/* Go to repetition button */} {/* Go to repetition button */}
@@ -215,7 +232,7 @@ function RepetitionBorder({
<button <button
onClick={handleSchedule} onClick={handleSchedule}
disabled={!allMarkersHaveConductors} disabled={!allMarkersHaveConductors}
className="flex-shrink-0 px-2 py-1 text-xs bg-green-600 hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white rounded transition-colors" className="flex-shrink-0 px-2 py-1 text-xs bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded transition-colors"
title={allMarkersHaveConductors ? "Schedule repetition" : "All markers must have at least one conductor assigned"} title={allMarkersHaveConductors ? "Schedule repetition" : "All markers must have at least one conductor assigned"}
> >
Schedule Schedule
@@ -1214,6 +1231,7 @@ export function HorizontalTimelineCalendar({
experimentNumber={metadata?.experimentNumber} experimentNumber={metadata?.experimentNumber}
repetitionNumber={metadata?.repetitionNumber} repetitionNumber={metadata?.repetitionNumber}
experimentId={metadata?.experimentId} experimentId={metadata?.experimentId}
isScheduledInDb={metadata?.isScheduledInDb}
onScrollToRepetition={onScrollToRepetition} onScrollToRepetition={onScrollToRepetition}
onScheduleRepetition={onScheduleRepetition} onScheduleRepetition={onScheduleRepetition}
visibleMarkers={borderMarkers} visibleMarkers={borderMarkers}

View File

@@ -251,29 +251,17 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
} }
const toggleRepetition = (repId: string) => { const toggleRepetition = (repId: string) => {
// Checking/unchecking should only control visibility on the timeline.
// It must NOT clear scheduling info or conductor assignments.
setSelectedRepetitionIds(prev => { setSelectedRepetitionIds(prev => {
const next = new Set(prev) const next = new Set(prev)
if (next.has(repId)) { if (next.has(repId)) {
// Hide this repetition from the timeline
next.delete(repId) next.delete(repId)
// Remove from scheduled repetitions when unchecked // Keep scheduledRepetitions and repetitionsWithTimes intact so that
setScheduledRepetitions(prevScheduled => { // re-checking the box restores the repetition in the correct spot.
const newScheduled = { ...prevScheduled }
delete newScheduled[repId]
return newScheduled
})
// Clear all related state when unchecked
setRepetitionsWithTimes(prev => {
const next = new Set(prev)
next.delete(repId)
return next
})
setSchedulingRepetitions(prev => {
const next = new Set(prev)
next.delete(repId)
return next
})
// Don't re-stagger remaining repetitions - they should keep their positions
} else { } else {
// Show this repetition on the timeline
next.add(repId) next.add(repId)
// Auto-spawn when checked - pass the updated set to ensure correct stagger calculation // Auto-spawn when checked - pass the updated set to ensure correct stagger calculation
// spawnSingleRepetition will position the new repetition relative to existing ones // spawnSingleRepetition will position the new repetition relative to existing ones
@@ -293,20 +281,14 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
const allSelected = allRepetitions.every(rep => prev.has(rep.id)) const allSelected = allRepetitions.every(rep => prev.has(rep.id))
if (allSelected) { if (allSelected) {
// Deselect all repetitions in this phase // Deselect all repetitions in this phase (hide from timeline only)
const next = new Set(prev) const next = new Set(prev)
allRepetitions.forEach(rep => { allRepetitions.forEach(rep => {
next.delete(rep.id) next.delete(rep.id)
// Remove from scheduled repetitions
setScheduledRepetitions(prevScheduled => {
const newScheduled = { ...prevScheduled }
delete newScheduled[rep.id]
return newScheduled
})
}) })
return next return next
} else { } else {
// Select all repetitions in this phase // Select all repetitions in this phase (show on timeline)
const next = new Set(prev) const next = new Set(prev)
allRepetitions.forEach(rep => { allRepetitions.forEach(rep => {
next.add(rep.id) next.add(rep.id)
@@ -738,6 +720,51 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
} }
} }
// Unschedule a repetition: clear its scheduling info and unassign all conductors.
const unscheduleRepetition = async (repId: string, experimentId: string) => {
setSchedulingRepetitions(prev => new Set(prev).add(repId))
try {
// Remove all conductor assignments for this repetition
removeRepetitionAssignments(repId)
// Clear scheduled_date on the repetition in local state
setRepetitionsByExperiment(prev => ({
...prev,
[experimentId]: prev[experimentId]?.map(r =>
r.id === repId ? { ...r, scheduled_date: null } : r
) || []
}))
// Clear scheduled times for this repetition so it disappears from the timeline
setScheduledRepetitions(prev => {
const next = { ...prev }
delete next[repId]
return next
})
// This repetition no longer has active times
setRepetitionsWithTimes(prev => {
const next = new Set(prev)
next.delete(repId)
return next
})
// Also clear scheduled_date in the database for this repetition
await repetitionManagement.updateRepetition(repId, {
scheduled_date: null
})
} catch (error: any) {
setError(error?.message || 'Failed to unschedule repetition')
} finally {
setSchedulingRepetitions(prev => {
const next = new Set(prev)
next.delete(repId)
return next
})
}
}
// Restore scroll position after scheduledRepetitions changes // Restore scroll position after scheduledRepetitions changes
useEffect(() => { useEffect(() => {
if (scrollPositionRef.current) { if (scrollPositionRef.current) {
@@ -792,6 +819,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
Object.values(scheduledRepetitions).forEach(scheduled => { Object.values(scheduledRepetitions).forEach(scheduled => {
const repId = scheduled.repetitionId const repId = scheduled.repetitionId
// Only include markers for repetitions that are checked (selected)
if (!selectedRepetitionIds.has(repId)) {
return
}
const markerIdPrefix = repId const markerIdPrefix = repId
if (scheduled.soakingStart) { if (scheduled.soakingStart) {
@@ -834,14 +866,19 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
conductorAvailabilities, conductorAvailabilities,
phaseMarkers phaseMarkers
} }
}, [selectedConductorIds, conductors, conductorColorMap, colorPalette, availabilityEvents, scheduledRepetitions, conductorAssignments, calendarStartDate, calendarZoom]) }, [selectedConductorIds, conductors, conductorColorMap, colorPalette, availabilityEvents, scheduledRepetitions, conductorAssignments, calendarStartDate, calendarZoom, selectedRepetitionIds])
// Build repetition metadata mapping for timeline display // Build repetition metadata mapping for timeline display
const repetitionMetadata = useMemo(() => { const repetitionMetadata = useMemo(() => {
const metadata: Record<string, { phaseName: string; experimentNumber: number; repetitionNumber: number }> = {} const metadata: Record<string, { phaseName: string; experimentNumber: number; repetitionNumber: number; experimentId: string; isScheduledInDb: boolean }> = {}
Object.values(scheduledRepetitions).forEach(scheduled => { Object.values(scheduledRepetitions).forEach(scheduled => {
const repId = scheduled.repetitionId const repId = scheduled.repetitionId
// Only include metadata for repetitions that are checked (selected)
if (!selectedRepetitionIds.has(repId)) {
return
}
const experiment = Object.values(experimentsByPhase).flat().find(e => e.id === scheduled.experimentId) const experiment = Object.values(experimentsByPhase).flat().find(e => e.id === scheduled.experimentId)
const repetition = Object.values(repetitionsByExperiment).flat().find(r => r.id === repId) const repetition = Object.values(repetitionsByExperiment).flat().find(r => r.id === repId)
const phase = phases.find(p => const phase = phases.find(p =>
@@ -853,13 +890,15 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
phaseName: phase.name, phaseName: phase.name,
experimentNumber: experiment.experiment_number, experimentNumber: experiment.experiment_number,
repetitionNumber: repetition.repetition_number, repetitionNumber: repetition.repetition_number,
experimentId: scheduled.experimentId experimentId: scheduled.experimentId,
// Consider a repetition \"scheduled\" in DB if it has a non-null scheduled_date
isScheduledInDb: Boolean(repetition.scheduled_date)
} }
} }
}) })
return metadata return metadata
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment, phases]) }, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment, phases, selectedRepetitionIds])
// Scroll to repetition in accordion // Scroll to repetition in accordion
const handleScrollToRepetition = useCallback(async (repetitionId: string) => { const handleScrollToRepetition = useCallback(async (repetitionId: string) => {
@@ -1239,8 +1278,8 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Rep {rep.repetition_number}</span> <span className="text-sm font-medium text-gray-700 dark:text-gray-300">Rep {rep.repetition_number}</span>
</label> </label>
{/* Time points (shown only if has been dropped/moved) */} {/* Time points (shown whenever the repetition has scheduled times) */}
{hasTimes && scheduled && ( {scheduled && (
<div className="mt-2 ml-6 text-xs space-y-1"> <div className="mt-2 ml-6 text-xs space-y-1">
{(() => { {(() => {
const repId = rep.id const repId = rep.id
@@ -1293,23 +1332,35 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
) )
})()} })()}
{/* Remove Assignments button and Schedule button */} {/* Remove Assignments button and Schedule/Unschedule button */}
<div className="flex items-center gap-3 mt-3 pt-2 border-t border-gray-200 dark:border-gray-600"> <div className="flex items-center gap-3 mt-3 pt-2 border-t border-gray-200 dark:border-gray-600">
{hasAssignments && ( {hasAssignments && (
<button <button
onClick={() => removeRepetitionAssignments(rep.id)} onClick={() => removeRepetitionAssignments(rep.id)}
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-xs transition-colors" disabled={Boolean(rep.scheduled_date)}
className="px-3 py-1 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
title={rep.scheduled_date ? "Unschedule the repetition first before removing assignments" : "Remove all conductor assignments from this repetition"}
> >
Remove Assignments Remove Assignments
</button> </button>
)} )}
<button {rep.scheduled_date ? (
onClick={() => scheduleRepetition(rep.id, exp.id)} <button
disabled={isScheduling} onClick={() => unscheduleRepetition(rep.id, exp.id)}
className="px-3 py-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors" disabled={isScheduling}
> className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
{isScheduling ? 'Scheduling...' : 'Schedule'} >
</button> {isScheduling ? 'Unscheduling...' : 'Unschedule'}
</button>
) : (
<button
onClick={() => scheduleRepetition(rep.id, exp.id)}
disabled={isScheduling}
className="px-3 py-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
>
{isScheduling ? 'Scheduling...' : 'Schedule'}
</button>
)}
</div> </div>
</div> </div>
)} )}

View File

@@ -60,57 +60,69 @@ echo "4. Rebuilding and starting all services in detached mode..."
docker compose up --build -d docker compose up --build -d
echo "" echo ""
echo "5. Waiting for Supabase database to be ready..." echo "5. Waiting for Supabase database to be ready (if configured)..."
# Wait for database to be healthy
MAX_WAIT=60
WAIT_COUNT=0
while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
if docker compose ps supabase-db | grep -q "healthy"; then
echo " ✓ Supabase database is healthy"
break
fi
echo " Waiting for database... ($WAIT_COUNT/$MAX_WAIT seconds)"
sleep 2
WAIT_COUNT=$((WAIT_COUNT + 2))
done
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then # Only wait for Supabase if the supabase-db service exists in docker-compose.yml
echo " ⚠ Warning: Database may not be fully ready" if docker compose config --services 2>/dev/null | grep -q "^supabase-db$"; then
# Wait for database to be healthy
MAX_WAIT=60
WAIT_COUNT=0
while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
if docker compose ps supabase-db | grep -q "healthy"; then
echo " ✓ Supabase database is healthy"
break
fi
echo " Waiting for database... ($WAIT_COUNT/$MAX_WAIT seconds)"
sleep 2
WAIT_COUNT=$((WAIT_COUNT + 2))
done
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then
echo " ⚠ Warning: Database may not be fully ready"
fi
else
echo " - Supabase services are currently disabled in docker-compose.yml; skipping DB wait step."
fi fi
echo "" echo ""
echo "6. Waiting for Supabase migrations to complete..." echo "6. Waiting for Supabase migrations to complete (if configured)..."
# Wait for migration container to complete (it has restart: "no", so it should exit when done)
MAX_WAIT=120
WAIT_COUNT=0
MIGRATE_CONTAINER="usda-vision-supabase-migrate"
while [ $WAIT_COUNT -lt $MAX_WAIT ]; do # Only wait for the migration container if the supabase-migrate service exists
# Check if container exists and its status if docker compose config --services 2>/dev/null | grep -q "^supabase-migrate$"; then
if docker ps -a --format "{{.Names}}\t{{.Status}}" | grep -q "^${MIGRATE_CONTAINER}"; then # Wait for migration container to complete (it has restart: "no", so it should exit when done)
CONTAINER_STATUS=$(docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^${MIGRATE_CONTAINER}" | awk '{print $2}') MAX_WAIT=120
WAIT_COUNT=0
if echo "$CONTAINER_STATUS" | grep -q "Exited"; then MIGRATE_CONTAINER="usda-vision-supabase-migrate"
EXIT_CODE=$(docker inspect "$MIGRATE_CONTAINER" --format='{{.State.ExitCode}}' 2>/dev/null || echo "1")
if [ "$EXIT_CODE" = "0" ]; then while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
echo " ✓ Supabase migrations completed successfully" # Check if container exists and its status
break if docker ps -a --format "{{.Names}}\t{{.Status}}" | grep -q "^${MIGRATE_CONTAINER}"; then
else CONTAINER_STATUS=$(docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^${MIGRATE_CONTAINER}" | awk '{print $2}')
echo " ⚠ Warning: Migrations may have failed (exit code: $EXIT_CODE)"
echo " Check logs with: docker compose logs supabase-migrate" if echo "$CONTAINER_STATUS" | grep -q "Exited"; then
break EXIT_CODE=$(docker inspect "$MIGRATE_CONTAINER" --format='{{.State.ExitCode}}' 2>/dev/null || echo "1")
if [ "$EXIT_CODE" = "0" ]; then
echo " ✓ Supabase migrations completed successfully"
break
else
echo " ⚠ Warning: Migrations may have failed (exit code: $EXIT_CODE)"
echo " Check logs with: docker compose logs supabase-migrate"
break
fi
fi fi
fi fi
fi
echo " Waiting for migrations... ($WAIT_COUNT/$MAX_WAIT seconds)"
echo " Waiting for migrations... ($WAIT_COUNT/$MAX_WAIT seconds)" sleep 2
sleep 2 WAIT_COUNT=$((WAIT_COUNT + 2))
WAIT_COUNT=$((WAIT_COUNT + 2)) done
done
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then if [ $WAIT_COUNT -ge $MAX_WAIT ]; then
echo " ⚠ Warning: Migration timeout - check logs with: docker compose logs supabase-migrate" echo " ⚠ Warning: Migration timeout - check logs with: docker compose logs supabase-migrate"
echo " Note: Migrations may still be running or the container may not have started yet" echo " Note: Migrations may still be running or the container may not have started yet"
fi
else
echo " - Supabase migration service is currently disabled in docker-compose.yml; skipping migration wait step."
fi fi
echo "" echo ""