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