diff --git a/.gitignore b/.gitignore index 293689c..de92f72 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ .uv/ *.env .env.*.local +.host-ip .pytest_cache/ .mypy_cache/ @@ -35,6 +36,10 @@ management-dashboard-web-app/users.txt # Jupyter Notebooks *.ipynb +supabase/.temp/cli-latest + +# Archive env backups (may contain secrets) +archive/management-dashboard-web-app/env-backups/ # Nix result result-* diff --git a/archive/management-dashboard-web-app/README.md b/archive/management-dashboard-web-app/README.md new file mode 100644 index 0000000..b336d0e --- /dev/null +++ b/archive/management-dashboard-web-app/README.md @@ -0,0 +1,7 @@ +# Archive: management-dashboard-web-app legacy/backup files + +Moved from `management-dashboard-web-app/` so the app directory only contains active code and config. + +- **env-backups/** – Old `.env.backup` and timestamped backup (Supabase URL/key). Keep out of version control if they contain secrets. +- **experiment-data/** – CSV run sheets: `phase_2_JC_experimental_run_sheet.csv`, `post_workshop_meyer_experiments.csv`. Source/reference data for experiments. +- **test-api-fix.js** – One-off test script for camera config API; not part of the app build. diff --git a/management-dashboard-web-app/phase_2_JC_experimental_run_sheet.csv b/archive/management-dashboard-web-app/experiment-data/phase_2_JC_experimental_run_sheet.csv similarity index 100% rename from management-dashboard-web-app/phase_2_JC_experimental_run_sheet.csv rename to archive/management-dashboard-web-app/experiment-data/phase_2_JC_experimental_run_sheet.csv diff --git a/management-dashboard-web-app/post_workshop_meyer_experiments.csv b/archive/management-dashboard-web-app/experiment-data/post_workshop_meyer_experiments.csv similarity index 100% rename from management-dashboard-web-app/post_workshop_meyer_experiments.csv rename to archive/management-dashboard-web-app/experiment-data/post_workshop_meyer_experiments.csv diff --git a/management-dashboard-web-app/test-api-fix.js b/archive/management-dashboard-web-app/test-api-fix.js similarity index 100% rename from management-dashboard-web-app/test-api-fix.js rename to archive/management-dashboard-web-app/test-api-fix.js diff --git a/docker-compose.sh b/docker-compose.sh index 17484e0..9dbcb99 100755 --- a/docker-compose.sh +++ b/docker-compose.sh @@ -12,9 +12,16 @@ PROJECT_ROOT="$SCRIPT_DIR" # Change to project root cd "$PROJECT_ROOT" || exit 1 -# Detect host IP -HOST_IP=$("$SCRIPT_DIR/scripts/get-host-ip.sh") -if [ $? -ne 0 ] || [ -z "$HOST_IP" ] || [ "$HOST_IP" = "127.0.0.1" ]; then +# Host IP: use HOST_IP env, then .host-ip file, then auto-detect +if [ -z "$HOST_IP" ] || [ "$HOST_IP" = "127.0.0.1" ]; then + if [ -f "$PROJECT_ROOT/.host-ip" ]; then + HOST_IP=$(cat "$PROJECT_ROOT/.host-ip" | tr -d '\n\r' | awk '{print $1}') + fi +fi +if [ -z "$HOST_IP" ] || [ "$HOST_IP" = "127.0.0.1" ]; then + HOST_IP=$("$SCRIPT_DIR/scripts/get-host-ip.sh" 2>/dev/null) || true +fi +if [ -z "$HOST_IP" ] || [ "$HOST_IP" = "127.0.0.1" ]; then echo "Warning: Could not detect host IP, using localhost" >&2 HOST_IP="localhost" fi diff --git a/docker-compose.yml b/docker-compose.yml index 588eecf..9f37c32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,10 @@ networks: usda-vision-network: driver: bridge -volumes: - supabase-db: - driver: local - supabase-storage: +# volumes: +# supabase-db: +# driver: local +# supabase-storage: services: # ============================================================================ @@ -17,7 +17,7 @@ services: # - Filter by label: docker compose ps --filter "label=com.usda-vision.service=supabase" # - Or use service names: docker compose ps supabase-* # - # NOTE: Currently commented out to test Supabase CLI setup from management-dashboard-web-app + # NOTE: Supabase CLI and docker-compose use root supabase/ # # # Supabase Database # # supabase-db: @@ -400,6 +400,8 @@ services: video-remote: container_name: usda-vision-video-remote image: node:20-alpine + tty: true + stdin_open: true working_dir: /app environment: - CHOKIDAR_USEPOLLING=true @@ -424,6 +426,8 @@ services: vision-system-remote: container_name: usda-vision-vision-system-remote image: node:20-alpine + tty: true + stdin_open: true working_dir: /app environment: - CHOKIDAR_USEPOLLING=true @@ -447,6 +451,8 @@ services: scheduling-remote: container_name: usda-vision-scheduling-remote image: node:20-alpine + tty: true + stdin_open: true working_dir: /app env_file: - ./management-dashboard-web-app/.env @@ -466,6 +472,14 @@ services: - "3003:3003" networks: - usda-vision-network + develop: + watch: + - path: ./scheduling-remote + action: restart + ignore: + - node_modules/ + - dist/ + - .git/ media-api: container_name: usda-vision-media-api diff --git a/docs/DESIGN_RECOMMENDATION_SUMMARY.md b/docs/DESIGN_RECOMMENDATION_SUMMARY.md index d62445d..71e04a0 100644 --- a/docs/DESIGN_RECOMMENDATION_SUMMARY.md +++ b/docs/DESIGN_RECOMMENDATION_SUMMARY.md @@ -68,7 +68,7 @@ I've created a migration file that implements a **unified `experiment_phase_exec ## Files Created 1. **`docs/database_design_analysis.md`** - Detailed analysis with comparison matrix -2. **`management-dashboard-web-app/supabase/migrations/00012_unified_phase_executions.sql`** - Complete migration implementation +2. **`supabase/migrations/00012_unified_phase_executions.sql`** - Complete migration implementation ## Migration Path diff --git a/docs/SUPABASE_MIGRATION.md b/docs/SUPABASE_MIGRATION.md index 974ca29..cd03d68 100644 --- a/docs/SUPABASE_MIGRATION.md +++ b/docs/SUPABASE_MIGRATION.md @@ -50,23 +50,18 @@ If you have scripts or documentation that reference the old path, update them: - ❌ `management-dashboard-web-app/supabase/config.toml` - ✅ `supabase/config.toml` -## Backward Compatibility +## Current State -The old directory (`management-dashboard-web-app/supabase/`) can be kept for reference, but it's no longer used by docker-compose or the Supabase CLI. You can safely remove it after verifying everything works: - -```bash -# After verifying everything works with the new location -rm -rf management-dashboard-web-app/supabase -``` +The old directory (`management-dashboard-web-app/supabase/`) has been removed. All Supabase and DB configuration, migrations, and seeds now live only under the project root `supabase/` directory. Docker Compose and the Supabase CLI use root `supabase/` exclusively. ## Verification -To verify the migration worked: +To verify the migration: -1. **Check docker-compose paths**: +1. **Check docker-compose paths** (only root supabase should be referenced): ```bash - grep -r "supabase" docker-compose.yml - # Should show: ./supabase/ (not ./management-dashboard-web-app/supabase/) + grep "supabase" docker-compose.yml + # Should show only ./supabase/ (no management-dashboard-web-app/supabase/) ``` 2. **Test Supabase CLI**: diff --git a/docs/database_entities.md b/docs/database_entities.md new file mode 100644 index 0000000..911359c --- /dev/null +++ b/docs/database_entities.md @@ -0,0 +1,303 @@ +# Database Entities Documentation + +This document describes the core entities in the USDA Vision database schema, focusing on entity-specific attributes (excluding generic fields like `id`, `created_at`, `updated_at`, `created_by`). + +## Entity Relationships Overview + +``` +Experiment Phase (Template) + ↓ +Experiment + ↓ +Experiment Repetition + ↓ +Experiment Phase Execution (Soaking, Airdrying, Cracking, Shelling) +``` + +--- + +## 1. Experiment Phase + +**Table:** `experiment_phases` + +**Purpose:** Defines a template/blueprint for experiments that specifies which processing phases are included and their configuration. + +### Attributes + +- **name** (TEXT, UNIQUE, NOT NULL) + - Unique name identifying the experiment phase template + - Example: "Phase 2 - Standard Processing" + +- **description** (TEXT, nullable) + - Optional description providing details about the experiment phase + +- **has_soaking** (BOOLEAN, NOT NULL, DEFAULT false) + - Indicates whether soaking phase is included in experiments using this template + +- **has_airdrying** (BOOLEAN, NOT NULL, DEFAULT false) + - Indicates whether airdrying phase is included in experiments using this template + +- **has_cracking** (BOOLEAN, NOT NULL, DEFAULT false) + - Indicates whether cracking phase is included in experiments using this template + +- **has_shelling** (BOOLEAN, NOT NULL, DEFAULT false) + - Indicates whether shelling phase is included in experiments using this template + +- **cracking_machine_type_id** (UUID, nullable) + - References the machine type to be used for cracking (required if `has_cracking` is true) + - Links to `machine_types` table + +### Constraints + +- At least one phase (soaking, airdrying, cracking, or shelling) must be enabled +- If `has_cracking` is true, `cracking_machine_type_id` must be provided + +--- + +## 2. Experiment + +**Table:** `experiments` + +**Purpose:** Defines an experiment blueprint that specifies the parameters and requirements for conducting pecan processing experiments. + +### Attributes + +- **experiment_number** (INTEGER, NOT NULL) + - Unique number identifying the experiment + - Combined with `phase_id` must be unique + +- **reps_required** (INTEGER, NOT NULL, CHECK > 0) + - Number of repetitions required for this experiment + - Must be greater than zero + +- **weight_per_repetition_lbs** (DOUBLE PRECISION, NOT NULL, DEFAULT 5.0, CHECK > 0) + - Weight in pounds required for each repetition of the experiment + +- **results_status** (TEXT, NOT NULL, DEFAULT 'valid', CHECK IN ('valid', 'invalid')) + - Status indicating whether the experiment results are considered valid or invalid + +- **completion_status** (BOOLEAN, NOT NULL, DEFAULT false) + - Indicates whether the experiment has been completed + +- **phase_id** (UUID, NOT NULL) + - References the experiment phase template this experiment belongs to + - Links to `experiment_phases` table + +### Constraints + +- Combination of `experiment_number` and `phase_id` must be unique + +--- + +## 3. Experiment Repetition + +**Table:** `experiment_repetitions` + +**Purpose:** Represents a single execution instance of an experiment that can be scheduled and tracked. + +### Attributes + +- **experiment_id** (UUID, NOT NULL) + - References the parent experiment blueprint + - Links to `experiments` table + +- **repetition_number** (INTEGER, NOT NULL, CHECK > 0) + - Sequential number identifying this repetition within the experiment + - Must be unique per experiment + +- **scheduled_date** (TIMESTAMP WITH TIME ZONE, nullable) + - Date and time when the repetition is scheduled to be executed + +- **status** (TEXT, NOT NULL, DEFAULT 'pending', CHECK IN ('pending', 'in_progress', 'completed', 'cancelled')) + - Current status of the repetition execution + - Values: `pending`, `in_progress`, `completed`, `cancelled` + +### Constraints + +- Combination of `experiment_id` and `repetition_number` must be unique + +--- + +## 4. Experiment Phase Executions + +**Table:** `experiment_phase_executions` + +**Purpose:** Unified table storing execution data for all phase types (soaking, airdrying, cracking, shelling) associated with experiment repetitions. + +### Common Attributes (All Phase Types) + +- **repetition_id** (UUID, NOT NULL) + - References the experiment repetition this phase execution belongs to + - Links to `experiment_repetitions` table + +- **phase_type** (TEXT, NOT NULL, CHECK IN ('soaking', 'airdrying', 'cracking', 'shelling')) + - Type of phase being executed + - Must be one of: `soaking`, `airdrying`, `cracking`, `shelling` + +- **scheduled_start_time** (TIMESTAMP WITH TIME ZONE, NOT NULL) + - Planned start time for the phase execution + +- **scheduled_end_time** (TIMESTAMP WITH TIME ZONE, nullable) + - Planned end time for the phase execution + - Automatically calculated for soaking and airdrying based on duration + +- **actual_start_time** (TIMESTAMP WITH TIME ZONE, nullable) + - Actual time when the phase execution started + +- **actual_end_time** (TIMESTAMP WITH TIME ZONE, nullable) + - Actual time when the phase execution ended + +- **status** (TEXT, NOT NULL, DEFAULT 'pending', CHECK IN ('pending', 'scheduled', 'in_progress', 'completed', 'cancelled')) + - Current status of the phase execution + +### Phase-Specific Concepts: Independent & Dependent Variables + +> **Note:** This section describes the conceptual variables for each phase (what we measure or control), not necessarily the current physical columns in the database. Some of these variables will be added to the schema in future migrations. + +#### Soaking Phase + +- **Independent variables (IV)** + - **Pre-soaking shell moisture percentage** + - Moisture percentage of the shell **before soaking**. + - **Pre-soaking kernel moisture percentage** + - Moisture percentage of the kernel **before soaking**. + - **Average pecan diameter (inches)** + - Average diameter of pecans in the batch, measured in inches. + - **Batch weight** + - Total weight of the batch being soaked. + - **Soaking duration (minutes)** + - Duration of the soaking process in minutes (currently represented as `soaking_duration_minutes`). + +- **Dependent variables (DV)** + - **Post-soaking shell moisture percentage** + - Moisture percentage of the shell **after soaking**. + - **Post-soaking kernel moisture percentage** + - Moisture percentage of the kernel **after soaking**. + +#### Airdrying Phase + +- **Independent variables (IV)** + - **Airdrying duration (minutes)** + - Duration of the airdrying process in minutes (currently represented as `duration_minutes`). + +- **Dependent variables (DV)** + - *(TBD — moisture/other measurements after airdrying can be added here when finalized.)* + +#### Cracking Phase + +- **Independent variables (IV)** + - **Cracking machine type** + - The type of cracking machine used (linked via `machine_type_id` and `experiment_phases.cracking_machine_type_id`). + +- **Dependent variables (DV)** + - *None defined yet for cracking.* + Business/analysis metrics for cracking can be added later (e.g., crack quality, breakage rates). + +#### Shelling Phase + +- **Independent variables (IV)** + - **Shelling machine configuration parameters** (not yet present in the DB schema) + - **Ring gap (inches)** + - Radial gap setting of the shelling ring (e.g., `0.34` inches). + - **Paddle RPM** + - Rotational speed of the paddles (integer RPM value). + - **Third machine parameter (TBD)** + - The shelling machine expects a third configuration parameter that will be defined and added to the schema later. + +- **Dependent variables (DV)** + - **Half-yield ratio (percentage)** + - Percentage of the shelled product that is composed of half kernels, relative to total yield. + +### Constraints + +- Combination of `repetition_id` and `phase_type` must be unique (one execution per phase type per repetition) + +### Notes + +- Phase executions are automatically created when an experiment repetition is created, based on the experiment phase template configuration +- Sequential phases (soaking → airdrying → cracking → shelling) automatically calculate their `scheduled_start_time` based on the previous phase's `scheduled_end_time` +- For soaking and airdrying phases, `scheduled_end_time` is automatically calculated from `scheduled_start_time` + duration + +--- + +## 5. Machine Types + +**Table:** `machine_types` + +**Purpose:** Defines the types of machines available for cracking operations. + +### Attributes + +- **name** (TEXT, UNIQUE, NOT NULL) + - Unique name identifying the machine type + - Example: "JC Cracker", "Meyer Cracker" + +- **description** (TEXT, nullable) + - Optional description of the machine type + +### Related Tables + +Machine types are referenced by: +- `experiment_phases.cracking_machine_type_id` - Defines which machine type to use for cracking in an experiment phase template +- `experiment_phase_executions.machine_type_id` - Specifies which machine was used for a specific cracking execution + +--- + +## 6. Cracker Parameters (Machine-Specific) + +### JC Cracker Parameters + +**Table:** `jc_cracker_parameters` + +**Purpose:** Stores parameters specific to JC Cracker machines. + +#### Attributes + +- **plate_contact_frequency_hz** (DOUBLE PRECISION, NOT NULL, CHECK > 0) + - Frequency of plate contact in Hertz + +- **throughput_rate_pecans_sec** (DOUBLE PRECISION, NOT NULL, CHECK > 0) + - Rate of pecan processing in pecans per second + +- **crush_amount_in** (DOUBLE PRECISION, NOT NULL, CHECK >= 0) + - Amount of crushing in inches + +- **entry_exit_height_diff_in** (DOUBLE PRECISION, NOT NULL) + - Difference in height between entry and exit points in inches + +### Meyer Cracker Parameters + +**Table:** `meyer_cracker_parameters` + +**Purpose:** Stores parameters specific to Meyer Cracker machines. + +#### Attributes + +- **motor_speed_hz** (DOUBLE PRECISION, NOT NULL, CHECK > 0) + - Motor speed in Hertz + +- **jig_displacement_inches** (DOUBLE PRECISION, NOT NULL) + - Jig displacement in inches + +- **spring_stiffness_nm** (DOUBLE PRECISION, NOT NULL, CHECK > 0) + - Spring stiffness in Newton-meters + +--- + +## Summary of Entity Relationships + +1. **Experiment Phase** → Defines which phases are included and machine type for cracking +2. **Experiment** → Belongs to an Experiment Phase, defines repetition requirements and weight per repetition +3. **Experiment Repetition** → Instance of an Experiment, can be scheduled and tracked +4. **Experiment Phase Execution** → Execution record for each phase (soaking, airdrying, cracking, shelling) within a repetition +5. **Machine Types** → Defines available cracking machines +6. **Cracker Parameters** → Machine-specific operational parameters (JC or Meyer) + +### Key Relationships + +- One Experiment Phase can have many Experiments +- One Experiment can have many Experiment Repetitions +- One Experiment Repetition can have multiple Phase Executions (one per phase type) +- Phase Executions are automatically created based on the Experiment Phase template configuration +- Cracking Phase Executions reference a Machine Type +- Machine Types can have associated Cracker Parameters (JC or Meyer specific) diff --git a/management-dashboard-web-app/.env.backup b/management-dashboard-web-app/.env.backup deleted file mode 100755 index 50956f7..0000000 --- a/management-dashboard-web-app/.env.backup +++ /dev/null @@ -1,4 +0,0 @@ -# Local Supabase config for Vite dev server -VITE_SUPABASE_URL=http://127.0.0.1:54321 -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 - diff --git a/management-dashboard-web-app/.env.backup.20251218_195621 b/management-dashboard-web-app/.env.backup.20251218_195621 deleted file mode 100755 index 3efeb2e..0000000 --- a/management-dashboard-web-app/.env.backup.20251218_195621 +++ /dev/null @@ -1,17 +0,0 @@ -# Local Supabase config for Vite dev server -VITE_SUPABASE_URL=http://exp-dash:54321 -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 - - -# Vision API Configuration -VITE_VISION_API_URL=http://exp-dash:8000 -VITE_ENABLE_VIDEO_MODULE=true -VITE_VIDEO_REMOTE_URL=http://exp-dash:3001/assets/remoteEntry.js?v=1761849082 -VITE_MEDIA_API_URL=http://exp-dash:8090 - -# Vision System Module -VITE_ENABLE_VISION_SYSTEM_MODULE=true -VITE_VISION_SYSTEM_REMOTE_URL=http://exp-dash:3002/assets/remoteEntry.js - -# Enable scheduling module -VITE_ENABLE_SCHEDULING_MODULE=true diff --git a/management-dashboard-web-app/src/components/ExperimentForm.tsx b/management-dashboard-web-app/src/components/ExperimentForm.tsx index 9e32b60..8329411 100755 --- a/management-dashboard-web-app/src/components/ExperimentForm.tsx +++ b/management-dashboard-web-app/src/components/ExperimentForm.tsx @@ -3,7 +3,7 @@ import type { CreateExperimentRequest, UpdateExperimentRequest, ScheduleStatus, import { experimentPhaseManagement, machineTypeManagement } from '../lib/supabase' interface ExperimentFormProps { - initialData?: Partial + initialData?: Partial & { phase_id?: string | null } onSubmit: (data: CreateExperimentRequest | UpdateExperimentRequest) => Promise onCancel: () => void isEditing?: boolean @@ -12,31 +12,41 @@ interface ExperimentFormProps { } export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = false, loading = false, phaseId }: ExperimentFormProps) { - const [formData, setFormData] = useState({ - experiment_number: initialData?.experiment_number || 0, - reps_required: initialData?.reps_required || 1, - weight_per_repetition_lbs: (initialData as any)?.weight_per_repetition_lbs || 1, - soaking_duration_hr: initialData?.soaking_duration_hr || 0, - air_drying_time_min: initialData?.air_drying_time_min || 0, - plate_contact_frequency_hz: initialData?.plate_contact_frequency_hz || 1, - throughput_rate_pecans_sec: initialData?.throughput_rate_pecans_sec || 1, - crush_amount_in: initialData?.crush_amount_in || 0, - entry_exit_height_diff_in: initialData?.entry_exit_height_diff_in || 0, - // Meyer-specific (UI only) - motor_speed_hz: (initialData as any)?.motor_speed_hz || 1, - jig_displacement_inches: (initialData as any)?.jig_displacement_inches || 0, - spring_stiffness_nm: (initialData as any)?.spring_stiffness_nm || 1, - schedule_status: initialData?.schedule_status || 'pending schedule', - results_status: initialData?.results_status || 'valid', - completion_status: initialData?.completion_status || false, - phase_id: initialData?.phase_id || phaseId + const getInitialFormState = (d: any) => ({ + experiment_number: d?.experiment_number ?? 0, + reps_required: d?.reps_required ?? 1, + weight_per_repetition_lbs: d?.weight_per_repetition_lbs ?? 1, + soaking_duration_hr: d?.soaking?.soaking_duration_hr ?? d?.soaking_duration_hr ?? 0, + air_drying_time_min: d?.airdrying?.duration_minutes ?? d?.air_drying_time_min ?? 0, + plate_contact_frequency_hz: d?.cracking?.plate_contact_frequency_hz ?? d?.plate_contact_frequency_hz ?? 1, + throughput_rate_pecans_sec: d?.cracking?.throughput_rate_pecans_sec ?? d?.throughput_rate_pecans_sec ?? 1, + crush_amount_in: d?.cracking?.crush_amount_in ?? d?.crush_amount_in ?? 0, + entry_exit_height_diff_in: d?.cracking?.entry_exit_height_diff_in ?? d?.entry_exit_height_diff_in ?? 0, + motor_speed_hz: d?.cracking?.motor_speed_hz ?? d?.motor_speed_hz ?? 1, + jig_displacement_inches: d?.cracking?.jig_displacement_inches ?? d?.jig_displacement_inches ?? 0, + spring_stiffness_nm: d?.cracking?.spring_stiffness_nm ?? d?.spring_stiffness_nm ?? 1, + schedule_status: d?.schedule_status ?? 'pending schedule', + results_status: d?.results_status ?? 'valid', + completion_status: d?.completion_status ?? false, + phase_id: d?.phase_id ?? phaseId, + ring_gap_inches: d?.shelling?.ring_gap_inches ?? d?.ring_gap_inches ?? null, + drum_rpm: d?.shelling?.drum_rpm ?? d?.drum_rpm ?? null }) + const [formData, setFormData] = useState(() => getInitialFormState(initialData)) + const [errors, setErrors] = useState>({}) const [phase, setPhase] = useState(null) const [crackingMachine, setCrackingMachine] = useState(null) const [metaLoading, setMetaLoading] = useState(false) + // When initialData loads with phase config (edit mode), sync form state + useEffect(() => { + if ((initialData as any)?.id) { + setFormData(prev => ({ ...prev, ...getInitialFormState(initialData) })) + } + }, [initialData]) + useEffect(() => { const loadMeta = async () => { if (!phaseId) return @@ -76,11 +86,11 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } - if (formData.soaking_duration_hr < 0) { + if ((formData.soaking_duration_hr ?? 0) < 0) { newErrors.soaking_duration_hr = 'Soaking duration cannot be negative' } - if (formData.air_drying_time_min < 0) { + if ((formData.air_drying_time_min ?? 0) < 0) { newErrors.air_drying_time_min = 'Air drying time cannot be negative' } @@ -93,7 +103,7 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa if (!formData.throughput_rate_pecans_sec || formData.throughput_rate_pecans_sec <= 0) { newErrors.throughput_rate_pecans_sec = 'Throughput rate must be positive' } - if (formData.crush_amount_in < 0) { + if ((formData.crush_amount_in ?? 0) < 0) { newErrors.crush_amount_in = 'Crush amount cannot be negative' } } @@ -110,6 +120,16 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } } + // Shelling: if provided, must be positive + if (phase?.has_shelling) { + if (formData.ring_gap_inches != null && (typeof formData.ring_gap_inches !== 'number' || formData.ring_gap_inches <= 0)) { + newErrors.ring_gap_inches = 'Ring gap must be positive' + } + if (formData.drum_rpm != null && (typeof formData.drum_rpm !== 'number' || formData.drum_rpm <= 0)) { + newErrors.drum_rpm = 'Drum RPM must be positive' + } + } + setErrors(newErrors) return Object.keys(newErrors).length === 0 } @@ -122,14 +142,25 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } try { - // Prepare data for submission + // Prepare data: include all phase params so they are stored in experiment_soaking, experiment_airdrying, experiment_cracking, experiment_shelling const submitData = isEditing ? formData : { experiment_number: formData.experiment_number, reps_required: formData.reps_required, weight_per_repetition_lbs: formData.weight_per_repetition_lbs, results_status: formData.results_status, completion_status: formData.completion_status, - phase_id: formData.phase_id + phase_id: formData.phase_id, + soaking_duration_hr: formData.soaking_duration_hr, + air_drying_time_min: formData.air_drying_time_min, + plate_contact_frequency_hz: formData.plate_contact_frequency_hz, + throughput_rate_pecans_sec: formData.throughput_rate_pecans_sec, + crush_amount_in: formData.crush_amount_in, + entry_exit_height_diff_in: formData.entry_exit_height_diff_in, + motor_speed_hz: (formData as any).motor_speed_hz, + jig_displacement_inches: (formData as any).jig_displacement_inches, + spring_stiffness_nm: (formData as any).spring_stiffness_nm, + ring_gap_inches: formData.ring_gap_inches ?? undefined, + drum_rpm: formData.drum_rpm ?? undefined } await onSubmit(submitData) @@ -138,7 +169,7 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } } - const handleInputChange = (field: keyof typeof formData, value: string | number | boolean) => { + const handleInputChange = (field: keyof typeof formData, value: string | number | boolean | null | undefined) => { setFormData(prev => ({ ...prev, [field]: value @@ -441,18 +472,40 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa

Shelling

-
+
+ + handleInputChange('drum_rpm' as any, e.target.value === '' ? null : parseInt(e.target.value, 10))} + className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.drum_rpm ? 'border-red-300' : 'border-gray-300'}`} + placeholder="e.g. 300" + min="1" step="1" /> + {errors.drum_rpm && ( +

{errors.drum_rpm}

+ )}
diff --git a/management-dashboard-web-app/src/components/ExperimentModal.tsx b/management-dashboard-web-app/src/components/ExperimentModal.tsx index b4d9a82..34797b2 100755 --- a/management-dashboard-web-app/src/components/ExperimentModal.tsx +++ b/management-dashboard-web-app/src/components/ExperimentModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { ExperimentForm } from './ExperimentForm' import { experimentManagement } from '../lib/supabase' import type { Experiment, CreateExperimentRequest, UpdateExperimentRequest } from '../lib/supabase' @@ -13,9 +13,20 @@ interface ExperimentModalProps { export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseId }: ExperimentModalProps) { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [initialData, setInitialData] = useState(experiment ?? undefined) const isEditing = !!experiment + useEffect(() => { + if (experiment) { + experimentManagement.getExperimentWithPhaseConfig(experiment.id) + .then((data) => setInitialData(data ?? experiment)) + .catch(() => setInitialData(experiment)) + } else { + setInitialData(undefined) + } + }, [experiment?.id]) + const handleSubmit = async (data: CreateExperimentRequest | UpdateExperimentRequest) => { setError(null) setLoading(true) @@ -24,22 +35,24 @@ export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseI let savedExperiment: Experiment if (isEditing && experiment) { - // Check if experiment number is unique (excluding current experiment) + // Check if experiment number is unique within this phase (excluding current experiment) if ('experiment_number' in data && data.experiment_number !== undefined && data.experiment_number !== experiment.experiment_number) { - const isUnique = await experimentManagement.isExperimentNumberUnique(data.experiment_number, experiment.id) + const phaseIdToCheck = data.phase_id ?? experiment.phase_id ?? phaseId + const isUnique = await experimentManagement.isExperimentNumberUnique(data.experiment_number, phaseIdToCheck ?? undefined, experiment.id) if (!isUnique) { - setError('Experiment number already exists. Please choose a different number.') + setError('Experiment number already exists in this phase. Please choose a different number.') return } } savedExperiment = await experimentManagement.updateExperiment(experiment.id, data) } else { - // Check if experiment number is unique for new experiments + // Check if experiment number is unique within this phase for new experiments const createData = data as CreateExperimentRequest - const isUnique = await experimentManagement.isExperimentNumberUnique(createData.experiment_number) + const phaseIdToCheck = createData.phase_id ?? phaseId + const isUnique = await experimentManagement.isExperimentNumberUnique(createData.experiment_number, phaseIdToCheck ?? undefined) if (!isUnique) { - setError('Experiment number already exists. Please choose a different number.') + setError('Experiment number already exists in this phase. Please choose a different number.') return } @@ -115,7 +128,7 @@ export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseI {/* Form */}
-

Experiment Phases

-

Select an experiment phase to view and manage its experiments

-

Experiment phases help organize experiments into logical groups for easier navigation and management.

+

Experiment Books

+

Select an experiment book to view and manage its experiments

+

Experiment books help organize experiments into logical groups for easier navigation and management.

{canManagePhases && ( )}
@@ -162,9 +162,9 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) { -

No experiment phases found

+

No experiment books found

- Get started by creating your first experiment phase. + Get started by creating your first experiment book.

{canManagePhases && (
@@ -172,7 +172,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) { onClick={() => setShowCreateModal(true)} className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" > - ➕ Create First Phase + ➕ Create First Book
)} diff --git a/management-dashboard-web-app/src/components/PhaseExperiments.tsx b/management-dashboard-web-app/src/components/PhaseExperiments.tsx index 624d7a8..c2ae719 100644 --- a/management-dashboard-web-app/src/components/PhaseExperiments.tsx +++ b/management-dashboard-web-app/src/components/PhaseExperiments.tsx @@ -193,7 +193,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) { - Back to Phases + Back to Books @@ -203,7 +203,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) { {phase.description && (

{phase.description}

)} -

Manage experiments within this phase

+

Manage experiments within this book

{canManageExperiments && (