Update Docker configuration, enhance error handling, and improve logging
- Added health check to the camera management API service in docker-compose.yml for better container reliability. - Updated installation scripts in Dockerfile to check for existing dependencies before installation, improving efficiency. - Enhanced error handling in the USDAVisionSystem class to allow partial operation if some components fail to start, preventing immediate shutdown. - Improved logging throughout the application, including more detailed error messages and critical error handling in the main loop. - Refactored WebSocketManager and CameraMonitor classes to use debug logging for connection events, reducing log noise.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,3 +32,6 @@ camera-management-api/usda_vision_system.log
|
||||
camera-management-api/camera_sdk/
|
||||
camera-management-api/core
|
||||
management-dashboard-web-app/users.txt
|
||||
|
||||
# Jupyter Notebooks
|
||||
*.ipynb
|
||||
|
||||
@@ -44,12 +44,12 @@ class WebSocketManager:
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
self.logger.info(f"WebSocket connected. Total connections: {len(self.active_connections)}")
|
||||
self.logger.debug(f"WebSocket connected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
if websocket in self.active_connections:
|
||||
self.active_connections.remove(websocket)
|
||||
self.logger.info(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
|
||||
self.logger.debug(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
async def send_personal_message(self, message: dict, websocket: WebSocket):
|
||||
try:
|
||||
@@ -300,9 +300,11 @@ class APIServer:
|
||||
asyncio.set_event_loop(self._event_loop)
|
||||
|
||||
# Map our log level to uvicorn's log level
|
||||
# Use "warning" for uvicorn to reduce noise (connection open/close messages)
|
||||
# Application logs will still use the configured log level
|
||||
uvicorn_log_level_map = {
|
||||
"DEBUG": "debug",
|
||||
"INFO": "info",
|
||||
"DEBUG": "warning", # Suppress uvicorn DEBUG logs even if app is in DEBUG mode
|
||||
"INFO": "warning", # Suppress uvicorn INFO logs (connection messages)
|
||||
"WARNING": "warning",
|
||||
"ERROR": "error",
|
||||
"CRITICAL": "critical"
|
||||
|
||||
@@ -151,42 +151,42 @@ class CameraMonitor:
|
||||
|
||||
# ALWAYS check our streamer state first, before doing any camera availability tests
|
||||
streamer = self.camera_manager.camera_streamers.get(camera_name)
|
||||
self.logger.info(f"Checking streamer for {camera_name}: {streamer}")
|
||||
self.logger.debug(f"Checking streamer for {camera_name}: {streamer}")
|
||||
if streamer and streamer.is_streaming():
|
||||
self.logger.info(f"Camera {camera_name} is streaming - setting status to streaming")
|
||||
self.logger.debug(f"Camera {camera_name} is streaming - setting status to streaming")
|
||||
return "streaming", "Camera streaming (live preview)", self._get_device_info_dict(device_info)
|
||||
|
||||
# Also check if our recorder is active
|
||||
recorder = self.camera_manager.camera_recorders.get(camera_name)
|
||||
if recorder and recorder.hCamera and recorder.recording:
|
||||
self.logger.info(f"Camera {camera_name} is recording - setting status to available")
|
||||
self.logger.debug(f"Camera {camera_name} is recording - setting status to available")
|
||||
return "available", "Camera recording (in use by system)", self._get_device_info_dict(device_info)
|
||||
|
||||
# Check if camera is already opened by another process
|
||||
try:
|
||||
self.logger.info(f"Checking if camera {camera_name} is opened...")
|
||||
self.logger.debug(f"Checking if camera {camera_name} is opened...")
|
||||
is_opened = mvsdk.CameraIsOpened(device_info)
|
||||
self.logger.info(f"CameraIsOpened result for {camera_name}: {is_opened}")
|
||||
self.logger.debug(f"CameraIsOpened result for {camera_name}: {is_opened}")
|
||||
|
||||
if is_opened:
|
||||
self.logger.info(f"Camera {camera_name} is opened by another process - setting status to busy")
|
||||
self.logger.debug(f"Camera {camera_name} is opened by another process - setting status to busy")
|
||||
return "busy", "Camera opened by another process", self._get_device_info_dict(device_info)
|
||||
else:
|
||||
self.logger.info(f"Camera {camera_name} is not opened, will try initialization")
|
||||
self.logger.debug(f"Camera {camera_name} is not opened, will try initialization")
|
||||
# Camera is not opened, so we can try to initialize it
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"CameraIsOpened failed for {camera_name}: {e}")
|
||||
# If we can't determine the status, try to initialize to see what happens
|
||||
self.logger.info(f"CameraIsOpened failed for {camera_name}, will try initialization: {e}")
|
||||
self.logger.debug(f"CameraIsOpened failed for {camera_name}, will try initialization: {e}")
|
||||
|
||||
# Try to initialize camera briefly to test availability
|
||||
try:
|
||||
# Ensure SDK is initialized
|
||||
ensure_sdk_initialized()
|
||||
|
||||
self.logger.info(f"Attempting to initialize camera {camera_name} for availability test...")
|
||||
self.logger.debug(f"Attempting to initialize camera {camera_name} for availability test...")
|
||||
|
||||
# Check if camera is already in use by recorder or streamer before trying to initialize
|
||||
recorder = self.camera_manager.camera_recorders.get(camera_name) if self.camera_manager else None
|
||||
@@ -198,7 +198,7 @@ class CameraMonitor:
|
||||
# Check if recorder has camera open
|
||||
if mvsdk.CameraIsOpened(recorder.hCamera):
|
||||
camera_in_use = True
|
||||
self.logger.info(f"Camera {camera_name} is already in use by recorder (handle: {recorder.hCamera})")
|
||||
self.logger.debug(f"Camera {camera_name} is already in use by recorder (handle: {recorder.hCamera})")
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -207,13 +207,13 @@ class CameraMonitor:
|
||||
# Check if streamer has camera open
|
||||
if mvsdk.CameraIsOpened(streamer.hCamera):
|
||||
camera_in_use = True
|
||||
self.logger.info(f"Camera {camera_name} is already in use by streamer (handle: {streamer.hCamera})")
|
||||
self.logger.debug(f"Camera {camera_name} is already in use by streamer (handle: {streamer.hCamera})")
|
||||
except:
|
||||
pass
|
||||
|
||||
# If camera is already in use, mark as available (since it's working, just occupied)
|
||||
if camera_in_use:
|
||||
self.logger.info(f"Camera {camera_name} is in use by system components - marking as available")
|
||||
self.logger.debug(f"Camera {camera_name} is in use by system components - marking as available")
|
||||
return "available", "Camera is in use by system", self._get_device_info_dict(device_info)
|
||||
|
||||
# Suppress output to avoid MVCAMAPI error messages during camera testing
|
||||
@@ -221,7 +221,7 @@ class CameraMonitor:
|
||||
try:
|
||||
with suppress_camera_errors():
|
||||
hCamera = mvsdk.CameraInit(device_info, -1, -1)
|
||||
self.logger.info(f"Camera {camera_name} initialized successfully, starting test capture...")
|
||||
self.logger.debug(f"Camera {camera_name} initialized successfully, starting test capture...")
|
||||
except mvsdk.CameraException as init_e:
|
||||
error_msg = f"CameraInit failed for {camera_name}: {init_e.message} (error_code: {init_e.error_code})"
|
||||
|
||||
@@ -256,14 +256,14 @@ class CameraMonitor:
|
||||
mvsdk.CameraSetTriggerMode(hCamera, 0)
|
||||
mvsdk.CameraPlay(hCamera)
|
||||
|
||||
self.logger.info(f"Camera {camera_name} test: Attempting to capture frame with {CAMERA_TEST_CAPTURE_TIMEOUT}ms timeout...")
|
||||
self.logger.debug(f"Camera {camera_name} test: Attempting to capture frame with {CAMERA_TEST_CAPTURE_TIMEOUT}ms timeout...")
|
||||
# Try to capture with short timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, CAMERA_TEST_CAPTURE_TIMEOUT)
|
||||
mvsdk.CameraReleaseImageBuffer(hCamera, pRawData)
|
||||
|
||||
# Success - camera is available
|
||||
mvsdk.CameraUnInit(hCamera)
|
||||
self.logger.info(f"Camera {camera_name} test successful - camera is available")
|
||||
self.logger.debug(f"Camera {camera_name} test successful - camera is available")
|
||||
return "available", "Camera test successful", self._get_device_info_dict(device_info)
|
||||
|
||||
except mvsdk.CameraException as capture_e:
|
||||
|
||||
@@ -90,8 +90,20 @@ class CameraStreamer:
|
||||
"""Start streaming preview frames"""
|
||||
with self._lock:
|
||||
if self.streaming:
|
||||
self.logger.warning("Streaming already active")
|
||||
return True
|
||||
self.logger.warning("Streaming already active - checking if thread is alive")
|
||||
# Check if thread is actually running
|
||||
if self._streaming_thread and self._streaming_thread.is_alive():
|
||||
self.logger.info("Streaming thread is alive, returning success")
|
||||
return True
|
||||
else:
|
||||
# Thread died but flag wasn't reset - clean up and restart
|
||||
self.logger.warning("Streaming flag set but thread is dead - cleaning up and restarting")
|
||||
self.streaming = False
|
||||
if self.hCamera is not None:
|
||||
try:
|
||||
self._cleanup_camera()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error cleaning up stale camera handle: {e}")
|
||||
|
||||
try:
|
||||
# Initialize camera for streaming
|
||||
@@ -249,6 +261,15 @@ class CameraStreamer:
|
||||
try:
|
||||
self.logger.info(f"Initializing camera for streaming: {self.camera_config.name}")
|
||||
|
||||
# Safety check: ensure no stale camera handle exists
|
||||
if self.hCamera is not None and not self._using_shared_camera:
|
||||
self.logger.warning("Stale camera handle detected during initialization - cleaning up first")
|
||||
try:
|
||||
mvsdk.CameraUnInit(self.hCamera)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error cleaning up stale handle: {e}")
|
||||
self.hCamera = None
|
||||
|
||||
# Check if recorder is active and has camera open - if so, share it
|
||||
if self.recorder and self.recorder.hCamera and self.recorder.recording:
|
||||
self.logger.info("Recorder is active with camera open - will share recorder's camera connection")
|
||||
@@ -423,11 +444,41 @@ class CameraStreamer:
|
||||
time.sleep(STREAMING_LOOP_SLEEP) # Just wait, recorder populates queues
|
||||
continue
|
||||
|
||||
# Safety check: ensure camera handle is valid before use
|
||||
if self.hCamera is None:
|
||||
self.logger.error("Camera handle is None in streaming loop - stopping")
|
||||
break
|
||||
|
||||
# Capture frame with timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, CAMERA_GET_BUFFER_TIMEOUT)
|
||||
try:
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, CAMERA_GET_BUFFER_TIMEOUT)
|
||||
except mvsdk.CameraException as e:
|
||||
if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
|
||||
continue # Timeout is normal, continue
|
||||
else:
|
||||
self.logger.error(f"CameraGetImageBuffer failed: {e.message} (error_code: {e.error_code})")
|
||||
# If camera is invalid, break to prevent segfault
|
||||
if e.error_code in [mvsdk.CAMERA_STATUS_INVALID_HANDLE, mvsdk.CAMERA_STATUS_INVALID_PARAM]:
|
||||
self.logger.error("Invalid camera handle detected - stopping streaming loop")
|
||||
break
|
||||
time.sleep(BRIEF_PAUSE_SLEEP)
|
||||
continue
|
||||
|
||||
# Process frame
|
||||
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
|
||||
try:
|
||||
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
|
||||
except mvsdk.CameraException as e:
|
||||
self.logger.error(f"CameraImageProcess failed: {e.message} (error_code: {e.error_code})")
|
||||
# Release buffer before continuing
|
||||
try:
|
||||
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
|
||||
except:
|
||||
pass
|
||||
if e.error_code in [mvsdk.CAMERA_STATUS_INVALID_HANDLE, mvsdk.CAMERA_STATUS_INVALID_PARAM]:
|
||||
self.logger.error("Invalid camera handle in image process - stopping streaming loop")
|
||||
break
|
||||
time.sleep(BRIEF_PAUSE_SLEEP)
|
||||
continue
|
||||
|
||||
# Convert to OpenCV format
|
||||
frame = self._convert_frame_to_opencv(FrameHead)
|
||||
@@ -477,7 +528,14 @@ class CameraStreamer:
|
||||
pass
|
||||
|
||||
# Release buffer
|
||||
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
|
||||
try:
|
||||
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
|
||||
except mvsdk.CameraException as e:
|
||||
self.logger.error(f"CameraReleaseImageBuffer failed: {e.message} (error_code: {e.error_code})")
|
||||
# If handle is invalid, break to prevent further issues
|
||||
if e.error_code in [mvsdk.CAMERA_STATUS_INVALID_HANDLE, mvsdk.CAMERA_STATUS_INVALID_PARAM]:
|
||||
self.logger.error("Invalid camera handle when releasing buffer - stopping streaming loop")
|
||||
break
|
||||
|
||||
# Control frame rate
|
||||
time.sleep(1.0 / self.preview_fps)
|
||||
@@ -491,6 +549,15 @@ class CameraStreamer:
|
||||
self.logger.error(f"Fatal error in streaming loop: {e}")
|
||||
finally:
|
||||
self.logger.info("Streaming loop ended")
|
||||
# Reset streaming flag when loop ends
|
||||
with self._lock:
|
||||
self.streaming = False
|
||||
# Cleanup camera resources if not already done
|
||||
if self.hCamera is not None and not self._using_shared_camera:
|
||||
try:
|
||||
self._cleanup_camera()
|
||||
except Exception as cleanup_e:
|
||||
self.logger.error(f"Error during cleanup after loop ended: {cleanup_e}")
|
||||
|
||||
def _convert_frame_to_opencv(self, FrameHead) -> Optional[np.ndarray]:
|
||||
"""Convert camera frame to OpenCV format"""
|
||||
|
||||
@@ -208,23 +208,50 @@ class USDAVisionSystem:
|
||||
def run(self) -> None:
|
||||
"""Run the system (blocking call)"""
|
||||
if not self.start():
|
||||
self.logger.error("Failed to start system")
|
||||
return
|
||||
self.logger.error("Failed to start system - some components may not be available")
|
||||
# Don't exit immediately - allow partial operation if some components started
|
||||
# Only exit if critical components failed
|
||||
if not self.running:
|
||||
self.logger.critical("Critical components failed to start - exiting")
|
||||
return
|
||||
|
||||
try:
|
||||
self.logger.info("System running... Press Ctrl+C to stop")
|
||||
|
||||
# Main loop - just keep the system alive
|
||||
consecutive_errors = 0
|
||||
max_consecutive_errors = 10
|
||||
|
||||
while self.running:
|
||||
time.sleep(1)
|
||||
try:
|
||||
time.sleep(1)
|
||||
consecutive_errors = 0 # Reset on successful iteration
|
||||
|
||||
# Periodic maintenance tasks could go here
|
||||
# For example: cleanup old recordings, health checks, etc.
|
||||
# Periodic maintenance tasks could go here
|
||||
# For example: cleanup old recordings, health checks, etc.
|
||||
|
||||
# Health check: verify critical components are still running
|
||||
if not self.mqtt_client.is_running():
|
||||
self.logger.warning("MQTT client stopped running - attempting restart")
|
||||
try:
|
||||
self.mqtt_client.start()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to restart MQTT client: {e}")
|
||||
consecutive_errors += 1
|
||||
|
||||
except Exception as e:
|
||||
consecutive_errors += 1
|
||||
self.logger.error(f"Error in main loop (consecutive: {consecutive_errors}): {e}", exc_info=True)
|
||||
|
||||
# If too many consecutive errors, exit to prevent infinite crash loop
|
||||
if consecutive_errors >= max_consecutive_errors:
|
||||
self.logger.critical(f"Too many consecutive errors ({consecutive_errors}) - shutting down to prevent crash loop")
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.logger.info("Keyboard interrupt received")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error in main loop: {e}")
|
||||
self.logger.error(f"Unexpected error in main loop: {e}", exc_info=True)
|
||||
finally:
|
||||
self.stop()
|
||||
|
||||
@@ -270,8 +297,14 @@ def main():
|
||||
|
||||
try:
|
||||
system.run()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Interrupted by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logging.error(f"Fatal error: {e}")
|
||||
logging.critical(f"Fatal error: {e}", exc_info=True)
|
||||
# Give a moment for logs to flush
|
||||
import time
|
||||
time.sleep(1)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
158
camera_files_list.csv
Normal file
158
camera_files_list.csv
Normal file
@@ -0,0 +1,158 @@
|
||||
file_path,size_mb,modification_date
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251020_102330.mp4 ,3410.421639442444,2025-10-20 14:31:07
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251020_103514.mp4 ,419.6413583755493,2025-10-20 14:36:13
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251017_104209.mp4 ,574.8742876052856,2025-10-17 14:43:26
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251016_152522.mp4 ,712.9073238372803,2025-10-16 19:27:03
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_163710.mp4 ,350.00945472717285,2025-10-15 20:37:59
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251021_164306.mp4 ,2726.057611465454,2025-10-21 20:49:32
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251020_103703.mp4 ,334.27384853363037,2025-10-20 14:37:51
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251024_152619.mp4 ,435.3051357269287,2025-10-24 19:27:22
|
||||
/mnt/nfs_share/camera1/20251027_132411_manual_camera1_2025-10-27T17-24-11-427Z.mp4 ,877.6074295043945,2025-10-27 17:26:26
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251112_102752.mp4 ,3077.46688079834,2025-11-12 15:35:05
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251023_154822.mp4 ,577.4699993133545,2025-10-23 19:49:45
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251027_152153.mp4 ,3774.1054792404175,2025-10-27 19:30:33
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251016_151728.mp4 ,3187.0334615707397,2025-10-16 19:24:28
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251111_154656.mp4 ,655.5560483932495,2025-11-11 20:48:32
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251112_104113.mp4 ,394.5743455886841,2025-11-12 15:42:16
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_162336.mp4 ,3033.105086326599,2025-10-15 20:30:24
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251020_103306.mp4 ,383.97029209136963,2025-10-20 14:34:00
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251104_145939.mp4 ,0.4626617431640625,2025-11-04 20:00:07
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251104_150128.mp4 ,0.5523891448974609,2025-11-04 20:01:59
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_110148.mp4 ,408.48339462280273,2025-11-05 16:09:06
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251111_154959.mp4 ,400.1362237930298,2025-11-11 20:51:00
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_102113.mp4 ,30.384151458740234,2025-11-05 15:21:43
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_151913.mp4 ,450.43613052368164,2025-11-05 20:20:19
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251104_144649.mp4 ,578.5874738693237,2025-11-04 19:59:38
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251104_114823.mp4 ,42.34638595581055,2025-11-04 16:48:29
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251027_153308.mp4 ,280.2379455566406,2025-10-27 19:33:51
|
||||
/mnt/nfs_share/camera1/20251027_132245_manual_camera1_2025-10-27T17-22-45-785Z.mp4 ,112.17321968078613,2025-10-27 17:23:03
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_111620.mp4 ,51.900516510009766,2025-11-05 16:17:13
|
||||
/mnt/nfs_share/camera1/20251027_131521_manual_camera1_2025-10-27T17-15-21-242Z.mp4 ,164.29076480865479,2025-10-27 17:15:46
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251024_101215.mp4 ,2795.5555295944214,2025-10-24 14:18:53
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251022_153109.mp4 ,429.8146381378174,2025-10-22 19:32:12
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251014_164830.mp4 ,355.2420530319214,2025-10-14 20:49:21
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251107_162117.mp4 ,3144.733729362488,2025-11-07 21:28:52
|
||||
/mnt/nfs_share/camera1/camera1_manual_20251027_133233.mp4 ,697.2805919647217,2025-10-27 17:34:17
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_111148.mp4 ,77.59451198577881,2025-11-05 16:13:10
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251107_163646.mp4 ,378.31123447418213,2025-11-07 21:37:42
|
||||
/mnt/nfs_share/camera1/20251027_130319_manual_camera1_2025-10-27T17-03-19-317Z.mp4 ,1.3117961883544922,2025-10-27 17:03:31
|
||||
/mnt/nfs_share/camera1/20251027_132054_manual_camera1_2025-10-27T17-20-54-417Z.mp4 ,303.9300184249878,2025-10-27 17:21:41
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251027_102543.mp4 ,55.90567111968994,2025-10-27 14:33:54
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251107_103008.mp4 ,3.954451560974121,2025-11-07 15:30:30
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_100520.mp4 ,16.943754196166992,2025-11-05 15:05:34
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_102231.mp4 ,600.7610549926758,2025-10-15 14:23:56
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_163913.mp4 ,294.5996503829956,2025-10-15 20:39:54
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251024_102251.mp4 ,376.4865026473999,2025-10-24 14:23:45
|
||||
/mnt/nfs_share/camera1/20251027_130413_manual_camera1_2025-10-27T17-04-13-377Z.mp4 ,1.122797966003418,2025-10-27 17:04:24
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251014_165003.mp4 ,370.4667148590088,2025-10-14 20:50:55
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_101125.mp4 ,3306.591923713684,2025-10-15 14:18:51
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_102545.mp4 ,343.356632232666,2025-10-15 14:26:37
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251104_150633.mp4 ,0.4837827682495117,2025-11-04 20:07:01
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251020_154656.mp4 ,3133.8277492523193,2025-10-20 19:53:59
|
||||
/mnt/nfs_share/camera1/20251101_132719_manual_camera1_2025-11-01T17-27-20-334Z.mp4 ,3.750041961669922,2025-11-01 17:27:55
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251107_104349.mp4 ,583.4240083694458,2025-11-07 15:45:14
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_163604.mp4 ,270.972692489624,2025-10-15 20:36:41
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251112_103644.mp4 ,459.7749557495117,2025-11-12 15:37:50
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_111447.mp4 ,50.30069637298584,2025-11-05 16:15:37
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251112_103855.mp4 ,390.3577947616577,2025-11-12 15:39:52
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251024_102421.mp4 ,240.2885036468506,2025-10-24 14:24:56
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251020_155806.mp4 ,408.3445301055908,2025-10-20 19:59:02
|
||||
/mnt/nfs_share/camera1/20251101_130836_manual_camera1_2025-11-01T17-08-36-984Z.mp4 ,0.9279050827026367,2025-11-01 17:08:46
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251016_152847.mp4 ,324.3079767227173,2025-10-16 19:29:33
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251017_104603.mp4 ,368.55879402160645,2025-10-17 14:46:53
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_101211.mp4 ,59.49912643432617,2025-11-05 15:13:09
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_152248.mp4 ,300.93520164489746,2025-11-05 20:23:34
|
||||
/mnt/nfs_share/camera1/20251101_125747_manual_camera1_2025-11-01T16-57-48-461Z.mp4 ,1.2230892181396484,2025-11-01 16:57:59
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251022_152301.mp4 ,2797.915633201599,2025-10-22 19:29:47
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251022_110557.mp4 ,517.8433504104614,2025-10-22 15:07:11
|
||||
/mnt/nfs_share/camera1/20251104_113624_manual_camera1_2025-11-04T16-36-24-794Z.mp4 ,31.728784561157227,2025-11-04 16:36:29
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_152113.mp4 ,241.2749366760254,2025-11-05 20:21:48
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251030_160405.mp4 ,2.8769702911376953,2025-10-30 20:04:25
|
||||
/mnt/nfs_share/camera1/20251103_153859_manual_camera1_2025-11-03T20-39-01-038Z.mp4 ,18.1849422454834,2025-11-03 20:39:02
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251022_101034.mp4 ,159.32698154449463,2025-10-22 14:11:48
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251020_155557.mp4 ,433.5608777999878,2025-10-20 19:56:55
|
||||
/mnt/nfs_share/camera1/20251014_162515_manual_camera1_2025-10-14T20-25-15-955Z.mp4 ,20.157227516174316,2025-10-14 20:25:23
|
||||
/mnt/nfs_share/camera1/20251027_132645_manual_camera1_2025-10-27T17-26-45-348Z.mp4 ,2223.18155670166,2025-10-27 17:32:15
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251111_151859.mp4 ,2962.1732845306396,2025-11-11 20:25:56
|
||||
/mnt/nfs_share/camera1/20251105_135148_manual_camera1_2025-11-05T18-51-49-573Z.mp4 ,0.17766284942626953,2025-11-05 18:51:57
|
||||
/mnt/nfs_share/camera1/20251027_133444_manual_camera1_2025-10-27T17-34-44-393Z.mp4 ,689.7500419616699,2025-10-27 17:36:26
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_101400.mp4 ,49.01335430145264,2025-11-05 15:14:47
|
||||
/mnt/nfs_share/camera1/20251027_130721_manual_camera1_2025-10-27T17-07-21-328Z.mp4 ,66.86591243743896,2025-10-27 17:07:31
|
||||
/mnt/nfs_share/camera1/20251027_131827_manual_camera1_2025-10-27T17-18-27-835Z.mp4 ,384.2500419616699,2025-10-27 17:19:27
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251030_155327.mp4 ,62.33474349975586,2025-10-30 20:00:36
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251017_103331.mp4 ,3270.361262321472,2025-10-17 14:40:48
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251023_153451.mp4 ,3281.6518783569336,2025-10-23 19:42:23
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251022_153519.mp4 ,413.2849578857422,2025-10-22 19:36:19
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251027_153609.mp4 ,482.2403316497803,2025-10-27 19:37:22
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251022_105509.mp4 ,2859.216413497925,2025-10-22 15:01:39
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251017_104401.mp4 ,433.6085367202759,2025-10-17 14:44:59
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251105_151008.mp4 ,2994.1441497802734,2025-11-05 20:17:14
|
||||
/mnt/nfs_share/camera1/.refresh ,0.0,2025-10-14 20:23:29
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251107_104542.mp4 ,340.4220886230469,2025-11-07 15:46:31
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251024_102040.mp4 ,474.6723299026489,2025-10-24 14:21:49
|
||||
/mnt/nfs_share/camera1/camera1_recording_20251107_163324.mp4 ,571.8697576522827,2025-11-07 21:34:48
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251022_110310.mp4 ,448.1354732513428,2025-10-22 15:04:15
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251024_151442.mp4 ,2802.3175411224365,2025-10-24 19:21:13
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251024_152351.mp4 ,421.7528409957886,2025-10-24 19:24:52
|
||||
/mnt/nfs_share/camera1/camera1_auto_blower_separator_20251015_163437.mp4 ,418.89909172058105,2025-10-15 20:35:34
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251030_100639.mp4 ,2192.158878326416,2025-10-30 14:11:11
|
||||
/mnt/nfs_share/camera2/20251101_140829_manual_camera2_2025-11-01T18-08-30-234Z.mp4 ,5.911883354187012,2025-11-01 18:08:31
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251024_093533.mp4 ,16607.57194709778,2025-10-24 14:11:19
|
||||
/mnt/nfs_share/camera2/20251105_135146_manual_camera2_2025-11-05T18-51-47-949Z.mp4 ,11.408576965332031,2025-11-05 18:51:55
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251023_144203.mp4 ,24270.21284866333,2025-10-23 19:33:50
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251020_094624.mp4 ,16144.342178344727,2025-10-20 14:21:48
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251023_094345.mp4 ,1862.6238870620728,2025-10-23 13:47:43
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251104_140219.mp4 ,4510.236128807068,2025-11-04 19:45:55
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251016_114800.mp4 ,1499.363600730896,2025-10-16 15:51:11
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251030_120601.mp4 ,1993.507830619812,2025-10-30 16:10:04
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251016_111402.mp4 ,1660.3650093078613,2025-10-16 15:17:31
|
||||
/mnt/nfs_share/camera2/20251101_145535_manual_camera2_2025-11-01T18-55-35-913Z.mp4 ,855.8802976608276,2025-11-01 18:57:55
|
||||
/mnt/nfs_share/camera2/20251101_140325_manual_camera2_2025-11-01T18-03-26-620Z.mp4 ,121.3832311630249,2025-11-01 18:03:44
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251028_102837.mp4 ,2286.2115926742554,2025-10-28 14:33:26
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251105_141738.mp4 ,36978.29885959625,2025-11-05 20:06:14
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251112_094651.mp4 ,28059.49242401123,2025-11-12 15:23:58
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251027_092101.mp4 ,15.555830955505371,2025-10-27 13:21:03
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251107_154318.mp4 ,28339.455046653748,2025-11-07 21:20:41
|
||||
/mnt/nfs_share/camera2/20251103_154326_manual_camera2_2025-11-03T20-43-28-822Z.mp4 ,747.4546556472778,2025-11-03 20:44:26
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251027_144707.mp4 ,16009.781435012817,2025-10-27 19:21:35
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251023_123047.mp4 ,1191.4632024765015,2025-10-23 16:33:05
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251030_151411.mp4 ,63.706143379211426,2025-10-30 19:14:18
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251028_110523.mp4 ,2216.680072784424,2025-10-28 15:09:54
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251022_101004.mp4 ,20563.820642471313,2025-10-22 14:54:11
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251106_115740.mp4 ,3574.437940597534,2025-11-06 17:02:20
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251027_093426.mp4 ,23129.598598480225,2025-10-27 14:23:38
|
||||
/mnt/nfs_share/camera2/20251101_140638_manual_camera2_2025-11-01T18-06-39-502Z.mp4 ,21.121756553649902,2025-11-01 18:06:53
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251016_121411.mp4 ,1398.3942489624023,2025-10-16 16:17:07
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251015_100527.mp4 ,4.00632381439209,2025-10-15 14:05:28
|
||||
/mnt/nfs_share/camera2/20251014_162513_manual_camera2_2025-10-14T20-25-13-249Z.mp4 ,0.4652433395385742,2025-10-14 20:25:22
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251016_094811.mp4 ,1435.4193124771118,2025-10-16 13:51:18
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251020_145623.mp4 ,22870.767561912537,2025-10-20 19:46:13
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251023_121409.mp4 ,1732.018045425415,2025-10-23 16:17:49
|
||||
/mnt/nfs_share/camera2/20251101_140355_manual_camera2_2025-11-01T18-03-56-281Z.mp4 ,79.599778175354,2025-11-01 18:05:09
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251017_100138.mp4 ,14737.022854804993,2025-10-17 14:32:39
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251030_104743.mp4 ,1549.366548538208,2025-10-30 14:50:53
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251106_103744.mp4 ,3663.7876415252686,2025-11-06 15:42:34
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251023_114107.mp4 ,1514.376932144165,2025-10-23 15:44:22
|
||||
/mnt/nfs_share/camera2/20251103_153851_manual_camera2_2025-11-03T20-38-53-740Z.mp4 ,35.77230358123779,2025-11-03 20:38:57
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251111_121518.mp4 ,2546.3795528411865,2025-11-11 17:18:37
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251112_154951.mp4 ,22451.03125,2025-11-12 21:20:34
|
||||
/mnt/nfs_share/camera2/20251101_140759_manual_camera2_2025-11-01T18-08-00-244Z.mp4 ,51.26141548156738,2025-11-01 18:08:07
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251016_102408.mp4 ,2265.5979137420654,2025-10-16 14:28:46
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251022_145322.mp4 ,13834.725661277771,2025-10-22 19:22:39
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251028_094749.mp4 ,1246.4291706085205,2025-10-28 13:50:18
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251021_160040.mp4 ,19044.06910610199,2025-10-21 20:41:59
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251106_091003.mp4 ,3608.199562072754,2025-11-06 14:14:45
|
||||
/mnt/nfs_share/camera2/20251103_164200_manual_camera2_2025-11-03T21-42-02-665Z.mp4 ,54.91640567779541,2025-11-03 21:42:09
|
||||
/mnt/nfs_share/camera2/20251101_134953_manual_camera2_2025-11-01T17-49-54-470Z.mp4 ,43.00830936431885,2025-11-01 17:50:03
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251111_100357.mp4 ,2877.733688354492,2025-11-11 15:07:36
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251106_112741.mp4 ,3612.6160192489624,2025-11-06 16:32:23
|
||||
/mnt/nfs_share/camera2/20251111_113352_camera2_auto_vibratory_conveyor_20251111_113352.mp4 ,2729.5866270065308,2025-11-11 16:37:30
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251111_104116.mp4 ,2905.7792949676514,2025-11-11 15:45:04
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251104_115434.mp4 ,27.297094345092773,2025-11-04 16:54:45
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251028_103831.mp4 ,823.6575527191162,2025-10-28 14:40:10
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251105_100139.mp4 ,6104.120680809021,2025-11-05 16:01:03
|
||||
/mnt/nfs_share/camera2/camera2_auto_vibratory_conveyor_20251015_093140.mp4 ,15725.424202919006,2025-10-15 14:05:12
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251106_095113.mp4 ,4051.650778770447,2025-11-06 14:56:34
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251107_094355.mp4 ,34698.75883579254,2025-11-07 15:29:40
|
||||
/mnt/nfs_share/camera2/camera2_recording_20251111_094856.mp4 ,2683.412371635437,2025-11-11 14:52:28
|
||||
/mnt/nfs_share/camera2/20251104_114330_manual_camera2_2025-11-04T16-43-30-646Z.mp4 ,42.72879123687744,2025-11-04 16:43:37
|
||||
|
@@ -6,6 +6,12 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
working_dir: /app
|
||||
restart: unless-stopped # Automatically restart container if it fails or exits
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python3 -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\").read()' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- ./camera-management-api:/app
|
||||
- /mnt/nfs_share:/mnt/nfs_share
|
||||
@@ -20,29 +26,42 @@ services:
|
||||
- MEDIAMTX_RTSP_PORT=8554
|
||||
command: >
|
||||
sh -lc "
|
||||
apt-get update && apt-get install -y libusb-1.0-0-dev ffmpeg;
|
||||
set -e # Exit on error
|
||||
|
||||
# Only install system packages if not already installed (check for ffmpeg)
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo 'Installing system dependencies...';
|
||||
apt-get update && apt-get install -y --no-install-recommends libusb-1.0-0-dev ffmpeg;
|
||||
else
|
||||
echo 'System dependencies already installed';
|
||||
fi
|
||||
|
||||
# Install camera SDK if not already installed
|
||||
if [ ! -f /lib/libMVSDK.so ] && [ -f 'camera_sdk/linuxSDK_V2.1.0.49(250108)/install.sh' ]; then
|
||||
echo 'Installing camera SDK...';
|
||||
cd 'camera_sdk/linuxSDK_V2.1.0.49(250108)';
|
||||
chmod +x install.sh;
|
||||
./install.sh;
|
||||
./install.sh || echo 'Warning: Camera SDK installation may have failed';
|
||||
cd /app;
|
||||
echo 'Camera SDK installed successfully';
|
||||
else
|
||||
echo 'Camera SDK already installed or install script not found';
|
||||
fi;
|
||||
|
||||
# Install Python dependencies
|
||||
# Install Python dependencies (only if requirements.txt changed or packages missing)
|
||||
if [ -f requirements.txt ]; then
|
||||
pip install --no-cache-dir -r requirements.txt;
|
||||
pip install --no-cache-dir -r requirements.txt || echo 'Warning: Some Python packages may have failed to install';
|
||||
else
|
||||
pip install --no-cache-dir -e .;
|
||||
pip install --no-cache-dir -e . || echo 'Warning: Package installation may have failed';
|
||||
fi;
|
||||
|
||||
# Start the application
|
||||
python main.py --config config.compose.json
|
||||
# Start the application with error handling
|
||||
echo 'Starting USDA Vision Camera System...';
|
||||
python main.py --config config.compose.json || {
|
||||
echo 'Application exited with error code: $?';
|
||||
echo 'Waiting 5 seconds before exit...';
|
||||
sleep 5;
|
||||
exit 1;
|
||||
}
|
||||
"
|
||||
network_mode: host
|
||||
|
||||
|
||||
141
docs/CONTAINER_CRASH_DEBUGGING.md
Normal file
141
docs/CONTAINER_CRASH_DEBUGGING.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Container Crash Debugging Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide helps diagnose and fix crashes in the `usda-vision-api` container.
|
||||
|
||||
## Quick Diagnostic
|
||||
|
||||
Run the diagnostic script:
|
||||
```bash
|
||||
./scripts/diagnose_container_crashes.sh
|
||||
```
|
||||
|
||||
## Common Causes of Crashes
|
||||
|
||||
### 1. MQTT Connection Failure
|
||||
**Symptoms:** Container exits immediately after startup
|
||||
**Check:**
|
||||
```bash
|
||||
docker logs usda-vision-api | grep -i mqtt
|
||||
```
|
||||
**Fix:** Ensure MQTT broker is accessible at `192.168.1.110:1883`
|
||||
|
||||
### 2. Camera SDK Initialization Failure
|
||||
**Symptoms:** Container crashes during camera initialization
|
||||
**Check:**
|
||||
```bash
|
||||
docker logs usda-vision-api | grep -i "camera\|sdk"
|
||||
```
|
||||
**Fix:** Check camera hardware connection and SDK installation
|
||||
|
||||
### 3. Storage Path Issues
|
||||
**Symptoms:** Container fails to start storage manager
|
||||
**Check:**
|
||||
```bash
|
||||
docker exec usda-vision-api ls -la /mnt/nfs_share
|
||||
```
|
||||
**Fix:** Ensure `/mnt/nfs_share` is mounted and writable
|
||||
|
||||
### 4. Out of Memory (OOM)
|
||||
**Symptoms:** Container killed by system, exit code 137
|
||||
**Check:**
|
||||
```bash
|
||||
dmesg | grep -i "killed process"
|
||||
docker stats usda-vision-api
|
||||
```
|
||||
**Fix:** Add memory limits or increase available memory
|
||||
|
||||
### 5. Missing Configuration File
|
||||
**Symptoms:** Container starts but exits quickly
|
||||
**Check:**
|
||||
```bash
|
||||
docker exec usda-vision-api cat /app/config.compose.json
|
||||
```
|
||||
**Fix:** Ensure `config.compose.json` exists in the container
|
||||
|
||||
### 6. Python Exception Not Caught
|
||||
**Symptoms:** Container exits with Python traceback
|
||||
**Check:**
|
||||
```bash
|
||||
docker logs usda-vision-api --tail 100
|
||||
```
|
||||
**Fix:** Check logs for unhandled exceptions
|
||||
|
||||
## Recent Improvements
|
||||
|
||||
### Enhanced Error Handling
|
||||
- System now continues running even if some non-critical components fail
|
||||
- Added consecutive error tracking to prevent infinite crash loops
|
||||
- Better logging with full exception traces
|
||||
|
||||
### Improved Startup Command
|
||||
- Only installs packages if missing (faster startup)
|
||||
- Better error messages during startup
|
||||
- Graceful exit with delay for log flushing
|
||||
|
||||
### Health Checks
|
||||
- Added Docker healthcheck to monitor container health
|
||||
- Health endpoint: `http://localhost:8000/health`
|
||||
|
||||
## Debugging Steps
|
||||
|
||||
1. **Check container status:**
|
||||
```bash
|
||||
docker ps -a | grep usda-vision-api
|
||||
```
|
||||
|
||||
2. **View recent logs:**
|
||||
```bash
|
||||
docker logs usda-vision-api --tail 100 -f
|
||||
```
|
||||
|
||||
3. **Check exit code:**
|
||||
```bash
|
||||
docker inspect usda-vision-api --format='{{.State.ExitCode}}'
|
||||
```
|
||||
- `0` = Normal exit
|
||||
- `1` = Application error
|
||||
- `137` = Killed (usually OOM)
|
||||
|
||||
4. **Check restart count:**
|
||||
```bash
|
||||
docker inspect usda-vision-api --format='{{.RestartCount}}'
|
||||
```
|
||||
|
||||
5. **Run with debug logging:**
|
||||
Edit `docker-compose.yml` and change the command to:
|
||||
```yaml
|
||||
python main.py --config config.compose.json --debug --verbose
|
||||
```
|
||||
|
||||
6. **Check resource usage:**
|
||||
```bash
|
||||
docker stats usda-vision-api
|
||||
```
|
||||
|
||||
## Manual Testing
|
||||
|
||||
To test the container manually:
|
||||
```bash
|
||||
docker exec -it usda-vision-api bash
|
||||
python main.py --config config.compose.json --debug
|
||||
```
|
||||
|
||||
## Prevention
|
||||
|
||||
The container now has:
|
||||
- ✅ Automatic restart policy (`restart: unless-stopped`)
|
||||
- ✅ Health checks
|
||||
- ✅ Better error handling
|
||||
- ✅ Graceful shutdown on signals
|
||||
- ✅ Partial operation if some components fail
|
||||
|
||||
## Getting Help
|
||||
|
||||
If crashes persist:
|
||||
1. Run the diagnostic script
|
||||
2. Collect logs: `docker logs usda-vision-api > crash_logs.txt`
|
||||
3. Check system resources: `docker stats usda-vision-api`
|
||||
4. Review recent changes to configuration or code
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
phase_name,machine_type,run_id,experiment_number,soaking_duration_hr,air_drying_duration_min,plate_contact_frequency_hz,throughput_rate_pecans_sec,crush_amount_in,entry_exit_height_diff_in,reps,rep
|
||||
"Phase 2 of JC Experiments","JC Cracker",1,0,34,19,53,28,0.05,-0.09,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",2,1,24,27,34,29,0.03,0.01,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",3,12,28,59,37,23,0.06,-0.08,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",4,15,16,60,30,24,0.07,0.02,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",5,4,13,41,41,38,0.05,0.03,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",6,18,18,49,38,35,0.07,-0.08,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",7,11,24,59,42,25,0.07,-0.05,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",8,16,20,59,41,14,0.07,0.04,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",9,4,13,41,41,38,0.05,0.03,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",10,19,11,25,56,34,0.06,-0.09,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",11,15,16,60,30,24,0.07,0.02,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",12,16,20,59,41,14,0.07,0.04,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",13,10,26,60,44,12,0.08,-0.1,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",14,1,24,27,34,29,0.03,0.01,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",15,17,34,60,34,29,0.07,-0.09,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",16,5,30,33,30,36,0.05,-0.04,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",17,2,38,10,60,28,0.06,-0.1,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",18,2,38,10,60,28,0.06,-0.1,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",19,13,21,59,41,21,0.06,-0.09,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",20,1,24,27,34,29,0.03,0.01,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",21,14,22,59,45,17,0.07,-0.08,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",22,6,10,22,37,30,0.06,0.02,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",23,11,24,59,42,25,0.07,-0.05,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",24,19,11,25,56,34,0.06,-0.09,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",25,8,27,12,55,24,0.04,0.04,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",26,18,18,49,38,35,0.07,-0.08,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",27,5,30,33,30,36,0.05,-0.04,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",28,9,32,26,47,26,0.07,0.03,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",29,3,11,36,42,13,0.07,-0.07,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",30,10,26,60,44,12,0.08,-0.1,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",31,8,27,12,55,24,0.04,0.04,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",32,5,30,33,30,36,0.05,-0.04,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",33,8,27,12,55,24,0.04,0.04,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",34,18,18,49,38,35,0.07,-0.08,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",35,3,11,36,42,13,0.07,-0.07,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",36,10,26,60,44,12,0.08,-0.1,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",37,17,34,60,34,29,0.07,-0.09,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",38,13,21,59,41,21,0.06,-0.09,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",39,12,28,59,37,23,0.06,-0.08,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",40,9,32,26,47,26,0.07,0.03,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",41,14,22,59,45,17,0.07,-0.08,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",42,0,34,19,53,28,0.05,-0.09,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",43,7,15,30,35,32,0.05,-0.07,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",44,0,34,19,53,28,0.05,-0.09,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",45,15,16,60,30,24,0.07,0.02,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",46,13,21,59,41,21,0.06,-0.09,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",47,11,24,59,42,25,0.07,-0.05,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",48,7,15,30,35,32,0.05,-0.07,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",49,16,20,59,41,14,0.07,0.04,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",50,3,11,36,42,13,0.07,-0.07,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",51,7,15,30,35,32,0.05,-0.07,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",52,6,10,22,37,30,0.06,0.02,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",53,19,11,25,56,34,0.06,-0.09,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",54,6,10,22,37,30,0.06,0.02,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",55,2,38,10,60,28,0.06,-0.1,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",56,14,22,59,45,17,0.07,-0.08,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",57,4,13,41,41,38,0.05,0.03,3,3
|
||||
"Phase 2 of JC Experiments","JC Cracker",58,9,32,26,47,26,0.07,0.03,3,2
|
||||
"Phase 2 of JC Experiments","JC Cracker",59,17,34,60,34,29,0.07,-0.09,3,1
|
||||
"Phase 2 of JC Experiments","JC Cracker",60,12,28,59,37,23,0.06,-0.08,3,3
|
||||
experiment_number,soaking_duration_hr,air_drying_duration_min,plate_contact_frequency_hz,throughput_rate_pecans_sec,crush_amount_in,entry_exit_height_diff_in
|
||||
0,34,19,53,28,0.05,-0.09
|
||||
1,24,27,34,29,0.03,0.01
|
||||
12,28,59,37,23,0.06,-0.08
|
||||
15,16,60,30,24,0.07,0.02
|
||||
4,13,41,41,38,0.05,0.03
|
||||
18,18,49,38,35,0.07,-0.08
|
||||
11,24,59,42,25,0.07,-0.05
|
||||
16,20,59,41,14,0.07,0.04
|
||||
4,13,41,41,38,0.05,0.03
|
||||
19,11,25,56,34,0.06,-0.09
|
||||
15,16,60,30,24,0.07,0.02
|
||||
16,20,59,41,14,0.07,0.04
|
||||
10,26,60,44,12,0.08,-0.1
|
||||
1,24,27,34,29,0.03,0.01
|
||||
17,34,60,34,29,0.07,-0.09
|
||||
5,30,33,30,36,0.05,-0.04
|
||||
2,38,10,60,28,0.06,-0.1
|
||||
2,38,10,60,28,0.06,-0.1
|
||||
13,21,59,41,21,0.06,-0.09
|
||||
1,24,27,34,29,0.03,0.01
|
||||
14,22,59,45,17,0.07,-0.08
|
||||
6,10,22,37,30,0.06,0.02
|
||||
11,24,59,42,25,0.07,-0.05
|
||||
19,11,25,56,34,0.06,-0.09
|
||||
8,27,12,55,24,0.04,0.04
|
||||
18,18,49,38,35,0.07,-0.08
|
||||
5,30,33,30,36,0.05,-0.04
|
||||
9,32,26,47,26,0.07,0.03
|
||||
3,11,36,42,13,0.07,-0.07
|
||||
10,26,60,44,12,0.08,-0.1
|
||||
8,27,12,55,24,0.04,0.04
|
||||
5,30,33,30,36,0.05,-0.04
|
||||
8,27,12,55,24,0.04,0.04
|
||||
18,18,49,38,35,0.07,-0.08
|
||||
3,11,36,42,13,0.07,-0.07
|
||||
10,26,60,44,12,0.08,-0.1
|
||||
17,34,60,34,29,0.07,-0.09
|
||||
13,21,59,41,21,0.06,-0.09
|
||||
12,28,59,37,23,0.06,-0.08
|
||||
9,32,26,47,26,0.07,0.03
|
||||
14,22,59,45,17,0.07,-0.08
|
||||
0,34,19,53,28,0.05,-0.09
|
||||
7,15,30,35,32,0.05,-0.07
|
||||
0,34,19,53,28,0.05,-0.09
|
||||
15,16,60,30,24,0.07,0.02
|
||||
13,21,59,41,21,0.06,-0.09
|
||||
11,24,59,42,25,0.07,-0.05
|
||||
7,15,30,35,32,0.05,-0.07
|
||||
16,20,59,41,14,0.07,0.04
|
||||
3,11,36,42,13,0.07,-0.07
|
||||
7,15,30,35,32,0.05,-0.07
|
||||
6,10,22,37,30,0.06,0.02
|
||||
19,11,25,56,34,0.06,-0.09
|
||||
6,10,22,37,30,0.06,0.02
|
||||
2,38,10,60,28,0.06,-0.1
|
||||
14,22,59,45,17,0.07,-0.08
|
||||
4,13,41,41,38,0.05,0.03
|
||||
9,32,26,47,26,0.07,0.03
|
||||
17,34,60,34,29,0.07,-0.09
|
||||
12,28,59,37,23,0.06,-0.08
|
||||
|
@@ -14,6 +14,8 @@ export function CameraPage({ cameraName }: CameraPageProps) {
|
||||
const [mqttEvents, setMqttEvents] = useState<MqttEvent[]>([])
|
||||
const [autoRecordingError, setAutoRecordingError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [retrying, setRetrying] = useState(false)
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
const statusIntervalRef = useRef<number | null>(null)
|
||||
const mqttEventsIntervalRef = useRef<number | null>(null)
|
||||
@@ -121,19 +123,39 @@ export function CameraPage({ cameraName }: CameraPageProps) {
|
||||
const loadCameraData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await Promise.all([loadCameraStatus(), loadCameraConfig()])
|
||||
} catch (error) {
|
||||
console.error('Error loading camera data:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to load camera data'
|
||||
setError(errorMessage)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = async () => {
|
||||
setRetrying(true)
|
||||
setError(null)
|
||||
try {
|
||||
await loadCameraData()
|
||||
// Also retry streaming if it was previously active
|
||||
if (streamStatus === 'error') {
|
||||
await startStreaming()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error retrying:', error)
|
||||
} finally {
|
||||
setRetrying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadCameraStatus = async () => {
|
||||
try {
|
||||
const status = await visionApi.getCameraStatus(cameraName)
|
||||
setCameraStatus(status)
|
||||
setIsRecording(status.is_recording)
|
||||
setError(null) // Clear error on successful load
|
||||
|
||||
// Update stream status based on camera status
|
||||
if (status.status === 'streaming' || status.status === 'available') {
|
||||
@@ -144,6 +166,11 @@ export function CameraPage({ cameraName }: CameraPageProps) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading camera status:', error)
|
||||
// Only set error if we don't have status data at all
|
||||
if (!cameraStatus) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to load camera status'
|
||||
setError(errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +178,14 @@ export function CameraPage({ cameraName }: CameraPageProps) {
|
||||
try {
|
||||
const config = await visionApi.getCameraConfig(cameraName)
|
||||
setCameraConfig(config)
|
||||
setError(null) // Clear error on successful load
|
||||
} catch (error) {
|
||||
console.error('Error loading camera config:', error)
|
||||
// Only set error if we don't have config data at all
|
||||
if (!cameraConfig) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to load camera configuration'
|
||||
setError(errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +310,7 @@ export function CameraPage({ cameraName }: CameraPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (loading && !cameraStatus && !cameraConfig) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-gray-900">
|
||||
<div className="text-center text-white">
|
||||
@@ -288,6 +321,51 @@ export function CameraPage({ cameraName }: CameraPageProps) {
|
||||
)
|
||||
}
|
||||
|
||||
if (error && !cameraStatus && !cameraConfig) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-gray-900">
|
||||
<div className="max-w-md w-full mx-4">
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-6">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3 flex-1">
|
||||
<h3 className="text-sm font-medium text-red-800">Error loading camera</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
disabled={retrying}
|
||||
className="bg-red-100 px-4 py-2 rounded-md text-sm font-medium text-red-800 hover:bg-red-200 disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 flex items-center space-x-2"
|
||||
>
|
||||
{retrying ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-800"></div>
|
||||
<span>Retrying...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Reload Module</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const healthStatus = getHealthStatus()
|
||||
|
||||
return (
|
||||
|
||||
@@ -217,7 +217,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
return <DataEntry />
|
||||
case 'vision-system':
|
||||
return (
|
||||
<ErrorBoundary fallback={<div className="p-6">Failed to load vision system module. Please try again.</div>}>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<div className="p-6">Loading vision system module...</div>}>
|
||||
<RemoteVisionSystem />
|
||||
</Suspense>
|
||||
@@ -225,7 +225,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
)
|
||||
case 'scheduling':
|
||||
return (
|
||||
<ErrorBoundary fallback={<div className="p-6">Failed to load scheduling module. Please try again.</div>}>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<div className="p-6">Loading scheduling module...</div>}>
|
||||
<RemoteScheduling user={user} currentRoute={currentRoute} />
|
||||
</Suspense>
|
||||
@@ -233,7 +233,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
)
|
||||
case 'video-library':
|
||||
return (
|
||||
<ErrorBoundary fallback={<div className="p-6">Failed to load video module. Please try again.</div>}>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<div className="p-6">Loading video module...</div>}>
|
||||
<RemoteVideoLibrary />
|
||||
</Suspense>
|
||||
@@ -312,7 +312,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`flex-1 transition-all duration-300 ease-in-out bg-gray-50 dark:bg-gray-900 ${isExpanded || isHovered ? "lg:ml-[290px]" : "lg:ml-[90px]"
|
||||
className={`flex-1 transition-all duration-300 ease-in-out bg-gray-50 dark:bg-gray-900 flex flex-col min-h-0 ${isExpanded || isHovered ? "lg:ml-[290px]" : "lg:ml-[90px]"
|
||||
} ${isMobileOpen ? "ml-0" : ""}`}
|
||||
>
|
||||
<TopNavbar
|
||||
@@ -323,7 +323,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
isSidebarOpen={isMobileOpen}
|
||||
onNavigateToProfile={() => handleViewChange('profile')}
|
||||
/>
|
||||
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">
|
||||
<div className="flex-1 min-h-0 overflow-hidden p-4 md:p-6">
|
||||
{renderCurrentView()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,40 @@ export function DataEntry() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
|
||||
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3 flex-1">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-400">Error loading data</h3>
|
||||
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
className="bg-red-100 dark:bg-red-900/30 px-4 py-2 rounded-md text-sm font-medium text-red-800 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/50 disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 flex items-center space-x-2"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-800 dark:border-red-300"></div>
|
||||
<span>Retrying...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Reload Module</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Component, ReactNode } from 'react'
|
||||
|
||||
type Props = { children: ReactNode, fallback?: ReactNode }
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
fallback?: ReactNode
|
||||
onRetry?: () => void
|
||||
showRetry?: boolean
|
||||
}
|
||||
type State = { hasError: boolean }
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
@@ -12,9 +17,48 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
|
||||
componentDidCatch() {}
|
||||
|
||||
handleRetry = () => {
|
||||
this.setState({ hasError: false })
|
||||
if (this.props.onRetry) {
|
||||
this.props.onRetry()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback ?? <div className="p-6">Something went wrong loading this section.</div>
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3 flex-1">
|
||||
<h3 className="text-sm font-medium text-red-800">Something went wrong loading this section</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<p>An error occurred while loading this component. Please try reloading it.</p>
|
||||
</div>
|
||||
{(this.props.showRetry !== false) && (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={this.handleRetry}
|
||||
className="bg-red-100 px-4 py-2 rounded-md text-sm font-medium text-red-800 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||||
>
|
||||
Reload Module
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
|
||||
@@ -936,7 +936,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
// Create/update soaking record with repetition_id
|
||||
await phaseManagement.createSoaking({
|
||||
experiment_id: experimentId,
|
||||
repetition_id: repId,
|
||||
scheduled_start_time: soakingStart.toISOString(),
|
||||
soaking_duration_minutes: soaking.soaking_duration_minutes
|
||||
@@ -944,7 +943,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
// Create/update airdrying record with repetition_id
|
||||
await phaseManagement.createAirdrying({
|
||||
experiment_id: experimentId,
|
||||
repetition_id: repId,
|
||||
scheduled_start_time: airdryingStart.toISOString(),
|
||||
duration_minutes: airdrying.duration_minutes
|
||||
@@ -957,7 +955,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
if (phase?.cracking_machine_type_id) {
|
||||
await phaseManagement.createCracking({
|
||||
experiment_id: experimentId,
|
||||
repetition_id: repId,
|
||||
machine_type_id: phase.cracking_machine_type_id,
|
||||
scheduled_start_time: crackingStart.toISOString()
|
||||
@@ -999,8 +996,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="h-full flex flex-col overflow-hidden -m-4 md:-m-6">
|
||||
<div className="p-6 flex-shrink-0">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mb-4"
|
||||
@@ -1018,7 +1015,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="px-6 pb-6 flex-1 min-h-0 overflow-hidden">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6 h-full flex flex-col min-h-0 overflow-hidden">
|
||||
{error && (
|
||||
<div className="mb-4 text-sm text-red-600 dark:text-red-400">{error}</div>
|
||||
)}
|
||||
@@ -1033,7 +1031,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">Fetching conductors, phases, and experiments.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 flex-shrink-0">
|
||||
{/* Left: Conductors with future availability */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
@@ -1254,8 +1252,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</div>
|
||||
)}
|
||||
{/* Week Calendar for selected conductors' availability */}
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="mt-6 flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-3 flex-shrink-0">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Selected Conductors' Availability & Experiment Scheduling</h3>
|
||||
<div className="flex gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Marker Style:</span>
|
||||
@@ -1297,7 +1295,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={calendarRef} className="h-[80vh] border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
|
||||
<div ref={calendarRef} className="flex-1 min-h-0 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DnDCalendar
|
||||
localizer={localizer}
|
||||
@@ -1387,6 +1385,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -80,8 +80,7 @@ export interface MachineType {
|
||||
// Phase-specific interfaces
|
||||
export interface Soaking {
|
||||
id: string
|
||||
experiment_id: string
|
||||
repetition_id?: string | null
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
actual_start_time?: string | null
|
||||
soaking_duration_minutes: number
|
||||
@@ -94,8 +93,7 @@ export interface Soaking {
|
||||
|
||||
export interface Airdrying {
|
||||
id: string
|
||||
experiment_id: string
|
||||
repetition_id?: string | null
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
actual_start_time?: string | null
|
||||
duration_minutes: number
|
||||
@@ -307,8 +305,7 @@ export interface UpdatePhaseDataRequest {
|
||||
|
||||
// Phase creation request interfaces
|
||||
export interface CreateSoakingRequest {
|
||||
experiment_id: string
|
||||
repetition_id?: string
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
soaking_duration_minutes: number
|
||||
actual_start_time?: string
|
||||
@@ -316,19 +313,17 @@ export interface CreateSoakingRequest {
|
||||
}
|
||||
|
||||
export interface CreateAirdryingRequest {
|
||||
experiment_id: string
|
||||
repetition_id?: string
|
||||
scheduled_start_time?: string // Will be auto-calculated from soaking if not provided
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
duration_minutes: number
|
||||
actual_start_time?: string
|
||||
actual_end_time?: string
|
||||
}
|
||||
|
||||
export interface CreateCrackingRequest {
|
||||
experiment_id: string
|
||||
repetition_id?: string
|
||||
repetition_id: string
|
||||
machine_type_id: string
|
||||
scheduled_start_time?: string // Will be auto-calculated from airdrying if not provided
|
||||
scheduled_start_time: string
|
||||
actual_start_time?: string
|
||||
actual_end_time?: string
|
||||
}
|
||||
@@ -798,11 +793,22 @@ export const phaseManagement = {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
if (!request.repetition_id) {
|
||||
throw new Error('repetition_id is required')
|
||||
}
|
||||
|
||||
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.soaking_duration_minutes * 60000).toISOString()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('soaking')
|
||||
.insert({
|
||||
...request,
|
||||
.upsert({
|
||||
repetition_id: request.repetition_id,
|
||||
scheduled_start_time: request.scheduled_start_time,
|
||||
soaking_duration_minutes: request.soaking_duration_minutes,
|
||||
scheduled_end_time: scheduledEndTime,
|
||||
created_by: user.id
|
||||
}, {
|
||||
onConflict: 'repetition_id'
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
@@ -824,10 +830,23 @@ export const phaseManagement = {
|
||||
},
|
||||
|
||||
async getSoakingByExperimentId(experimentId: string): Promise<Soaking | null> {
|
||||
// Get the first repetition for this experiment
|
||||
const { data: repetitions, error: repsError } = await supabase
|
||||
.from('experiment_repetitions')
|
||||
.select('id')
|
||||
.eq('experiment_id', experimentId)
|
||||
.order('repetition_number', { ascending: true })
|
||||
.limit(1)
|
||||
|
||||
if (repsError || !repetitions || repetitions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get soaking for the first repetition
|
||||
const { data, error } = await supabase
|
||||
.from('soaking')
|
||||
.select('*')
|
||||
.eq('experiment_id', experimentId)
|
||||
.eq('repetition_id', repetitions[0].id)
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
@@ -856,11 +875,26 @@ export const phaseManagement = {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
if (!request.repetition_id) {
|
||||
throw new Error('repetition_id is required')
|
||||
}
|
||||
|
||||
if (!request.scheduled_start_time) {
|
||||
throw new Error('scheduled_start_time is required')
|
||||
}
|
||||
|
||||
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.duration_minutes * 60000).toISOString()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('airdrying')
|
||||
.insert({
|
||||
...request,
|
||||
.upsert({
|
||||
repetition_id: request.repetition_id,
|
||||
scheduled_start_time: request.scheduled_start_time,
|
||||
duration_minutes: request.duration_minutes,
|
||||
scheduled_end_time: scheduledEndTime,
|
||||
created_by: user.id
|
||||
}, {
|
||||
onConflict: 'repetition_id'
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
@@ -882,10 +916,23 @@ export const phaseManagement = {
|
||||
},
|
||||
|
||||
async getAirdryingByExperimentId(experimentId: string): Promise<Airdrying | null> {
|
||||
// Get the first repetition for this experiment
|
||||
const { data: repetitions, error: repsError } = await supabase
|
||||
.from('experiment_repetitions')
|
||||
.select('id')
|
||||
.eq('experiment_id', experimentId)
|
||||
.order('repetition_number', { ascending: true })
|
||||
.limit(1)
|
||||
|
||||
if (repsError || !repetitions || repetitions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get airdrying for the first repetition
|
||||
const { data, error } = await supabase
|
||||
.from('airdrying')
|
||||
.select('*')
|
||||
.eq('experiment_id', experimentId)
|
||||
.eq('repetition_id', repetitions[0].id)
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
@@ -914,11 +961,23 @@ export const phaseManagement = {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
if (!request.repetition_id) {
|
||||
throw new Error('repetition_id is required')
|
||||
}
|
||||
|
||||
if (!request.scheduled_start_time) {
|
||||
throw new Error('scheduled_start_time is required')
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('cracking')
|
||||
.insert({
|
||||
...request,
|
||||
.upsert({
|
||||
repetition_id: request.repetition_id,
|
||||
machine_type_id: request.machine_type_id,
|
||||
scheduled_start_time: request.scheduled_start_time,
|
||||
created_by: user.id
|
||||
}, {
|
||||
onConflict: 'repetition_id'
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.62.10
|
||||
v2.65.2
|
||||
@@ -57,7 +57,7 @@ schema_paths = []
|
||||
enabled = true
|
||||
# Specifies an ordered list of seed files to load during db reset.
|
||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||
sql_paths = ["./seed_01_users.sql"]
|
||||
sql_paths = ["./seed_01_users.sql", "./seed_02_phase2_experiments.sql"]
|
||||
# , "./seed_04_phase2_jc_experiments.sql", "./seed_05_meyer_experiments.sql"]
|
||||
|
||||
[db.network_restrictions]
|
||||
|
||||
@@ -15,7 +15,10 @@ CREATE TABLE IF NOT EXISTS public.experiments (
|
||||
phase_id UUID NOT NULL REFERENCES public.experiment_phases(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES public.user_profiles(id)
|
||||
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
|
||||
|
||||
-- Ensure unique combination of experiment_number and phase_id
|
||||
CONSTRAINT unique_experiment_number_phase UNIQUE (experiment_number, phase_id)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.soaking (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
actual_start_time TIMESTAMP WITH TIME ZONE,
|
||||
soaking_duration_minutes INTEGER NOT NULL CHECK (soaking_duration_minutes > 0),
|
||||
@@ -18,8 +17,7 @@ CREATE TABLE IF NOT EXISTS public.soaking (
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
|
||||
|
||||
-- Ensure only one soaking per experiment or repetition
|
||||
CONSTRAINT unique_soaking_per_experiment UNIQUE (experiment_id),
|
||||
-- Ensure only one soaking per repetition
|
||||
CONSTRAINT unique_soaking_per_repetition UNIQUE (repetition_id)
|
||||
);
|
||||
|
||||
@@ -29,8 +27,7 @@ CREATE TABLE IF NOT EXISTS public.soaking (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.airdrying (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
actual_start_time TIMESTAMP WITH TIME ZONE,
|
||||
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0),
|
||||
@@ -40,8 +37,7 @@ CREATE TABLE IF NOT EXISTS public.airdrying (
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
|
||||
|
||||
-- Ensure only one airdrying per experiment or repetition
|
||||
CONSTRAINT unique_airdrying_per_experiment UNIQUE (experiment_id),
|
||||
-- Ensure only one airdrying per repetition
|
||||
CONSTRAINT unique_airdrying_per_repetition UNIQUE (repetition_id)
|
||||
);
|
||||
|
||||
@@ -51,8 +47,7 @@ CREATE TABLE IF NOT EXISTS public.airdrying (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.cracking (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
machine_type_id UUID NOT NULL REFERENCES public.machine_types(id),
|
||||
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
actual_start_time TIMESTAMP WITH TIME ZONE,
|
||||
@@ -61,8 +56,7 @@ CREATE TABLE IF NOT EXISTS public.cracking (
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
|
||||
|
||||
-- Ensure only one cracking per experiment or repetition
|
||||
CONSTRAINT unique_cracking_per_experiment UNIQUE (experiment_id),
|
||||
-- Ensure only one cracking per repetition
|
||||
CONSTRAINT unique_cracking_per_repetition UNIQUE (repetition_id)
|
||||
);
|
||||
|
||||
@@ -72,8 +66,7 @@ CREATE TABLE IF NOT EXISTS public.cracking (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.shelling (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
|
||||
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
actual_start_time TIMESTAMP WITH TIME ZONE,
|
||||
actual_end_time TIMESTAMP WITH TIME ZONE,
|
||||
@@ -81,8 +74,7 @@ CREATE TABLE IF NOT EXISTS public.shelling (
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
|
||||
|
||||
-- Ensure only one shelling per experiment or repetition
|
||||
CONSTRAINT unique_shelling_per_experiment UNIQUE (experiment_id),
|
||||
-- Ensure only one shelling per repetition
|
||||
CONSTRAINT unique_shelling_per_repetition UNIQUE (repetition_id)
|
||||
);
|
||||
|
||||
@@ -90,12 +82,6 @@ CREATE TABLE IF NOT EXISTS public.shelling (
|
||||
-- 5. INDEXES FOR PERFORMANCE
|
||||
-- =============================================
|
||||
|
||||
-- Create indexes for experiment_id references
|
||||
CREATE INDEX IF NOT EXISTS idx_soaking_experiment_id ON public.soaking(experiment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_airdrying_experiment_id ON public.airdrying(experiment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cracking_experiment_id ON public.cracking(experiment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_shelling_experiment_id ON public.shelling(experiment_id);
|
||||
|
||||
-- Create indexes for repetition references
|
||||
CREATE INDEX IF NOT EXISTS idx_soaking_repetition_id ON public.soaking(repetition_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_airdrying_repetition_id ON public.airdrying(repetition_id);
|
||||
@@ -138,11 +124,11 @@ CREATE OR REPLACE FUNCTION set_airdrying_scheduled_start_time()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- If this is a new airdrying record and no scheduled_start_time is provided,
|
||||
-- try to get it from the associated soaking's scheduled_end_time
|
||||
-- try to get it from the associated soaking's scheduled_end_time for the same repetition
|
||||
IF NEW.scheduled_start_time IS NULL THEN
|
||||
SELECT s.scheduled_end_time INTO NEW.scheduled_start_time
|
||||
FROM public.soaking s
|
||||
WHERE s.experiment_id = NEW.experiment_id
|
||||
WHERE s.repetition_id = NEW.repetition_id
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
@@ -154,11 +140,11 @@ CREATE OR REPLACE FUNCTION set_cracking_scheduled_start_time()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- If this is a new cracking record and no scheduled_start_time is provided,
|
||||
-- try to get it from the associated airdrying's scheduled_end_time
|
||||
-- try to get it from the associated airdrying's scheduled_end_time for the same repetition
|
||||
IF NEW.scheduled_start_time IS NULL THEN
|
||||
SELECT a.scheduled_end_time INTO NEW.scheduled_start_time
|
||||
FROM public.airdrying a
|
||||
WHERE a.experiment_id = NEW.experiment_id
|
||||
WHERE a.repetition_id = NEW.repetition_id
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
-- =============================================
|
||||
|
||||
-- View for experiments with all phase information
|
||||
-- Note: Since phases are now per-repetition, this view shows phase data from the first repetition
|
||||
CREATE OR REPLACE VIEW public.experiments_with_phases AS
|
||||
SELECT
|
||||
e.id,
|
||||
@@ -24,6 +25,8 @@ SELECT
|
||||
ep.has_airdrying,
|
||||
ep.has_cracking,
|
||||
ep.has_shelling,
|
||||
er.id as first_repetition_id,
|
||||
er.repetition_number as first_repetition_number,
|
||||
s.id as soaking_id,
|
||||
s.scheduled_start_time as soaking_scheduled_start,
|
||||
s.actual_start_time as soaking_actual_start,
|
||||
@@ -47,11 +50,18 @@ SELECT
|
||||
sh.actual_end_time as shelling_actual_end
|
||||
FROM public.experiments e
|
||||
LEFT JOIN public.experiment_phases ep ON e.phase_id = ep.id
|
||||
LEFT JOIN public.soaking s ON s.experiment_id = e.id
|
||||
LEFT JOIN public.airdrying ad ON ad.experiment_id = e.id
|
||||
LEFT JOIN public.cracking c ON c.experiment_id = e.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id, repetition_number
|
||||
FROM public.experiment_repetitions
|
||||
WHERE experiment_id = e.id
|
||||
ORDER BY repetition_number
|
||||
LIMIT 1
|
||||
) er ON true
|
||||
LEFT JOIN public.soaking s ON s.repetition_id = er.id
|
||||
LEFT JOIN public.airdrying ad ON ad.repetition_id = er.id
|
||||
LEFT JOIN public.cracking c ON c.repetition_id = er.id
|
||||
LEFT JOIN public.machine_types mt ON c.machine_type_id = mt.id
|
||||
LEFT JOIN public.shelling sh ON sh.experiment_id = e.id;
|
||||
LEFT JOIN public.shelling sh ON sh.repetition_id = er.id;
|
||||
|
||||
-- View for repetitions with phase information
|
||||
CREATE OR REPLACE VIEW public.repetitions_with_phases AS
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
-- Add repetition_id foreign key to cracker parameters tables
|
||||
-- This migration adds a foreign key to link cracker parameters to their repetitions
|
||||
|
||||
-- =============================================
|
||||
-- 1. ADD REPETITION_ID TO JC CRACKER PARAMETERS
|
||||
-- =============================================
|
||||
|
||||
ALTER TABLE public.jc_cracker_parameters
|
||||
ADD COLUMN IF NOT EXISTS repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE;
|
||||
|
||||
-- Add index for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_jc_cracker_parameters_repetition_id
|
||||
ON public.jc_cracker_parameters(repetition_id);
|
||||
|
||||
-- Add unique constraint to ensure one parameter set per repetition
|
||||
ALTER TABLE public.jc_cracker_parameters
|
||||
ADD CONSTRAINT unique_jc_cracker_parameters_per_repetition
|
||||
UNIQUE (repetition_id);
|
||||
|
||||
-- =============================================
|
||||
-- 2. ADD REPETITION_ID TO MEYER CRACKER PARAMETERS
|
||||
-- =============================================
|
||||
|
||||
ALTER TABLE public.meyer_cracker_parameters
|
||||
ADD COLUMN IF NOT EXISTS repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE;
|
||||
|
||||
-- Add index for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_meyer_cracker_parameters_repetition_id
|
||||
ON public.meyer_cracker_parameters(repetition_id);
|
||||
|
||||
-- Add unique constraint to ensure one parameter set per repetition
|
||||
ALTER TABLE public.meyer_cracker_parameters
|
||||
ADD CONSTRAINT unique_meyer_cracker_parameters_per_repetition
|
||||
UNIQUE (repetition_id);
|
||||
|
||||
3196
management-dashboard-web-app/supabase/seed_02_phase2_experiments.sql
Normal file
3196
management-dashboard-web-app/supabase/seed_02_phase2_experiments.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -932,7 +932,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
// Create/update soaking record with repetition_id
|
||||
await phaseManagement.createSoaking({
|
||||
experiment_id: experimentId,
|
||||
repetition_id: repId,
|
||||
scheduled_start_time: soakingStart.toISOString(),
|
||||
soaking_duration_minutes: soaking.soaking_duration_minutes
|
||||
@@ -940,7 +939,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
// Create/update airdrying record with repetition_id
|
||||
await phaseManagement.createAirdrying({
|
||||
experiment_id: experimentId,
|
||||
repetition_id: repId,
|
||||
scheduled_start_time: airdryingStart.toISOString(),
|
||||
duration_minutes: airdrying.duration_minutes
|
||||
@@ -953,7 +951,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
if (phase?.cracking_machine_type_id) {
|
||||
await phaseManagement.createCracking({
|
||||
experiment_id: experimentId,
|
||||
repetition_id: repId,
|
||||
machine_type_id: phase.cracking_machine_type_id,
|
||||
scheduled_start_time: crackingStart.toISOString()
|
||||
@@ -995,8 +992,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-6 flex flex-col" style={{ height: 'calc(100vh - 48px)' }}>
|
||||
<div className="mb-6 flex-shrink-0">
|
||||
<div className="h-full flex flex-col overflow-hidden -m-4 md:-m-6">
|
||||
<div className="p-6 flex-shrink-0">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mb-4"
|
||||
@@ -1014,7 +1011,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6 flex flex-col flex-1 min-h-0">
|
||||
<div className="px-6 pb-6 flex-1 min-h-0 overflow-hidden">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6 h-full flex flex-col min-h-0 overflow-hidden">
|
||||
{error && (
|
||||
<div className="mb-4 text-sm text-red-600 dark:text-red-400">{error}</div>
|
||||
)}
|
||||
@@ -1250,8 +1248,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</div>
|
||||
)}
|
||||
{/* Week Calendar for selected conductors' availability */}
|
||||
<div className="mt-6 flex flex-col flex-1 min-h-0">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="mt-6 flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-3 flex-shrink-0">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Selected Conductors' Availability & Experiment Scheduling</h3>
|
||||
<div className="flex gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Marker Style:</span>
|
||||
@@ -1293,7 +1291,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={calendarRef} className="flex-1 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
|
||||
<div ref={calendarRef} className="flex-1 min-h-0 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DnDCalendar
|
||||
localizer={localizer}
|
||||
@@ -1383,6 +1381,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -63,8 +63,7 @@ export interface ExperimentRepetition {
|
||||
|
||||
export interface Soaking {
|
||||
id: string
|
||||
experiment_id: string
|
||||
repetition_id?: string | null
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
actual_start_time?: string | null
|
||||
soaking_duration_minutes: number
|
||||
@@ -77,8 +76,7 @@ export interface Soaking {
|
||||
|
||||
export interface Airdrying {
|
||||
id: string
|
||||
experiment_id: string
|
||||
repetition_id?: string | null
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
actual_start_time?: string | null
|
||||
duration_minutes: number
|
||||
@@ -119,22 +117,19 @@ export interface UpdateRepetitionRequest {
|
||||
}
|
||||
|
||||
export interface CreateSoakingRequest {
|
||||
experiment_id: string
|
||||
repetition_id?: string | null
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
soaking_duration_minutes: number
|
||||
}
|
||||
|
||||
export interface CreateAirdryingRequest {
|
||||
experiment_id: string
|
||||
repetition_id?: string | null
|
||||
repetition_id: string
|
||||
scheduled_start_time: string
|
||||
duration_minutes: number
|
||||
}
|
||||
|
||||
export interface CreateCrackingRequest {
|
||||
experiment_id: string
|
||||
repetition_id?: string | null
|
||||
repetition_id: string
|
||||
machine_type_id: string
|
||||
scheduled_start_time: string
|
||||
}
|
||||
@@ -277,16 +272,22 @@ export const phaseManagement = {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
if (!request.repetition_id) {
|
||||
throw new Error('repetition_id is required')
|
||||
}
|
||||
|
||||
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.soaking_duration_minutes * 60000).toISOString()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('soaking')
|
||||
.upsert({
|
||||
...request,
|
||||
repetition_id: request.repetition_id,
|
||||
scheduled_start_time: request.scheduled_start_time,
|
||||
soaking_duration_minutes: request.soaking_duration_minutes,
|
||||
scheduled_end_time: scheduledEndTime,
|
||||
created_by: user.id
|
||||
}, {
|
||||
onConflict: 'experiment_id,repetition_id'
|
||||
onConflict: 'repetition_id'
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
@@ -299,16 +300,26 @@ export const phaseManagement = {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
if (!request.repetition_id) {
|
||||
throw new Error('repetition_id is required')
|
||||
}
|
||||
|
||||
if (!request.scheduled_start_time) {
|
||||
throw new Error('scheduled_start_time is required')
|
||||
}
|
||||
|
||||
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.duration_minutes * 60000).toISOString()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('airdrying')
|
||||
.upsert({
|
||||
...request,
|
||||
repetition_id: request.repetition_id,
|
||||
scheduled_start_time: request.scheduled_start_time,
|
||||
duration_minutes: request.duration_minutes,
|
||||
scheduled_end_time: scheduledEndTime,
|
||||
created_by: user.id
|
||||
}, {
|
||||
onConflict: 'experiment_id,repetition_id'
|
||||
onConflict: 'repetition_id'
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
@@ -321,13 +332,23 @@ export const phaseManagement = {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
if (!request.repetition_id) {
|
||||
throw new Error('repetition_id is required')
|
||||
}
|
||||
|
||||
if (!request.scheduled_start_time) {
|
||||
throw new Error('scheduled_start_time is required')
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('cracking')
|
||||
.upsert({
|
||||
...request,
|
||||
repetition_id: request.repetition_id,
|
||||
machine_type_id: request.machine_type_id,
|
||||
scheduled_start_time: request.scheduled_start_time,
|
||||
created_by: user.id
|
||||
}, {
|
||||
onConflict: 'experiment_id,repetition_id'
|
||||
onConflict: 'repetition_id'
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
@@ -337,11 +358,23 @@ export const phaseManagement = {
|
||||
},
|
||||
|
||||
async getSoakingByExperimentId(experimentId: string): Promise<Soaking | null> {
|
||||
// Get the first repetition for this experiment
|
||||
const { data: repetitions, error: repsError } = await supabase
|
||||
.from('experiment_repetitions')
|
||||
.select('id')
|
||||
.eq('experiment_id', experimentId)
|
||||
.order('repetition_number', { ascending: true })
|
||||
.limit(1)
|
||||
|
||||
if (repsError || !repetitions || repetitions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get soaking for the first repetition
|
||||
const { data, error } = await supabase
|
||||
.from('soaking')
|
||||
.select('*')
|
||||
.eq('experiment_id', experimentId)
|
||||
.is('repetition_id', null)
|
||||
.eq('repetition_id', repetitions[0].id)
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
@@ -352,11 +385,23 @@ export const phaseManagement = {
|
||||
},
|
||||
|
||||
async getAirdryingByExperimentId(experimentId: string): Promise<Airdrying | null> {
|
||||
// Get the first repetition for this experiment
|
||||
const { data: repetitions, error: repsError } = await supabase
|
||||
.from('experiment_repetitions')
|
||||
.select('id')
|
||||
.eq('experiment_id', experimentId)
|
||||
.order('repetition_number', { ascending: true })
|
||||
.limit(1)
|
||||
|
||||
if (repsError || !repetitions || repetitions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get airdrying for the first repetition
|
||||
const { data, error } = await supabase
|
||||
.from('airdrying')
|
||||
.select('*')
|
||||
.eq('experiment_id', experimentId)
|
||||
.is('repetition_id', null)
|
||||
.eq('repetition_id', repetitions[0].id)
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -27,6 +27,14 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Add hash to filenames for cache busting
|
||||
entryFileNames: 'assets/[name]-[hash].js',
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
61
scripts/diagnose_container_crashes.sh
Executable file
61
scripts/diagnose_container_crashes.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# Diagnostic script to investigate container crashes
|
||||
|
||||
echo "=========================================="
|
||||
echo "Container Crash Diagnostic"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo "1. Checking container status..."
|
||||
docker ps -a | grep usda-vision-api
|
||||
echo ""
|
||||
|
||||
echo "2. Recent container exit codes and status..."
|
||||
docker inspect usda-vision-api --format='{{.State.Status}} - Exit Code: {{.State.ExitCode}} - Started: {{.State.StartedAt}} - Finished: {{.State.FinishedAt}}' 2>/dev/null || echo "Container not found"
|
||||
echo ""
|
||||
|
||||
echo "3. Last 50 lines of container logs..."
|
||||
docker logs usda-vision-api --tail 50 2>&1
|
||||
echo ""
|
||||
|
||||
echo "4. Checking for common error patterns..."
|
||||
docker logs usda-vision-api 2>&1 | grep -iE "error|exception|failed|fatal|traceback|crash" | tail -20
|
||||
echo ""
|
||||
|
||||
echo "5. Checking container resource usage (if running)..."
|
||||
docker stats usda-vision-api --no-stream 2>/dev/null || echo "Container not running"
|
||||
echo ""
|
||||
|
||||
echo "6. Checking if config file exists in container..."
|
||||
docker exec usda-vision-api ls -la /app/config.compose.json 2>/dev/null || echo "Cannot access container or file missing"
|
||||
echo ""
|
||||
|
||||
echo "7. Checking Python process..."
|
||||
docker exec usda-vision-api ps aux | grep python 2>/dev/null || echo "Cannot access container"
|
||||
echo ""
|
||||
|
||||
echo "8. Checking for OOM (Out of Memory) kills..."
|
||||
dmesg | grep -i "killed process" | tail -5 || echo "No OOM kills found in dmesg (may require sudo)"
|
||||
echo ""
|
||||
|
||||
echo "9. Container restart count..."
|
||||
docker inspect usda-vision-api --format='Restart Count: {{.RestartCount}}' 2>/dev/null || echo "Container not found"
|
||||
echo ""
|
||||
|
||||
echo "10. Checking docker-compose status..."
|
||||
docker compose ps api 2>/dev/null || echo "Cannot check compose status"
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "Diagnostic Complete"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Common causes of crashes:"
|
||||
echo " - MQTT connection failure (check broker at 192.168.1.110:1883)"
|
||||
echo " - Camera SDK initialization failure"
|
||||
echo " - Storage path issues (/mnt/nfs_share)"
|
||||
echo " - Out of memory (OOM)"
|
||||
echo " - Missing config file"
|
||||
echo " - Python exception not caught"
|
||||
echo ""
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
onChange: (filters: { camera_name?: string; start_date?: string; end_date?: string }) => void
|
||||
onChange: (filters: { camera_name?: string; start_date?: string; end_date?: string; min_size_mb?: number; max_size_mb?: number }) => void
|
||||
}
|
||||
|
||||
export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
const [camera, setCamera] = useState('')
|
||||
const [start, setStart] = useState('')
|
||||
const [end, setEnd] = useState('')
|
||||
const [minSize, setMinSize] = useState('')
|
||||
const [maxSize, setMaxSize] = useState('')
|
||||
|
||||
const handleApply = () => {
|
||||
onChange({
|
||||
camera_name: camera || undefined,
|
||||
start_date: start || undefined,
|
||||
end_date: end || undefined
|
||||
end_date: end || undefined,
|
||||
min_size_mb: minSize ? parseFloat(minSize) : undefined,
|
||||
max_size_mb: maxSize ? parseFloat(maxSize) : undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +25,8 @@ export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
setCamera('')
|
||||
setStart('')
|
||||
setEnd('')
|
||||
setMinSize('')
|
||||
setMaxSize('')
|
||||
onChange({})
|
||||
}
|
||||
|
||||
@@ -35,7 +41,7 @@ export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
Reset all
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Camera Name
|
||||
@@ -69,6 +75,34 @@ export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Min Size (MB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={minSize}
|
||||
onChange={e => setMinSize(e.target.value)}
|
||||
placeholder="Min MB"
|
||||
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Max Size (MB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={maxSize}
|
||||
onChange={e => setMaxSize(e.target.value)}
|
||||
placeholder="Max MB"
|
||||
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={handleApply}
|
||||
|
||||
@@ -13,15 +13,45 @@ export const VideoList: React.FC<Props> = ({ onSelect }) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [filters, setFilters] = useState<{ camera_name?: string; start_date?: string; end_date?: string }>({})
|
||||
const [filters, setFilters] = useState<{ camera_name?: string; start_date?: string; end_date?: string; min_size_mb?: number; max_size_mb?: number }>({})
|
||||
const [allVideos, setAllVideos] = useState<any[]>([])
|
||||
|
||||
async function load(p: number, f = filters) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetchVideos({ page: p, limit: 12, ...f })
|
||||
setVideos(res.videos)
|
||||
setTotalPages(res.total_pages || 1)
|
||||
const { min_size_mb, max_size_mb, ...backendFilters } = f
|
||||
const hasSizeFilters = min_size_mb !== undefined || max_size_mb !== undefined
|
||||
|
||||
if (hasSizeFilters) {
|
||||
// When size filters are applied, fetch a larger batch and filter client-side
|
||||
// Fetch more videos to ensure we can filter properly
|
||||
const res = await fetchVideos({ page: 1, limit: 500, ...backendFilters })
|
||||
|
||||
// Filter by size on the client side
|
||||
const filtered = res.videos.filter((v: any) => {
|
||||
const sizeMB = v.file_size_bytes / (1024 * 1024)
|
||||
if (min_size_mb !== undefined && sizeMB < min_size_mb) return false
|
||||
if (max_size_mb !== undefined && sizeMB > max_size_mb) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// Apply pagination to filtered results
|
||||
const itemsPerPage = 12
|
||||
const start = (p - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
const paginatedVideos = filtered.slice(start, end)
|
||||
|
||||
setAllVideos(filtered)
|
||||
setVideos(paginatedVideos)
|
||||
setTotalPages(Math.ceil(filtered.length / itemsPerPage) || 1)
|
||||
} else {
|
||||
// No size filters - use normal backend pagination
|
||||
const res = await fetchVideos({ page: p, limit: 12, ...backendFilters })
|
||||
setVideos(res.videos)
|
||||
setTotalPages(res.total_pages || 1)
|
||||
setAllVideos(res.videos) // For display count
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to load videos')
|
||||
} finally {
|
||||
@@ -93,7 +123,11 @@ export const VideoList: React.FC<Props> = ({ onSelect }) => {
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Showing <span className="font-medium text-gray-900 dark:text-white">{videos.length}</span> videos
|
||||
{filters.min_size_mb !== undefined || filters.max_size_mb !== undefined ? (
|
||||
<>Showing <span className="font-medium text-gray-900 dark:text-white">{videos.length}</span> of <span className="font-medium text-gray-900 dark:text-white">{allVideos.length}</span> filtered videos</>
|
||||
) : (
|
||||
<>Showing <span className="font-medium text-gray-900 dark:text-white">{videos.length}</span> videos</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import React, { useRef, useEffect, useState } from 'react'
|
||||
import videojs from 'video.js'
|
||||
import 'video.js/dist/video-js.css'
|
||||
|
||||
@@ -12,20 +12,43 @@ type Props = {
|
||||
export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const playerRef = useRef<any>(null)
|
||||
// Use transcoded endpoint for browser compatibility (H.264)
|
||||
const src = fileId ? `${BASE}/videos/stream-transcoded?file_id=${encodeURIComponent(fileId)}` : null
|
||||
const [useTranscoded, setUseTranscoded] = useState(false)
|
||||
|
||||
// Try regular stream first (works for downloads), fallback to transcoded if needed
|
||||
const regularSrc = fileId ? `${BASE}/videos/stream?file_id=${encodeURIComponent(fileId)}` : null
|
||||
const transcodedSrc = fileId ? `${BASE}/videos/stream-transcoded?file_id=${encodeURIComponent(fileId)}` : null
|
||||
const src = useTranscoded ? transcodedSrc : regularSrc
|
||||
|
||||
// Reset transcoded flag when fileId changes
|
||||
useEffect(() => {
|
||||
setUseTranscoded(false)
|
||||
}, [fileId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileId || !src || !videoRef.current) return
|
||||
|
||||
// Dispose existing player if any
|
||||
if (playerRef.current) {
|
||||
playerRef.current.dispose()
|
||||
playerRef.current = null
|
||||
}
|
||||
|
||||
// Initialize Video.js player
|
||||
const player = videojs(videoRef.current, {
|
||||
controls: true,
|
||||
autoplay: true,
|
||||
preload: 'auto',
|
||||
autoplay: false, // Don't autoplay - let user control
|
||||
preload: 'metadata', // Load metadata first, then data on play
|
||||
fluid: false, // Disable fluid mode to respect container boundaries
|
||||
responsive: false, // Disable responsive mode to prevent overflow
|
||||
playbackRates: [0.5, 1, 1.25, 1.5, 2],
|
||||
html5: {
|
||||
vhs: {
|
||||
overrideNative: true
|
||||
},
|
||||
nativeVideoTracks: false,
|
||||
nativeAudioTracks: false,
|
||||
nativeTextTracks: false
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
src: src,
|
||||
@@ -36,14 +59,38 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
|
||||
playerRef.current = player
|
||||
|
||||
player.on('error', () => {
|
||||
let errorHandled = false
|
||||
const handleError = () => {
|
||||
const error = player.error()
|
||||
if (error) {
|
||||
if (error && !errorHandled) {
|
||||
errorHandled = true
|
||||
console.error('Video.js error:', error)
|
||||
console.error('Error code:', error.code)
|
||||
console.error('Error message:', error.message)
|
||||
|
||||
// If regular stream fails and we haven't tried transcoded yet, switch to transcoded
|
||||
if (!useTranscoded && transcodedSrc) {
|
||||
console.log('Switching to transcoded endpoint...')
|
||||
// Clear the player ref to prevent double disposal
|
||||
playerRef.current = null
|
||||
// Dispose player before switching
|
||||
try {
|
||||
player.dispose()
|
||||
} catch (e) {
|
||||
// Ignore disposal errors
|
||||
}
|
||||
// Use setTimeout to allow cleanup to complete before switching
|
||||
setTimeout(() => {
|
||||
setUseTranscoded(true)
|
||||
}, 100)
|
||||
} else {
|
||||
// Show user-friendly error
|
||||
console.error('Video playback failed. Please try downloading the video instead.')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
player.on('error', handleError)
|
||||
|
||||
player.on('loadedmetadata', () => {
|
||||
console.log('Video metadata loaded, duration:', player.duration())
|
||||
@@ -60,7 +107,7 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
playerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [fileId, src])
|
||||
}, [fileId, src, useTranscoded, transcodedSrc])
|
||||
|
||||
if (!fileId || !src) return null
|
||||
|
||||
@@ -127,7 +174,7 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
ref={videoRef}
|
||||
className="video-js vjs-default-skin w-full h-full"
|
||||
playsInline
|
||||
key={fileId}
|
||||
key={`${fileId}-${useTranscoded ? 'transcoded' : 'regular'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type VideoListParams = { camera_name?: string; start_date?: string; end_date?: string; limit?: number; page?: number; offset?: number; include_metadata?: boolean }
|
||||
type VideoListParams = { camera_name?: string; start_date?: string; end_date?: string; limit?: number; page?: number; offset?: number; include_metadata?: boolean; min_size_mb?: number; max_size_mb?: number }
|
||||
type VideoFile = { file_id: string; camera_name: string; filename: string; file_size_bytes: number; format: string; status: string; created_at: string; is_streamable: boolean; needs_conversion: boolean }
|
||||
type VideoListResponse = { videos: VideoFile[]; total_count: number; page?: number; total_pages?: number }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user