From 228efb0f55475ea7f136e43bf836afc74c999b1f Mon Sep 17 00:00:00 2001 From: Alireza Vaezi Date: Tue, 5 Aug 2025 13:56:26 -0400 Subject: [PATCH] feat: add Pagination component for video list navigation - Implemented a reusable Pagination component with first/last, previous/next, and numbered page buttons. - Added PageInfo component to display current page and total items. - Integrated pagination into VideoList component, allowing users to navigate through video pages. - Updated useVideoList hook to manage current page and total pages state. - Modified videoApi service to support pagination with offset-based API. - Enhanced VideoCard styling for better UI consistency. - Updated Tailwind CSS configuration to include custom colors and shadows for branding. - Refactored video file settings to use 'h264' codec for better compatibility. --- .../docs/API_CHANGES_SUMMARY.md | 49 ++- API Documentations/docs/API_DOCUMENTATION.md | 118 ++++++- .../docs/API_QUICK_REFERENCE.md | 48 +-- API Documentations/docs/MP4_FORMAT_UPDATE.md | 39 ++- API Documentations/docs/PROJECT_COMPLETE.md | 14 +- API Documentations/docs/VIDEO_STREAMING.md | 4 +- .../docs/api/CAMERA_CONFIG_API.md | 10 +- .../docs/guides/CAMERA_RECOVERY_GUIDE.md | 10 +- .../docs/guides/MQTT_LOGGING_GUIDE.md | 12 +- .../docs/guides/STREAMING_GUIDE.md | 14 +- .../docs/legacy/IMPLEMENTATION_SUMMARY.md | 16 +- .../docs/legacy/README_SYSTEM.md | 8 +- .../docs/legacy/TIMEZONE_SETUP_SUMMARY.md | 2 +- api-endpoints.http | 6 +- src/components/AutoRecordingTest.tsx | 2 +- src/components/CameraConfigModal.tsx | 41 ++- src/components/CameraPreviewModal.tsx | 45 ++- src/components/CreateUserModal.tsx | 49 ++- src/components/DashboardHome.tsx | 172 +++++----- src/components/DashboardLayout.tsx | 75 ++++- src/components/ExperimentModal.tsx | 41 ++- src/components/RepetitionScheduleModal.tsx | 45 ++- src/components/ScheduleModal.tsx | 103 +++--- src/components/Sidebar.tsx | 285 +++++++++++++---- src/components/TopNavbar.tsx | 271 +++++++++++----- src/components/VisionSystem.tsx | 2 +- .../video-streaming/VideoStreamingPage.tsx | 198 ++++++------ .../video-streaming/components/Pagination.tsx | 160 ++++++++++ .../video-streaming/components/VideoCard.tsx | 10 +- .../video-streaming/components/VideoList.tsx | 72 +++-- .../video-streaming/components/index.ts | 2 + .../video-streaming/hooks/useVideoList.ts | 81 ++++- .../video-streaming/services/videoApi.ts | 29 +- src/features/video-streaming/types/index.ts | 17 + src/index.css | 295 +++++++++++++++++- src/lib/autoRecordingManager.ts | 4 +- src/utils/videoFileUtils.ts | 4 +- tailwind.config.js | 87 +++++- 38 files changed, 1836 insertions(+), 604 deletions(-) create mode 100644 src/features/video-streaming/components/Pagination.tsx diff --git a/API Documentations/docs/API_CHANGES_SUMMARY.md b/API Documentations/docs/API_CHANGES_SUMMARY.md index d7af414..a23b324 100644 --- a/API Documentations/docs/API_CHANGES_SUMMARY.md +++ b/API Documentations/docs/API_CHANGES_SUMMARY.md @@ -1,27 +1,32 @@ # API Changes Summary: Camera Settings and Video Format Updates ## Overview + This document tracks major API changes including camera settings enhancements and the MP4 video format update. ## 🎥 Latest Update: MP4 Video Format (v2.1) + **Date**: August 2025 **Major Changes**: -- **Video Format**: Changed from AVI/XVID to MP4/MPEG-4 format + +- **Video Format**: Changed from AVI/XVID to MP4/H.264 format - **File Extensions**: New recordings use `.mp4` instead of `.avi` - **File Size**: ~40% reduction in file sizes - **Streaming**: Better web browser compatibility **New Configuration Fields**: + ```json { "video_format": "mp4", // File format: "mp4" or "avi" - "video_codec": "mp4v", // Video codec: "mp4v", "XVID", "MJPG" + "video_codec": "h264", // Video codec: "h264", "mp4v", "XVID", "MJPG" "video_quality": 95 // Quality: 0-100 (higher = better) } ``` **Frontend Impact**: + - ✅ Better streaming performance and browser support - ✅ Smaller file sizes for faster transfers - ✅ Universal HTML5 video player compatibility @@ -38,12 +43,14 @@ Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accep ## Changes Made ### 1. API Models (`usda_vision_system/api/models.py`) + - **Enhanced `StartRecordingRequest`** to include optional parameters: - `exposure_ms: Optional[float]` - Exposure time in milliseconds - `gain: Optional[float]` - Camera gain value - `fps: Optional[float]` - Target frames per second ### 2. Camera Recorder (`usda_vision_system/camera/recorder.py`) + - **Added `update_camera_settings()` method** to dynamically update camera settings: - Updates exposure time using `mvsdk.CameraSetExposureTime()` - Updates gain using `mvsdk.CameraSetAnalogGain()` @@ -52,20 +59,23 @@ Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accep - Returns boolean indicating success/failure ### 3. Camera Manager (`usda_vision_system/camera/manager.py`) + - **Enhanced `manual_start_recording()` method** to accept new parameters: - Added optional `exposure_ms`, `gain`, and `fps` parameters - Calls `update_camera_settings()` if any settings are provided - **Automatic datetime prefix**: Always prepends timestamp to filename - If custom filename provided: `{timestamp}_{custom_filename}` - - If no filename provided: `{camera_name}_manual_{timestamp}.avi` + - If no filename provided: `{camera_name}_manual_{timestamp}.mp4` ### 4. API Server (`usda_vision_system/api/server.py`) + - **Updated start-recording endpoint** to: - Pass new camera settings to camera manager - Handle filename response with datetime prefix - Maintain backward compatibility with existing requests ### 5. API Tests (`api-tests.http`) + - **Added comprehensive test examples**: - Basic recording (existing functionality) - Recording with camera settings @@ -75,8 +85,9 @@ Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accep ## Usage Examples ### Basic Recording (unchanged) + ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -84,11 +95,13 @@ Content-Type: application/json "filename": "test.avi" } ``` + **Result**: File saved as `20241223_143022_test.avi` ### Recording with Camera Settings + ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -99,13 +112,16 @@ Content-Type: application/json "fps": 5.0 } ``` + **Result**: + - Camera settings updated before recording - File saved as `20241223_143022_high_quality.avi` ### Maximum FPS Recording + ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -116,14 +132,17 @@ Content-Type: application/json "fps": 0 } ``` + **Result**: + - Camera captures at maximum possible speed (no delay between frames) - Video file saved with 30 FPS metadata for proper playback - Actual capture rate depends on camera hardware and exposure settings ### Settings Only (no filename) + ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -133,34 +152,41 @@ Content-Type: application/json "fps": 7.0 } ``` -**Result**: + +**Result**: + - Camera settings updated - File saved as `camera1_manual_20241223_143022.avi` ## Key Features ### 1. **Backward Compatibility** + - All existing API calls continue to work unchanged - New parameters are optional - Default behavior preserved when no settings provided ### 2. **Automatic Datetime Prefix** + - **ALL filenames now have datetime prefix** regardless of what's sent - Format: `YYYYMMDD_HHMMSS_` (Atlanta timezone) - Ensures unique filenames and chronological ordering ### 3. **Dynamic Camera Settings** + - Settings can be changed per recording without restarting system - Based on proven implementation from `old tests/camera_video_recorder.py` - Proper error handling and logging ### 4. **Maximum FPS Capture** + - **`fps: 0`** = Capture at maximum possible speed (no delay between frames) - **`fps > 0`** = Capture at specified frame rate with controlled timing - **`fps` omitted** = Uses camera config default (usually 3.0 fps) - Video files saved with 30 FPS metadata when fps=0 for proper playback ### 5. **Parameter Validation** + - Uses Pydantic models for automatic validation - Optional parameters with proper type checking - Descriptive field documentation @@ -168,6 +194,7 @@ Content-Type: application/json ## Testing Run the test script to verify functionality: + ```bash # Start the system first python main.py @@ -177,6 +204,7 @@ python test_api_changes.py ``` The test script verifies: + - Basic recording functionality - Camera settings application - Filename datetime prefix handling @@ -185,22 +213,27 @@ The test script verifies: ## Implementation Notes ### Camera Settings Mapping + - **Exposure**: Converted from milliseconds to microseconds for SDK - **Gain**: Converted to camera units (multiplied by 100) - **FPS**: Stored in camera config, used by recording loop ### Error Handling + - Settings update failures are logged but don't prevent recording - Invalid camera names return appropriate HTTP errors - Camera initialization failures are handled gracefully ### Filename Generation + - Uses `format_filename_timestamp()` from timezone utilities - Ensures Atlanta timezone consistency - Handles both custom and auto-generated filenames ## Similar to Old Implementation + The camera settings functionality mirrors the proven approach in `old tests/camera_video_recorder.py`: + - Same parameter names and ranges - Same SDK function calls - Same conversion factors diff --git a/API Documentations/docs/API_DOCUMENTATION.md b/API Documentations/docs/API_DOCUMENTATION.md index 0a648c0..065413d 100644 --- a/API Documentations/docs/API_DOCUMENTATION.md +++ b/API Documentations/docs/API_DOCUMENTATION.md @@ -18,10 +18,13 @@ This document provides comprehensive documentation for all API endpoints in the ## 🔧 System Status & Health ### Get System Status + ```http GET /system/status ``` + **Response**: `SystemStatusResponse` + ```json { "system_started": true, @@ -49,10 +52,13 @@ GET /system/status ``` ### Health Check + ```http GET /health ``` + **Response**: Simple health status + ```json { "status": "healthy", @@ -63,16 +69,21 @@ GET /health ## 📷 Camera Management ### Get All Cameras + ```http GET /cameras ``` + **Response**: `Dict[str, CameraStatusResponse]` ### Get Specific Camera Status + ```http GET /cameras/{camera_name}/status ``` + **Response**: `CameraStatusResponse` + ```json { "name": "camera1", @@ -97,12 +108,13 @@ GET /cameras/{camera_name}/status ## 🎥 Recording Control ### Start Recording + ```http POST /cameras/{camera_name}/start-recording Content-Type: application/json { - "filename": "test_recording.avi", + "filename": "test_recording.mp4", "exposure_ms": 2.0, "gain": 4.0, "fps": 5.0 @@ -110,30 +122,36 @@ Content-Type: application/json ``` **Request Model**: `StartRecordingRequest` + - `filename` (optional): Custom filename (datetime prefix will be added automatically) - `exposure_ms` (optional): Exposure time in milliseconds - `gain` (optional): Camera gain value - `fps` (optional): Target frames per second **Response**: `StartRecordingResponse` + ```json { "success": true, "message": "Recording started for camera1", - "filename": "20240115_103000_test_recording.avi" + "filename": "20240115_103000_test_recording.mp4" } ``` **Key Features**: + - ✅ **Automatic datetime prefix**: All filenames get `YYYYMMDD_HHMMSS_` prefix - ✅ **Dynamic camera settings**: Adjust exposure, gain, and FPS per recording - ✅ **Backward compatibility**: All existing API calls work unchanged ### Stop Recording + ```http POST /cameras/{camera_name}/stop-recording ``` + **Response**: `StopRecordingResponse` + ```json { "success": true, @@ -145,10 +163,13 @@ POST /cameras/{camera_name}/stop-recording ## 🤖 Auto-Recording Management ### Enable Auto-Recording for Camera + ```http POST /cameras/{camera_name}/auto-recording/enable ``` + **Response**: `AutoRecordingConfigResponse` + ```json { "success": true, @@ -159,16 +180,21 @@ POST /cameras/{camera_name}/auto-recording/enable ``` ### Disable Auto-Recording for Camera + ```http POST /cameras/{camera_name}/auto-recording/disable ``` + **Response**: `AutoRecordingConfigResponse` ### Get Auto-Recording Status + ```http GET /auto-recording/status ``` + **Response**: `AutoRecordingStatusResponse` + ```json { "running": true, @@ -179,6 +205,7 @@ GET /auto-recording/status ``` **Auto-Recording Features**: + - 🤖 **MQTT-triggered recording**: Automatically starts/stops based on machine state - 🔄 **Retry logic**: Failed recordings are retried with configurable delays - 📊 **Per-camera control**: Enable/disable auto-recording individually @@ -187,10 +214,13 @@ GET /auto-recording/status ## 🎛️ Camera Configuration ### Get Camera Configuration + ```http GET /cameras/{camera_name}/config ``` + **Response**: `CameraConfigResponse` + ```json { "name": "camera1", @@ -225,6 +255,7 @@ GET /cameras/{camera_name}/config ``` ### Update Camera Configuration + ```http PUT /cameras/{camera_name}/config Content-Type: application/json @@ -238,11 +269,13 @@ Content-Type: application/json ``` ### Apply Configuration (Restart Required) + ```http POST /cameras/{camera_name}/apply-config ``` **Configuration Categories**: + - ✅ **Real-time**: `exposure_ms`, `gain`, `target_fps`, `sharpness`, `contrast`, etc. - ⚠️ **Restart required**: `noise_filter_enabled`, `denoise_3d_enabled`, `bit_depth`, `video_format`, `video_codec`, `video_quality` @@ -251,16 +284,21 @@ For detailed configuration options, see [Camera Configuration API Guide](api/CAM ## 📡 MQTT & Machine Status ### Get All Machines + ```http GET /machines ``` + **Response**: `Dict[str, MachineStatusResponse]` ### Get MQTT Status + ```http GET /mqtt/status ``` + **Response**: `MQTTStatusResponse` + ```json { "connected": true, @@ -275,10 +313,13 @@ GET /mqtt/status ``` ### Get MQTT Events History + ```http GET /mqtt/events?limit=10 ``` + **Response**: `MQTTEventsHistoryResponse` + ```json { "events": [ @@ -299,10 +340,13 @@ GET /mqtt/events?limit=10 ## 💾 Storage & File Management ### Get Storage Statistics + ```http GET /storage/stats ``` + **Response**: `StorageStatsResponse` + ```json { "base_path": "/storage", @@ -328,6 +372,7 @@ GET /storage/stats ``` ### Get File List + ```http POST /storage/files Content-Type: application/json @@ -339,7 +384,9 @@ Content-Type: application/json "limit": 50 } ``` + **Response**: `FileListResponse` + ```json { "files": [ @@ -356,6 +403,7 @@ Content-Type: application/json ``` ### Cleanup Old Files + ```http POST /storage/cleanup Content-Type: application/json @@ -364,7 +412,9 @@ Content-Type: application/json "max_age_days": 30 } ``` + **Response**: `CleanupResponse` + ```json { "files_removed": 25, @@ -376,42 +426,55 @@ Content-Type: application/json ## 🔄 Camera Recovery & Diagnostics ### Test Camera Connection + ```http POST /cameras/{camera_name}/test-connection ``` + **Response**: `CameraTestResponse` ### Reconnect Camera + ```http POST /cameras/{camera_name}/reconnect ``` + **Response**: `CameraRecoveryResponse` ### Restart Camera Grab Process + ```http POST /cameras/{camera_name}/restart-grab ``` + **Response**: `CameraRecoveryResponse` ### Reset Camera Timestamp + ```http POST /cameras/{camera_name}/reset-timestamp ``` + **Response**: `CameraRecoveryResponse` ### Full Camera Reset + ```http POST /cameras/{camera_name}/full-reset ``` + **Response**: `CameraRecoveryResponse` ### Reinitialize Camera + ```http POST /cameras/{camera_name}/reinitialize ``` + **Response**: `CameraRecoveryResponse` **Recovery Response Example**: + ```json { "success": true, @@ -425,22 +488,27 @@ POST /cameras/{camera_name}/reinitialize ## 📺 Live Streaming ### Get Live MJPEG Stream + ```http GET /cameras/{camera_name}/stream ``` + **Response**: MJPEG video stream (multipart/x-mixed-replace) ### Start Camera Stream + ```http POST /cameras/{camera_name}/start-stream ``` ### Stop Camera Stream + ```http POST /cameras/{camera_name}/stop-stream ``` **Streaming Features**: + - 📺 **MJPEG format**: Compatible with web browsers and React apps - 🔄 **Concurrent operation**: Stream while recording simultaneously - ⚡ **Low latency**: Real-time preview for monitoring @@ -450,8 +518,9 @@ For detailed streaming integration, see [Streaming Guide](guides/STREAMING_GUIDE ## 🌐 WebSocket Real-time Updates ### Connect to WebSocket + ```javascript -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); @@ -460,6 +529,7 @@ ws.onmessage = (event) => { ``` **WebSocket Message Types**: + - `system_status`: System status changes - `camera_status`: Camera status updates - `recording_started`: Recording start events @@ -468,6 +538,7 @@ ws.onmessage = (event) => { - `auto_recording_event`: Auto-recording status changes **Example WebSocket Message**: + ```json { "type": "recording_started", @@ -483,26 +554,28 @@ ws.onmessage = (event) => { ## 🚀 Quick Start Examples ### Basic System Monitoring + ```bash # Check system health -curl http://localhost:8000/health +curl http://vision:8000/health # Get overall system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Get all camera statuses -curl http://localhost:8000/cameras +curl http://vision:8000/cameras ``` ### Manual Recording Control + ```bash # Start recording with default settings -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"filename": "manual_test.avi"}' # Start recording with custom camera settings -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{ "filename": "high_quality.avi", @@ -512,28 +585,30 @@ curl -X POST http://localhost:8000/cameras/camera1/start-recording \ }' # Stop recording -curl -X POST http://localhost:8000/cameras/camera1/stop-recording +curl -X POST http://vision:8000/cameras/camera1/stop-recording ``` ### Auto-Recording Management + ```bash # Enable auto-recording for camera1 -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/enable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/enable # Check auto-recording status -curl http://localhost:8000/auto-recording/status +curl http://vision:8000/auto-recording/status # Disable auto-recording for camera1 -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/disable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/disable ``` ### Camera Configuration + ```bash # Get current camera configuration -curl http://localhost:8000/cameras/camera1/config +curl http://vision:8000/cameras/camera1/config # Update camera settings (real-time) -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "exposure_ms": 1.5, @@ -548,28 +623,33 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### ✨ New in Latest Version #### 1. Enhanced Recording API + - **Dynamic camera settings**: Set exposure, gain, and FPS per recording - **Automatic datetime prefixes**: All filenames get timestamp prefixes - **Backward compatibility**: Existing API calls work unchanged #### 2. Auto-Recording Feature + - **Per-camera control**: Enable/disable auto-recording individually - **MQTT integration**: Automatic recording based on machine states - **Retry logic**: Failed recordings are automatically retried - **Status tracking**: Monitor auto-recording attempts and failures #### 3. Advanced Camera Configuration + - **Real-time settings**: Update exposure, gain, image quality without restart - **Image enhancement**: Sharpness, contrast, saturation, gamma controls - **Noise reduction**: Configurable noise filtering and 3D denoising - **HDR support**: High Dynamic Range imaging capabilities #### 4. Live Streaming + - **MJPEG streaming**: Real-time camera preview - **Concurrent operation**: Stream while recording simultaneously - **Web-compatible**: Direct integration with React/HTML video elements #### 5. Enhanced Monitoring + - **MQTT event history**: Track machine state changes over time - **Storage statistics**: Monitor disk usage and file counts - **WebSocket updates**: Real-time system status notifications @@ -577,11 +657,13 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### 🔄 Migration Notes #### From Previous Versions + 1. **Recording API**: All existing calls work, but now return filenames with datetime prefixes 2. **Configuration**: New camera settings are optional and backward compatible 3. **Auto-recording**: New feature, requires enabling in `config.json` and per camera #### Configuration Updates + ```json { "cameras": [ @@ -613,22 +695,28 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ## 📞 Support & Integration ### API Base URL -- **Development**: `http://localhost:8000` + +- **Development**: `http://vision:8000` - **Production**: Configure in `config.json` under `system.api_host` and `system.api_port` ### Error Handling + All endpoints return standard HTTP status codes: + - `200`: Success - `404`: Resource not found (camera, file, etc.) - `500`: Internal server error - `503`: Service unavailable (camera manager, MQTT, etc.) ### Rate Limiting + - No rate limiting currently implemented - WebSocket connections are limited to reasonable concurrent connections ### CORS Support + - CORS is enabled for web dashboard integration - Configure allowed origins in the API server settings + ``` ``` diff --git a/API Documentations/docs/API_QUICK_REFERENCE.md b/API Documentations/docs/API_QUICK_REFERENCE.md index 1ec7a54..0c267bf 100644 --- a/API Documentations/docs/API_QUICK_REFERENCE.md +++ b/API Documentations/docs/API_QUICK_REFERENCE.md @@ -6,30 +6,30 @@ Quick reference for the most commonly used API endpoints. For complete documenta ```bash # Health check -curl http://localhost:8000/health +curl http://vision:8000/health # System overview -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # All cameras -curl http://localhost:8000/cameras +curl http://vision:8000/cameras # All machines -curl http://localhost:8000/machines +curl http://vision:8000/machines ``` ## 🎥 Recording Control ### Start Recording (Basic) ```bash -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"filename": "test.avi"}' ``` ### Start Recording (With Settings) ```bash -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{ "filename": "high_quality.avi", @@ -41,30 +41,30 @@ curl -X POST http://localhost:8000/cameras/camera1/start-recording \ ### Stop Recording ```bash -curl -X POST http://localhost:8000/cameras/camera1/stop-recording +curl -X POST http://vision:8000/cameras/camera1/stop-recording ``` ## 🤖 Auto-Recording ```bash # Enable auto-recording -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/enable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/enable # Disable auto-recording -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/disable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/disable # Check auto-recording status -curl http://localhost:8000/auto-recording/status +curl http://vision:8000/auto-recording/status ``` ## 🎛️ Camera Configuration ```bash # Get camera config -curl http://localhost:8000/cameras/camera1/config +curl http://vision:8000/cameras/camera1/config # Update camera settings -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "exposure_ms": 1.5, @@ -77,41 +77,41 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ```bash # Start streaming -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # Get MJPEG stream (use in browser/video element) -# http://localhost:8000/cameras/camera1/stream +# http://vision:8000/cameras/camera1/stream # Stop streaming -curl -X POST http://localhost:8000/cameras/camera1/stop-stream +curl -X POST http://vision:8000/cameras/camera1/stop-stream ``` ## 🔄 Camera Recovery ```bash # Test connection -curl -X POST http://localhost:8000/cameras/camera1/test-connection +curl -X POST http://vision:8000/cameras/camera1/test-connection # Reconnect camera -curl -X POST http://localhost:8000/cameras/camera1/reconnect +curl -X POST http://vision:8000/cameras/camera1/reconnect # Full reset -curl -X POST http://localhost:8000/cameras/camera1/full-reset +curl -X POST http://vision:8000/cameras/camera1/full-reset ``` ## 💾 Storage Management ```bash # Storage statistics -curl http://localhost:8000/storage/stats +curl http://vision:8000/storage/stats # List files -curl -X POST http://localhost:8000/storage/files \ +curl -X POST http://vision:8000/storage/files \ -H "Content-Type: application/json" \ -d '{"camera_name": "camera1", "limit": 10}' # Cleanup old files -curl -X POST http://localhost:8000/storage/cleanup \ +curl -X POST http://vision:8000/storage/cleanup \ -H "Content-Type: application/json" \ -d '{"max_age_days": 30}' ``` @@ -120,17 +120,17 @@ curl -X POST http://localhost:8000/storage/cleanup \ ```bash # MQTT status -curl http://localhost:8000/mqtt/status +curl http://vision:8000/mqtt/status # Recent MQTT events -curl http://localhost:8000/mqtt/events?limit=10 +curl http://vision:8000/mqtt/events?limit=10 ``` ## 🌐 WebSocket Connection ```javascript // Connect to real-time updates -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); diff --git a/API Documentations/docs/MP4_FORMAT_UPDATE.md b/API Documentations/docs/MP4_FORMAT_UPDATE.md index ecae663..a6f2dcc 100644 --- a/API Documentations/docs/MP4_FORMAT_UPDATE.md +++ b/API Documentations/docs/MP4_FORMAT_UPDATE.md @@ -1,20 +1,24 @@ # 🎥 MP4 Video Format Update - Frontend Integration Guide ## Overview + The USDA Vision Camera System has been updated to record videos in **MP4 format** instead of AVI format for better streaming compatibility and smaller file sizes. ## 🔄 What Changed ### Video Format + - **Before**: AVI files with XVID codec (`.avi` extension) -- **After**: MP4 files with MPEG-4 codec (`.mp4` extension) +- **After**: MP4 files with H.264 codec (`.mp4` extension) ### File Extensions + - All new video recordings now use `.mp4` extension - Existing `.avi` files remain accessible and functional - File size reduction: ~40% smaller than equivalent AVI files ### API Response Updates + New fields added to camera configuration responses: ```json @@ -28,13 +32,17 @@ New fields added to camera configuration responses: ## 🌐 Frontend Impact ### 1. Video Player Compatibility + **✅ Better Browser Support** + - MP4 format has native support in all modern browsers - No need for additional codecs or plugins - Better mobile device compatibility (iOS/Android) ### 2. File Handling Updates + **File Extension Handling** + ```javascript // Update file extension checks const isVideoFile = (filename) => { @@ -50,7 +58,9 @@ const getVideoMimeType = (filename) => { ``` ### 3. Video Streaming + **Improved Streaming Performance** + ```javascript // MP4 files can be streamed directly without conversion const videoUrl = `/api/videos/${videoId}/stream`; @@ -63,7 +73,9 @@ const videoUrl = `/api/videos/${videoId}/stream`; ``` ### 4. File Size Display + **Updated Size Expectations** + - MP4 files are ~40% smaller than equivalent AVI files - Update any file size warnings or storage calculations - Better compression means faster downloads and uploads @@ -71,9 +83,11 @@ const videoUrl = `/api/videos/${videoId}/stream`; ## 📡 API Changes ### Camera Configuration Endpoint + **GET** `/cameras/{camera_name}/config` **New Response Fields:** + ```json { "name": "camera1", @@ -95,7 +109,9 @@ const videoUrl = `/api/videos/${videoId}/stream`; ``` ### Video Listing Endpoints + **File Extension Updates** + - Video files in responses will now have `.mp4` extensions - Existing `.avi` files will still appear in listings - Filter by both extensions when needed @@ -103,42 +119,49 @@ const videoUrl = `/api/videos/${videoId}/stream`; ## 🔧 Configuration Options ### Video Format Settings + ```json { "video_format": "mp4", // Options: "mp4", "avi" - "video_codec": "mp4v", // Options: "mp4v", "XVID", "MJPG" + "video_codec": "h264", // Options: "h264", "mp4v", "XVID", "MJPG" "video_quality": 95 // Range: 0-100 (higher = better quality) } ``` ### Recommended Settings -- **Production**: `"mp4"` format, `"mp4v"` codec, `95` quality -- **Storage Optimized**: `"mp4"` format, `"mp4v"` codec, `85` quality + +- **Production**: `"mp4"` format, `"h264"` codec, `95` quality +- **Storage Optimized**: `"mp4"` format, `"h264"` codec, `85` quality - **Legacy Mode**: `"avi"` format, `"XVID"` codec, `95` quality ## 🎯 Frontend Implementation Checklist ### ✅ Video Player Updates + - [ ] Verify HTML5 video player works with MP4 files - [ ] Update video MIME type handling - [ ] Test streaming performance with new format ### ✅ File Management + - [ ] Update file extension filters to include `.mp4` - [ ] Modify file type detection logic - [ ] Update download/upload handling for MP4 files ### ✅ UI/UX Updates + - [ ] Update file size expectations in UI - [ ] Modify any format-specific icons or indicators - [ ] Update help text or tooltips mentioning video formats ### ✅ Configuration Interface + - [ ] Add video format settings to camera config UI - [ ] Include video quality slider/selector - [ ] Add restart warning for video format changes ### ✅ Testing + - [ ] Test video playback with new MP4 files - [ ] Verify backward compatibility with existing AVI files - [ ] Test streaming performance and loading times @@ -146,11 +169,13 @@ const videoUrl = `/api/videos/${videoId}/stream`; ## 🔄 Backward Compatibility ### Existing AVI Files + - All existing `.avi` files remain fully functional - No conversion or migration required - Video player should handle both formats ### API Compatibility + - All existing API endpoints continue to work - New fields are additive (won't break existing code) - Default values provided for new configuration fields @@ -158,6 +183,7 @@ const videoUrl = `/api/videos/${videoId}/stream`; ## 📊 Performance Benefits ### File Size Reduction + ``` Example 5-minute recording at 1280x1024: - AVI/XVID: ~180 MB @@ -165,12 +191,14 @@ Example 5-minute recording at 1280x1024: ``` ### Streaming Improvements + - Faster initial load times - Better progressive download support - Reduced bandwidth usage - Native browser optimization ### Storage Efficiency + - More recordings fit in same storage space - Faster backup and transfer operations - Reduced storage costs over time @@ -178,16 +206,19 @@ Example 5-minute recording at 1280x1024: ## 🚨 Important Notes ### Restart Required + - Video format changes require camera service restart - Mark video format settings as "restart required" in UI - Provide clear user feedback about restart necessity ### Browser Compatibility + - MP4 format supported in all modern browsers - Better mobile device support than AVI - No additional plugins or codecs needed ### Quality Assurance + - Video quality maintained at 95/100 setting - No visual degradation compared to AVI - High bitrate ensures professional quality diff --git a/API Documentations/docs/PROJECT_COMPLETE.md b/API Documentations/docs/PROJECT_COMPLETE.md index 0f4df48..7f240d6 100644 --- a/API Documentations/docs/PROJECT_COMPLETE.md +++ b/API Documentations/docs/PROJECT_COMPLETE.md @@ -97,11 +97,11 @@ python test_system.py ### Dashboard Integration ```javascript // React component example -const systemStatus = await fetch('http://localhost:8000/system/status'); -const cameras = await fetch('http://localhost:8000/cameras'); +const systemStatus = await fetch('http://vision:8000/system/status'); +const cameras = await fetch('http://vision:8000/cameras'); // WebSocket for real-time updates -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); // Handle real-time system updates @@ -111,13 +111,13 @@ ws.onmessage = (event) => { ### Manual Control ```bash # Start recording manually -curl -X POST http://localhost:8000/cameras/camera1/start-recording +curl -X POST http://vision:8000/cameras/camera1/start-recording # Stop recording manually -curl -X POST http://localhost:8000/cameras/camera1/stop-recording +curl -X POST http://vision:8000/cameras/camera1/stop-recording # Get system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status ``` ## 📊 System Capabilities @@ -151,7 +151,7 @@ curl http://localhost:8000/system/status ### Troubleshooting - **Test Suite**: `python test_system.py` - **Time Check**: `python check_time.py` -- **API Health**: `curl http://localhost:8000/health` +- **API Health**: `curl http://vision:8000/health` - **Debug Mode**: `python main.py --log-level DEBUG` ## 🎯 Production Readiness diff --git a/API Documentations/docs/VIDEO_STREAMING.md b/API Documentations/docs/VIDEO_STREAMING.md index 8e2cb61..8cbed70 100644 --- a/API Documentations/docs/VIDEO_STREAMING.md +++ b/API Documentations/docs/VIDEO_STREAMING.md @@ -204,10 +204,10 @@ sudo systemctl restart usda-vision-camera ### Check Status ```bash # Check video module status -curl http://localhost:8000/system/video-module +curl http://vision:8000/system/video-module # Check available videos -curl http://localhost:8000/videos/ +curl http://vision:8000/videos/ ``` ### Logs diff --git a/API Documentations/docs/api/CAMERA_CONFIG_API.md b/API Documentations/docs/api/CAMERA_CONFIG_API.md index d65f0f8..e0e898f 100644 --- a/API Documentations/docs/api/CAMERA_CONFIG_API.md +++ b/API Documentations/docs/api/CAMERA_CONFIG_API.md @@ -185,7 +185,7 @@ POST /cameras/{camera_name}/apply-config ### Example 1: Adjust Exposure and Gain ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "exposure_ms": 1.5, @@ -195,7 +195,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 2: Improve Image Quality ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "sharpness": 150, @@ -206,7 +206,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 3: Configure for Indoor Lighting ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "anti_flicker_enabled": true, @@ -218,7 +218,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 4: Enable HDR Mode ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "hdr_enabled": true, @@ -232,7 +232,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ```jsx import React, { useState, useEffect } from 'react'; -const CameraConfig = ({ cameraName, apiBaseUrl = 'http://localhost:8000' }) => { +const CameraConfig = ({ cameraName, apiBaseUrl = 'http://vision:8000' }) => { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); diff --git a/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md b/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md index 963f3ef..4787e57 100644 --- a/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md +++ b/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md @@ -56,27 +56,27 @@ When a camera has issues, follow this order: 1. **Test Connection** - Diagnose the problem ```http - POST http://localhost:8000/cameras/camera1/test-connection + POST http://vision:8000/cameras/camera1/test-connection ``` 2. **Try Reconnect** - Most common fix ```http - POST http://localhost:8000/cameras/camera1/reconnect + POST http://vision:8000/cameras/camera1/reconnect ``` 3. **Restart Grab** - If reconnect doesn't work ```http - POST http://localhost:8000/cameras/camera1/restart-grab + POST http://vision:8000/cameras/camera1/restart-grab ``` 4. **Full Reset** - For persistent issues ```http - POST http://localhost:8000/cameras/camera1/full-reset + POST http://vision:8000/cameras/camera1/full-reset ``` 5. **Reinitialize** - For cameras that never worked ```http - POST http://localhost:8000/cameras/camera1/reinitialize + POST http://vision:8000/cameras/camera1/reinitialize ``` ## Response Format diff --git a/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md b/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md index abe1859..f1f9fd0 100644 --- a/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md +++ b/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md @@ -38,7 +38,7 @@ When you run the system, you'll see: ### MQTT Status ```http -GET http://localhost:8000/mqtt/status +GET http://vision:8000/mqtt/status ``` **Response:** @@ -60,7 +60,7 @@ GET http://localhost:8000/mqtt/status ### Machine Status ```http -GET http://localhost:8000/machines +GET http://vision:8000/machines ``` **Response:** @@ -85,7 +85,7 @@ GET http://localhost:8000/machines ### System Status ```http -GET http://localhost:8000/system/status +GET http://vision:8000/system/status ``` **Response:** @@ -125,13 +125,13 @@ Tests all the API endpoints and shows expected responses. ### 4. **Query APIs Directly** ```bash # Check MQTT status -curl http://localhost:8000/mqtt/status +curl http://vision:8000/mqtt/status # Check machine states -curl http://localhost:8000/machines +curl http://vision:8000/machines # Check overall system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status ``` ## 🔧 Configuration diff --git a/API Documentations/docs/guides/STREAMING_GUIDE.md b/API Documentations/docs/guides/STREAMING_GUIDE.md index ca55700..e35c6c3 100644 --- a/API Documentations/docs/guides/STREAMING_GUIDE.md +++ b/API Documentations/docs/guides/STREAMING_GUIDE.md @@ -40,13 +40,13 @@ Open `camera_preview.html` in your browser and click "Start Stream" for any came ### 3. API Usage ```bash # Start streaming for camera1 -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # View live stream (open in browser) -http://localhost:8000/cameras/camera1/stream +http://vision:8000/cameras/camera1/stream # Stop streaming -curl -X POST http://localhost:8000/cameras/camera1/stop-stream +curl -X POST http://vision:8000/cameras/camera1/stop-stream ``` ## 📡 API Endpoints @@ -150,10 +150,10 @@ The system supports these concurrent operations: ### Example: Concurrent Usage ```bash # Start streaming -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # Start recording (while streaming continues) -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"filename": "test_recording.avi"}' @@ -232,8 +232,8 @@ For issues with streaming functionality: 1. Check the system logs: `usda_vision_system.log` 2. Run the test script: `python test_streaming.py` -3. Verify API health: `http://localhost:8000/health` -4. Check camera status: `http://localhost:8000/cameras` +3. Verify API health: `http://vision:8000/health` +4. Check camera status: `http://vision:8000/cameras` --- diff --git a/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md b/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md index f16e737..84759d9 100644 --- a/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md +++ b/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md @@ -73,10 +73,10 @@ Edit `config.json` to customize: - System parameters ### API Access -- System status: `http://localhost:8000/system/status` -- Camera status: `http://localhost:8000/cameras` -- Manual recording: `POST http://localhost:8000/cameras/camera1/start-recording` -- Real-time updates: WebSocket at `ws://localhost:8000/ws` +- System status: `http://vision:8000/system/status` +- Camera status: `http://vision:8000/cameras` +- Manual recording: `POST http://vision:8000/cameras/camera1/start-recording` +- Real-time updates: WebSocket at `ws://vision:8000/ws` ## 📊 Test Results @@ -146,18 +146,18 @@ The system provides everything needed for your React dashboard: ```javascript // Example API usage -const systemStatus = await fetch('http://localhost:8000/system/status'); -const cameras = await fetch('http://localhost:8000/cameras'); +const systemStatus = await fetch('http://vision:8000/system/status'); +const cameras = await fetch('http://vision:8000/cameras'); // WebSocket for real-time updates -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); // Handle real-time system updates }; // Manual recording control -await fetch('http://localhost:8000/cameras/camera1/start-recording', { +await fetch('http://vision:8000/cameras/camera1/start-recording', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ camera_name: 'camera1' }) diff --git a/API Documentations/docs/legacy/README_SYSTEM.md b/API Documentations/docs/legacy/README_SYSTEM.md index 932f632..67b0542 100644 --- a/API Documentations/docs/legacy/README_SYSTEM.md +++ b/API Documentations/docs/legacy/README_SYSTEM.md @@ -192,13 +192,13 @@ Comprehensive error tracking with: ```bash # Check system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Check camera status -curl http://localhost:8000/cameras +curl http://vision:8000/cameras # Manual recording start -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"camera_name": "camera1"}' ``` @@ -246,4 +246,4 @@ This project is developed for USDA research purposes. For issues and questions: 1. Check the logs in `usda_vision_system.log` 2. Review the troubleshooting section -3. Check API status at `http://localhost:8000/health` +3. Check API status at `http://vision:8000/health` diff --git a/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md b/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md index 9866f08..24ef130 100644 --- a/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md +++ b/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md @@ -76,7 +76,7 @@ timedatectl status ### API Endpoints ```bash # System status includes time info -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Example response includes: { diff --git a/api-endpoints.http b/api-endpoints.http index 66265e7..721f549 100644 --- a/api-endpoints.http +++ b/api-endpoints.http @@ -171,7 +171,7 @@ POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { - "filename": "test_recording.avi", + "filename": "test_recording.mp4", "exposure_ms": 1.5, "gain": 3.0, "fps": 0 @@ -187,7 +187,7 @@ Content-Type: application/json # { # "success": true, # "message": "Recording started for camera1", -# "filename": "20250728_120000_test_recording.avi" +# "filename": "20250728_120000_test_recording.mp4" # } ### @@ -197,7 +197,7 @@ POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { - "filename": "simple_test.avi" + "filename": "simple_test.mp4" } ### diff --git a/src/components/AutoRecordingTest.tsx b/src/components/AutoRecordingTest.tsx index 8670c11..0459047 100644 --- a/src/components/AutoRecordingTest.tsx +++ b/src/components/AutoRecordingTest.tsx @@ -52,7 +52,7 @@ export function AutoRecordingTest() { if (state === 'on') { // Simulate starting recording on the correct camera const result = await visionApi.startRecording(cameraName, { - filename: `test_auto_${machine}_${Date.now()}.avi` + filename: `test_auto_${machine}_${Date.now()}.mp4` }) event.result = result.success ? `✅ Recording started on ${cameraName}: ${result.filename}` : `❌ Failed: ${result.message}` } else { diff --git a/src/components/CameraConfigModal.tsx b/src/components/CameraConfigModal.tsx index b7b63a0..bf1971d 100644 --- a/src/components/CameraConfigModal.tsx +++ b/src/components/CameraConfigModal.tsx @@ -166,22 +166,39 @@ export function CameraConfigModal({ cameraName, isOpen, onClose, onSuccess, onEr if (!isOpen) return null return ( -
-
+
+
+
e.stopPropagation()}> + {/* Close Button */} + + {/* Header */} -
+
-

+

Camera Configuration - {cameraName}

-
diff --git a/src/components/CameraPreviewModal.tsx b/src/components/CameraPreviewModal.tsx index 344cea4..f64b8d1 100644 --- a/src/components/CameraPreviewModal.tsx +++ b/src/components/CameraPreviewModal.tsx @@ -44,12 +44,12 @@ export function CameraPreviewModal({ cameraName, isOpen, onClose, onError }: Cam setError(null) const result = await visionApi.startStream(cameraName) - + if (result.success) { setStreaming(true) const streamUrl = visionApi.getStreamUrl(cameraName) streamUrlRef.current = streamUrl - + // Add timestamp to prevent caching if (imgRef.current) { imgRef.current.src = `${streamUrl}?t=${Date.now()}` @@ -72,7 +72,7 @@ export function CameraPreviewModal({ cameraName, isOpen, onClose, onError }: Cam await visionApi.stopStream(cameraName) setStreaming(false) streamUrlRef.current = null - + // Clear the image source if (imgRef.current) { imgRef.current.src = '' @@ -100,22 +100,39 @@ export function CameraPreviewModal({ cameraName, isOpen, onClose, onError }: Cam if (!isOpen) return null return ( -
-
+
+
+
e.stopPropagation()}> + {/* Close Button */} + +
{/* Header */}
-

+

Camera Preview: {cameraName}

-
{/* Content */} diff --git a/src/components/CreateUserModal.tsx b/src/components/CreateUserModal.tsx index 04c5456..f378d05 100644 --- a/src/components/CreateUserModal.tsx +++ b/src/components/CreateUserModal.tsx @@ -106,19 +106,36 @@ export function CreateUserModal({ roles, onClose, onUserCreated }: CreateUserMod } return ( -
-
- {/* Header */} -
-

Create New User

- + + + + + {/* Header */} +
+

Create New User

@@ -135,7 +152,7 @@ export function CreateUserModal({ roles, onClose, onUserCreated }: CreateUserMod id="email" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm placeholder-gray-400" + className="h-11 w-full rounded-lg border appearance-none px-4 py-2.5 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-brand-500/20 dark:border-gray-700 dark:focus:border-brand-800" placeholder="user@example.com" required /> @@ -238,11 +255,11 @@ export function CreateUserModal({ roles, onClose, onUserCreated }: CreateUserMod
{/* Actions */} -
+
@@ -250,7 +267,7 @@ export function CreateUserModal({ roles, onClose, onUserCreated }: CreateUserMod type="submit" form="create-user-form" disabled={loading} - className="px-6 py-2.5 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" + className="px-6 py-2.5 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 focus:outline-none focus:ring-3 focus:ring-brand-500/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > {loading ? (
diff --git a/src/components/DashboardHome.tsx b/src/components/DashboardHome.tsx index 21d033a..9ecd366 100644 --- a/src/components/DashboardHome.tsx +++ b/src/components/DashboardHome.tsx @@ -8,15 +8,15 @@ export function DashboardHome({ user }: DashboardHomeProps) { const getRoleBadgeColor = (role: string) => { switch (role) { case 'admin': - return 'bg-red-100 text-red-800' + return 'bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500' case 'conductor': - return 'bg-blue-100 text-blue-800' + return 'bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400' case 'analyst': - return 'bg-green-100 text-green-800' + return 'bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-500' case 'data recorder': - return 'bg-purple-100 text-purple-800' + return 'bg-theme-purple-500/10 text-theme-purple-500' default: - return 'bg-gray-100 text-gray-800' + return 'bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80' } } @@ -36,126 +36,144 @@ export function DashboardHome({ user }: DashboardHomeProps) { } return ( -
-
-

Dashboard

-

Welcome to the Pecan Experiments Dashboard

+
+ {/* Welcome Section */} +
+

Dashboard

+

Welcome to the Pecan Experiments Dashboard

{/* User Information Card */} -
-
-

+
+
+
+ + + +
+ +

User Information

-

+

Your account details and role permissions.

-
-
-
-
-
Email
-
- {user.email} -
+ +
+
+ Email + {user.email}
-
-
Roles
-
-
- {user.roles.map((role) => ( - - {role.charAt(0).toUpperCase() + role.slice(1)} - - ))} -
-
+ +
+ Roles +
+ {user.roles.map((role) => ( + + {role.charAt(0).toUpperCase() + role.slice(1)} + + ))} +
-
-
Status
-
- + Status + - {user.status.charAt(0).toUpperCase() + user.status.slice(1)} - -
+ {user.status.charAt(0).toUpperCase() + user.status.slice(1)} +
-
-
User ID
-
- {user.id} -
+ +
+ User ID + {user.id}
-
-
Member since
-
+ +
+ Member since + {new Date(user.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} -
+
-
+

{/* Role Permissions */} -
- {user.roles.map((role) => ( -
-
-
-
- +
+
+
+ + + +
+ +

+ Role Permissions +

+

+ Your access levels and capabilities. +

+ +
+ {user.roles.map((role) => ( +
+
+ {role.charAt(0).toUpperCase() + role.slice(1)}
-
-
-

Permissions

    {getPermissionsByRole(role).map((permission, index) => ( -
  • - +
  • + {permission}
  • ))}
-
+ ))}
- ))} +
{/* Quick Actions */} {user.roles.includes('admin') && ( -
-
-

+
+
+
+ + + +
+ +

Quick Actions

-

+

Administrative shortcuts and tools.

-
-
+
- - - -
diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index e2191d4..62f9865 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -18,6 +18,9 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [currentView, setCurrentView] = useState('dashboard') + const [isExpanded, setIsExpanded] = useState(true) + const [isMobileOpen, setIsMobileOpen] = useState(false) + const [isHovered, setIsHovered] = useState(false) useEffect(() => { fetchUserProfile() @@ -48,6 +51,22 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) { window.dispatchEvent(new PopStateEvent('popstate')) } + const toggleSidebar = () => { + setIsExpanded(!isExpanded) + } + + const toggleMobileSidebar = () => { + setIsMobileOpen(!isMobileOpen) + } + + const handleToggleSidebar = () => { + if (window.innerWidth >= 1024) { + toggleSidebar() + } else { + toggleMobileSidebar() + } + } + const renderCurrentView = () => { if (!user) return null @@ -96,8 +115,8 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) { return (
-
-

Loading dashboard...

+
+

Loading dashboard...

) @@ -107,12 +126,12 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) { return (
-
-
{error}
+
+
{error}
@@ -125,10 +144,10 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) { return (
-
No user data available
+
No user data available
@@ -138,17 +157,39 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) { } return ( -
- -
- -
+
+
+ + {/* Backdrop for mobile */} + {isMobileOpen && ( +
setIsMobileOpen(false)} + /> + )} +
+
+ +
{renderCurrentView()} -
+
) diff --git a/src/components/ExperimentModal.tsx b/src/components/ExperimentModal.tsx index a08489b..7a950dd 100644 --- a/src/components/ExperimentModal.tsx +++ b/src/components/ExperimentModal.tsx @@ -60,21 +60,38 @@ export function ExperimentModal({ experiment, onClose, onExperimentSaved }: Expe } return ( -
-
+
+
+
e.stopPropagation()}> + {/* Close Button */} + + {/* Header */} -
-

+
+

{isEditing ? `Edit Experiment #${experiment.experiment_number}` : 'Create New Experiment'}

-
diff --git a/src/components/RepetitionScheduleModal.tsx b/src/components/RepetitionScheduleModal.tsx index 9c7848d..0a307ea 100644 --- a/src/components/RepetitionScheduleModal.tsx +++ b/src/components/RepetitionScheduleModal.tsx @@ -12,7 +12,7 @@ interface RepetitionScheduleModalProps { export function RepetitionScheduleModal({ experiment, repetition, onClose, onScheduleUpdated }: RepetitionScheduleModalProps) { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - + // Initialize with existing scheduled date or current date/time const getInitialDateTime = () => { if (repetition.scheduled_date) { @@ -22,7 +22,7 @@ export function RepetitionScheduleModal({ experiment, repetition, onClose, onSch time: date.toTimeString().slice(0, 5) } } - + const now = new Date() // Set to next hour by default now.setHours(now.getHours() + 1, 0, 0, 0) @@ -92,21 +92,38 @@ export function RepetitionScheduleModal({ experiment, repetition, onClose, onSch } return ( -
-
+
+
+
e.stopPropagation()}> + {/* Close Button */} + + {/* Header */} -
-

+
+

Schedule Repetition

-
diff --git a/src/components/ScheduleModal.tsx b/src/components/ScheduleModal.tsx index 27bbc27..2cd7133 100644 --- a/src/components/ScheduleModal.tsx +++ b/src/components/ScheduleModal.tsx @@ -11,7 +11,7 @@ interface ScheduleModalProps { export function ScheduleModal({ experiment, onClose, onScheduleUpdated }: ScheduleModalProps) { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - + // Initialize with existing scheduled date or current date/time const getInitialDateTime = () => { if (experiment.scheduled_date) { @@ -21,7 +21,7 @@ export function ScheduleModal({ experiment, onClose, onScheduleUpdated }: Schedu time: date.toTimeString().slice(0, 5) } } - + const now = new Date() // Set to next hour by default now.setHours(now.getHours() + 1, 0, 0, 0) @@ -92,21 +92,38 @@ export function ScheduleModal({ experiment, onClose, onScheduleUpdated }: Schedu } return ( -
-
+
+
+
e.stopPropagation()}> + {/* Close Button */} + + {/* Header */} -
-

+
+

{isScheduled ? 'Update Schedule' : 'Schedule Experiment'}

-
@@ -138,31 +155,45 @@ export function ScheduleModal({ experiment, onClose, onScheduleUpdated }: Schedu {/* Schedule Form */}
-
-
{/* Action Buttons */} @@ -173,26 +204,26 @@ export function ScheduleModal({ experiment, onClose, onScheduleUpdated }: Schedu type="button" onClick={handleRemoveSchedule} disabled={loading} - className="px-4 py-2 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50" + className="px-4 py-2 text-sm font-medium text-error-600 hover:text-error-700 hover:bg-error-50 dark:text-error-500 dark:hover:bg-error-500/15 rounded-lg transition-colors disabled:opacity-50" > Remove Schedule )}
- +
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index ef4dbec..86768cb 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,10 +1,14 @@ -import { useState } from 'react' +import { useState, useCallback, useEffect, useRef } from 'react' import type { User } from '../lib/supabase' interface SidebarProps { user: User currentView: string onViewChange: (view: string) => void + isExpanded?: boolean + isMobileOpen?: boolean + isHovered?: boolean + setIsHovered?: (hovered: boolean) => void } interface MenuItem { @@ -12,17 +16,28 @@ interface MenuItem { name: string icon: React.ReactElement requiredRoles?: string[] + subItems?: { name: string; id: string; requiredRoles?: string[] }[] } -export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { - const [isCollapsed, setIsCollapsed] = useState(false) +export function Sidebar({ + user, + currentView, + onViewChange, + isExpanded = true, + isMobileOpen = false, + isHovered = false, + setIsHovered +}: SidebarProps) { + const [openSubmenu, setOpenSubmenu] = useState(null) + const [subMenuHeight, setSubMenuHeight] = useState>({}) + const subMenuRefs = useRef>({}) const menuItems: MenuItem[] = [ { id: 'dashboard', name: 'Dashboard', icon: ( - + @@ -32,7 +47,7 @@ export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { id: 'user-management', name: 'User Management', icon: ( - + ), @@ -42,7 +57,7 @@ export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { id: 'experiments', name: 'Experiments', icon: ( - + ), @@ -52,7 +67,7 @@ export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { id: 'video-library', name: 'Video Library', icon: ( - + ), @@ -61,7 +76,7 @@ export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { id: 'analytics', name: 'Analytics', icon: ( - + ), @@ -71,7 +86,7 @@ export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { id: 'data-entry', name: 'Data Entry', icon: ( - + ), @@ -81,80 +96,216 @@ export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { id: 'vision-system', name: 'Vision System', icon: ( - + ), } ] + // const isActive = (path: string) => location.pathname === path; + const isActive = useCallback( + (id: string) => currentView === id, + [currentView] + ) + + useEffect(() => { + // Auto-open submenu if current view is in a submenu + menuItems.forEach((nav, index) => { + if (nav.subItems) { + nav.subItems.forEach((subItem) => { + if (isActive(subItem.id)) { + setOpenSubmenu(index) + } + }) + } + }) + }, [currentView, isActive, menuItems]) + + useEffect(() => { + if (openSubmenu !== null) { + const key = `submenu-${openSubmenu}` + if (subMenuRefs.current[key]) { + setSubMenuHeight((prevHeights) => ({ + ...prevHeights, + [key]: subMenuRefs.current[key]?.scrollHeight || 0, + })) + } + } + }, [openSubmenu]) + + const handleSubmenuToggle = (index: number) => { + setOpenSubmenu((prevOpenSubmenu) => { + if (prevOpenSubmenu === index) { + return null + } + return index + }) + } + const hasAccess = (item: MenuItem): boolean => { if (!item.requiredRoles) return true return item.requiredRoles.some(role => user.roles.includes(role as any)) } + const renderMenuItems = (items: MenuItem[]) => ( +
    + {items.map((nav, index) => { + if (!hasAccess(nav)) return null + + return ( +
  • + {nav.subItems ? ( + + ) : ( + + )} + {nav.subItems && (isExpanded || isHovered || isMobileOpen) && ( +
    { + subMenuRefs.current[`submenu-${index}`] = el + }} + className="overflow-hidden transition-all duration-300" + style={{ + height: + openSubmenu === index + ? `${subMenuHeight[`submenu-${index}`]}px` + : "0px", + }} + > +
      + {nav.subItems.map((subItem) => { + if (subItem.requiredRoles && !subItem.requiredRoles.some(role => user.roles.includes(role as any))) { + return null + } + return ( +
    • + +
    • + ) + })} +
    +
    + )} +
  • + ) + })} +
+ ) + return ( -
- {/* Header */} -
-
- {!isCollapsed && ( -
-

Pecan Experiments

-

Admin Dashboard

+
+
+ +
+ ) } diff --git a/src/components/TopNavbar.tsx b/src/components/TopNavbar.tsx index fb68153..c58c7a0 100644 --- a/src/components/TopNavbar.tsx +++ b/src/components/TopNavbar.tsx @@ -5,9 +5,17 @@ interface TopNavbarProps { user: User onLogout: () => void currentView?: string + onToggleSidebar?: () => void + isSidebarOpen?: boolean } -export function TopNavbar({ user, onLogout, currentView = 'dashboard' }: TopNavbarProps) { +export function TopNavbar({ + user, + onLogout, + currentView = 'dashboard', + onToggleSidebar, + isSidebarOpen = false +}: TopNavbarProps) { const [isUserMenuOpen, setIsUserMenuOpen] = useState(false) const getPageTitle = (view: string) => { @@ -24,6 +32,8 @@ export function TopNavbar({ user, onLogout, currentView = 'dashboard' }: TopNavb return 'Data Entry' case 'vision-system': return 'Vision System' + case 'video-library': + return 'Video Library' default: return 'Dashboard' } @@ -32,110 +42,215 @@ export function TopNavbar({ user, onLogout, currentView = 'dashboard' }: TopNavb const getRoleBadgeColor = (role: string) => { switch (role) { case 'admin': - return 'bg-red-100 text-red-800' + return 'bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500' case 'conductor': - return 'bg-blue-100 text-blue-800' + return 'bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400' case 'analyst': - return 'bg-green-100 text-green-800' + return 'bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-500' case 'data recorder': - return 'bg-purple-100 text-purple-800' + return 'bg-theme-purple-500/10 text-theme-purple-500' default: - return 'bg-gray-100 text-gray-800' + return 'bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80' } } return ( -
-
- {/* Left side - could add breadcrumbs or page title here */} -
-

{getPageTitle(currentView)}

+
+
+
+ + + {/* Page title */} +
+

{getPageTitle(currentView)}

+
+ + {/* Search bar - hidden on mobile, shown on desktop */} +
+ +
+ + + + + + + +
+ +
- {/* Right side - User menu */} -
- {/* User info and avatar */} +
+ {/* User Area */}
{/* Dropdown menu */} {isUserMenuOpen && ( -
-
-
-
- {user.email.charAt(0).toUpperCase()} -
-
-
- {user.email} -
-
- Status: - {user.status} - -
-
-
- - {/* User roles */} -
-
Roles:
-
- {user.roles.map((role) => ( - - {role.charAt(0).toUpperCase() + role.slice(1)} - - ))} -
-
+
+
+ + {user.email.split('@')[0]} + + + {user.email} +
-
- -
+ + + Sign out +
)}
diff --git a/src/components/VisionSystem.tsx b/src/components/VisionSystem.tsx index 2eb02d1..eb000ab 100644 --- a/src/components/VisionSystem.tsx +++ b/src/components/VisionSystem.tsx @@ -618,7 +618,7 @@ export function VisionSystem() { const handleStartRecording = async (cameraName: string) => { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') - const filename = `manual_${cameraName}_${timestamp}.avi` + const filename = `manual_${cameraName}_${timestamp}.mp4` const result = await visionApi.startRecording(cameraName, { filename }) diff --git a/src/features/video-streaming/VideoStreamingPage.tsx b/src/features/video-streaming/VideoStreamingPage.tsx index d5ac8e9..a866b8a 100644 --- a/src/features/video-streaming/VideoStreamingPage.tsx +++ b/src/features/video-streaming/VideoStreamingPage.tsx @@ -50,123 +50,117 @@ export const VideoStreamingPage: React.FC = () => { }; return ( -
+
{/* Header */} -
-
-
-

Video Library

-

- Browse and view recorded videos from your camera system -

-
-
+
+

Video Library

+

+ Browse and view recorded videos from your camera system +

{/* Filters and Controls */} -
-
-
- {/* Camera Filter */} -
- +
+
+ {/* Camera Filter */} +
+ + +
+ + {/* Sort Options */} +
+ +
-
- - {/* Sort Options */} -
- -
- - -
-
- - {/* Date Range Filter */} -
- -
- handleDateRangeChange(e.target.value, filters.dateRange?.end || '')} - className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> - handleDateRangeChange(filters.dateRange?.start || '', e.target.value)} - className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
+
- {/* Clear Filters */} - {(filters.cameraName || filters.dateRange) && ( -
- + {/* Date Range Filter */} +
+ +
+ handleDateRangeChange(e.target.value, filters.dateRange?.end || '')} + className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> + handleDateRangeChange(filters.dateRange?.start || '', e.target.value)} + className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + />
- )} +
- {/* Video List */} - + {/* Clear Filters */} + {(filters.cameraName || filters.dateRange) && ( +
+ +
+ )}
+ {/* Video List */} + + {/* Video Modal */} = ({ + currentPage, + totalPages, + onPageChange, + showFirstLast = true, + showPrevNext = true, + maxVisiblePages = 5, + className = '', +}) => { + // Don't render if there's only one page or no pages + if (totalPages <= 1) { + return null; + } + + // Calculate visible page numbers + const getVisiblePages = (): number[] => { + const pages: number[] = []; + const halfVisible = Math.floor(maxVisiblePages / 2); + + let startPage = Math.max(1, currentPage - halfVisible); + let endPage = Math.min(totalPages, currentPage + halfVisible); + + // Adjust if we're near the beginning or end + if (endPage - startPage + 1 < maxVisiblePages) { + if (startPage === 1) { + endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); + } else { + startPage = Math.max(1, endPage - maxVisiblePages + 1); + } + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; + }; + + const visiblePages = getVisiblePages(); + const isFirstPage = currentPage === 1; + const isLastPage = currentPage === totalPages; + + // Button base classes matching dashboard template + const baseButtonClasses = "inline-flex items-center justify-center px-3 py-2 text-sm font-medium transition rounded-lg border"; + + // Active page button classes + const activeButtonClasses = "bg-brand-500 text-white border-brand-500 hover:bg-brand-600 shadow-theme-xs"; + + // Inactive page button classes + const inactiveButtonClasses = "bg-white text-gray-700 border-gray-300 hover:bg-gray-50"; + + // Disabled button classes + const disabledButtonClasses = "bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed opacity-50"; + + const handlePageClick = (page: number) => { + if (page !== currentPage && page >= 1 && page <= totalPages) { + onPageChange(page); + } + }; + + return ( +
+ {/* First Page Button */} + {showFirstLast && !isFirstPage && ( + + )} + + {/* Previous Page Button */} + {showPrevNext && ( + + )} + + {/* Page Number Buttons */} + {visiblePages.map((page) => ( + + ))} + + {/* Next Page Button */} + {showPrevNext && ( + + )} + + {/* Last Page Button */} + {showFirstLast && !isLastPage && ( + + )} +
+ ); +}; + +// Page info component to show current page and total +export const PageInfo: React.FC<{ + currentPage: number; + totalPages: number; + totalItems: number; + itemsPerPage: number; + className?: string; +}> = ({ currentPage, totalPages, totalItems, itemsPerPage, className = '' }) => { + const startItem = (currentPage - 1) * itemsPerPage + 1; + const endItem = Math.min(currentPage * itemsPerPage, totalItems); + + return ( +
+ Showing {startItem} to {endItem} of {totalItems} results (Page {currentPage} of {totalPages}) +
+ ); +}; diff --git a/src/features/video-streaming/components/VideoCard.tsx b/src/features/video-streaming/components/VideoCard.tsx index e61ac53..c893ee0 100644 --- a/src/features/video-streaming/components/VideoCard.tsx +++ b/src/features/video-streaming/components/VideoCard.tsx @@ -33,8 +33,8 @@ export const VideoCard: React.FC = ({ }; const cardClasses = [ - 'bg-white rounded-lg shadow-md overflow-hidden transition-shadow hover:shadow-lg', - onClick ? 'cursor-pointer' : '', + 'bg-white rounded-xl border border-gray-200 overflow-hidden transition-all hover:shadow-theme-md', + onClick ? 'cursor-pointer hover:border-gray-300' : '', className, ].filter(Boolean).join(' '); @@ -117,7 +117,7 @@ export const VideoCard: React.FC = ({ {/* Metadata (if available and requested) */} {showMetadata && 'metadata' in video && video.metadata && ( -
+
Duration: {Math.round(video.metadata.duration_seconds)}s @@ -136,7 +136,7 @@ export const VideoCard: React.FC = ({ )} {/* Actions */} -
+
{formatVideoDate(video.created_at)}
@@ -147,7 +147,7 @@ export const VideoCard: React.FC = ({ e.stopPropagation(); handleClick(); }} - className="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" + className="inline-flex items-center px-3 py-1.5 text-xs font-medium transition rounded-lg border border-transparent bg-brand-500 text-white hover:bg-brand-600 shadow-theme-xs" > diff --git a/src/features/video-streaming/components/VideoList.tsx b/src/features/video-streaming/components/VideoList.tsx index 9b251ea..8889238 100644 --- a/src/features/video-streaming/components/VideoList.tsx +++ b/src/features/video-streaming/components/VideoList.tsx @@ -8,6 +8,7 @@ import React, { useState, useEffect } from 'react'; import { type VideoListProps, type VideoListFilters, type VideoListSortOptions } from '../types'; import { useVideoList } from '../hooks/useVideoList'; import { VideoCard } from './VideoCard'; +import { Pagination, PageInfo } from './Pagination'; export const VideoList: React.FC = ({ filters, @@ -24,11 +25,16 @@ export const VideoList: React.FC = ({ const { videos, totalCount, + currentPage, + totalPages, loading, error, refetch, loadMore, hasMore, + goToPage, + nextPage, + previousPage, updateFilters, updateSort, } = useVideoList({ @@ -38,6 +44,7 @@ export const VideoList: React.FC = ({ end_date: localFilters.dateRange?.end, limit, include_metadata: true, + page: 1, // Start with page 1 }, autoFetch: true, }); @@ -130,17 +137,22 @@ export const VideoList: React.FC = ({ {/* Results Summary */}
- Showing {videos.length} of {totalCount} videos + {totalPages > 0 ? ( + <>Showing page {currentPage} of {totalPages} ({totalCount} total videos) + ) : ( + <>Showing {videos.length} of {totalCount} videos + )}
@@ -156,37 +168,37 @@ export const VideoList: React.FC = ({ ))}
- {/* Load More Button */} - {hasMore && ( -
- + {/* Pagination */} + {totalPages > 1 && ( +
+ {/* Page Info */} + + + {/* Pagination Controls */} +
)} - {/* Loading Indicator for Additional Videos */} - {loading === 'loading' && videos.length > 0 && ( -
+ {/* Loading Indicator */} + {loading === 'loading' && ( +
-
- Loading more videos... +
+ Loading videos...
)} diff --git a/src/features/video-streaming/components/index.ts b/src/features/video-streaming/components/index.ts index 1a07684..c1c3c77 100644 --- a/src/features/video-streaming/components/index.ts +++ b/src/features/video-streaming/components/index.ts @@ -10,6 +10,7 @@ export { VideoThumbnail } from './VideoThumbnail'; export { VideoCard } from './VideoCard'; export { VideoList } from './VideoList'; export { VideoModal } from './VideoModal'; +export { Pagination, PageInfo } from './Pagination'; // Re-export component prop types for convenience export type { @@ -17,4 +18,5 @@ export type { VideoThumbnailProps, VideoCardProps, VideoListProps, + PaginationProps, } from '../types'; diff --git a/src/features/video-streaming/hooks/useVideoList.ts b/src/features/video-streaming/hooks/useVideoList.ts index 179528c..3722820 100644 --- a/src/features/video-streaming/hooks/useVideoList.ts +++ b/src/features/video-streaming/hooks/useVideoList.ts @@ -19,11 +19,16 @@ import { export interface UseVideoListReturn { videos: VideoFile[]; totalCount: number; + currentPage: number; + totalPages: number; loading: LoadingState; error: VideoError | null; refetch: () => Promise; loadMore: () => Promise; hasMore: boolean; + goToPage: (page: number) => Promise; + nextPage: () => Promise; + previousPage: () => Promise; updateFilters: (filters: VideoListFilters) => void; updateSort: (sortOptions: VideoListSortOptions) => void; clearCache: () => void; @@ -47,6 +52,8 @@ export function useVideoList(options: UseVideoListOptions = {}) { // State const [videos, setVideos] = useState([]); const [totalCount, setTotalCount] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(0); const [loading, setLoading] = useState('idle'); const [error, setError] = useState(null); const [hasMore, setHasMore] = useState(true); @@ -85,7 +92,17 @@ export function useVideoList(options: UseVideoListOptions = {}) { // Update state setVideos(append ? prev => [...prev, ...response.videos] : response.videos); setTotalCount(response.total_count); - setHasMore(response.videos.length === (params.limit || 50)); + + // Update pagination state + if (response.page && response.total_pages) { + setCurrentPage(response.page); + setTotalPages(response.total_pages); + setHasMore(response.has_next || false); + } else { + // Fallback for offset-based pagination + setHasMore(response.videos.length === (params.limit || 50)); + } + setLoading('success'); } catch (err) { @@ -105,14 +122,19 @@ export function useVideoList(options: UseVideoListOptions = {}) { }, [initialParams]); /** - * Refetch videos with initial parameters + * Refetch videos with current page */ const refetch = useCallback(async (): Promise => { - await fetchVideos(initialParams, false); - }, [fetchVideos, initialParams]); + const currentParams = { + ...initialParams, + page: currentPage, + limit: initialParams.limit || 20, + }; + await fetchVideos(currentParams, false); + }, [fetchVideos, initialParams, currentPage]); /** - * Load more videos (pagination) + * Load more videos (pagination) - for backward compatibility */ const loadMore = useCallback(async (): Promise => { if (!hasMore || loading === 'loading') { @@ -124,6 +146,36 @@ export function useVideoList(options: UseVideoListOptions = {}) { await fetchVideos(params, true); }, [hasMore, loading, videos.length, initialParams, fetchVideos]); + /** + * Go to specific page + */ + const goToPage = useCallback(async (page: number): Promise => { + if (page < 1 || (totalPages > 0 && page > totalPages) || loading === 'loading') { + return; + } + + const params = { ...initialParams, page, limit: initialParams.limit || 20 }; + await fetchVideos(params, false); + }, [initialParams, totalPages, loading, fetchVideos]); + + /** + * Go to next page + */ + const nextPage = useCallback(async (): Promise => { + if (currentPage < totalPages) { + await goToPage(currentPage + 1); + } + }, [currentPage, totalPages, goToPage]); + + /** + * Go to previous page + */ + const previousPage = useCallback(async (): Promise => { + if (currentPage > 1) { + await goToPage(currentPage - 1); + } + }, [currentPage, goToPage]); + /** * Update filters and refetch */ @@ -133,6 +185,8 @@ export function useVideoList(options: UseVideoListOptions = {}) { camera_name: filters.cameraName, start_date: filters.dateRange?.start, end_date: filters.dateRange?.end, + page: 1, // Reset to first page when filters change + limit: initialParams.limit || 20, }; fetchVideos(newParams, false); @@ -146,12 +200,22 @@ export function useVideoList(options: UseVideoListOptions = {}) { setVideos(prev => sortVideos(prev, sortOptions.field, sortOptions.direction)); }, []); + /** + * Clear cache (placeholder for future caching implementation) + */ + const clearCache = useCallback((): void => { + // TODO: Implement cache clearing when caching is added + console.log('Cache cleared'); + }, []); + /** * Reset to initial state */ const reset = useCallback((): void => { setVideos([]); setTotalCount(0); + setCurrentPage(1); + setTotalPages(0); setLoading('idle'); setError(null); setHasMore(true); @@ -174,14 +238,21 @@ export function useVideoList(options: UseVideoListOptions = {}) { return { videos, totalCount, + currentPage, + totalPages, loading, error, refetch, loadMore, hasMore, + // Pagination methods + goToPage, + nextPage, + previousPage, // Additional utility methods updateFilters, updateSort, + clearCache, reset, }; } diff --git a/src/features/video-streaming/services/videoApi.ts b/src/features/video-streaming/services/videoApi.ts index f88bac7..e036d9d 100644 --- a/src/features/video-streaming/services/videoApi.ts +++ b/src/features/video-streaming/services/videoApi.ts @@ -90,9 +90,18 @@ export class VideoApiService { */ async getVideos(params: VideoListParams = {}): Promise { try { - const queryString = buildQueryString(params); + // Convert page-based params to offset-based for API compatibility + const apiParams = { ...params }; + + // If page is provided, convert to offset + if (params.page && params.limit) { + apiParams.offset = (params.page - 1) * params.limit; + delete apiParams.page; // Remove page param as API expects offset + } + + const queryString = buildQueryString(apiParams); const url = `${this.baseUrl}/videos/${queryString ? `?${queryString}` : ''}`; - + const response = await fetch(url, { method: 'GET', headers: { @@ -100,7 +109,21 @@ export class VideoApiService { }, }); - return await handleApiResponse(response); + const result = await handleApiResponse(response); + + // Add pagination metadata if page was requested + if (params.page && params.limit) { + const totalPages = Math.ceil(result.total_count / params.limit); + return { + ...result, + page: params.page, + total_pages: totalPages, + has_next: params.page < totalPages, + has_previous: params.page > 1, + }; + } + + return result; } catch (error) { if (error instanceof VideoApiError) { throw error; diff --git a/src/features/video-streaming/types/index.ts b/src/features/video-streaming/types/index.ts index 1a1f0ea..c6738b5 100644 --- a/src/features/video-streaming/types/index.ts +++ b/src/features/video-streaming/types/index.ts @@ -35,6 +35,10 @@ export interface VideoWithMetadata extends VideoFile { export interface VideoListResponse { videos: VideoFile[]; total_count: number; + page?: number; + total_pages?: number; + has_next?: boolean; + has_previous?: boolean; } // API response for video info @@ -66,6 +70,8 @@ export interface VideoListParams { end_date?: string; limit?: number; include_metadata?: boolean; + page?: number; + offset?: number; } // Thumbnail request parameters @@ -122,6 +128,17 @@ export interface VideoListProps { className?: string; } +// Pagination component props +export interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + showFirstLast?: boolean; + showPrevNext?: boolean; + maxVisiblePages?: number; + className?: string; +} + export interface VideoThumbnailProps { fileId: string; timestamp?: number; diff --git a/src/index.css b/src/index.css index 2325ac1..8711221 100644 --- a/src/index.css +++ b/src/index.css @@ -1,15 +1,290 @@ +@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap") layer(base); + @import "tailwindcss"; -/* Reset some default styles that conflict with Tailwind */ -body { - margin: 0; - min-height: 100vh; +@custom-variant dark (&:is(.dark *)); + +@theme { + --font-*: initial; + --font-outfit: Outfit, sans-serif; + + --breakpoint-*: initial; + --breakpoint-2xsm: 375px; + --breakpoint-xsm: 425px; + --breakpoint-3xl: 2000px; + --breakpoint-sm: 640px; + --breakpoint-md: 768px; + --breakpoint-lg: 1024px; + --breakpoint-xl: 1280px; + --breakpoint-2xl: 1536px; + + --text-title-2xl: 72px; + --text-title-2xl--line-height: 90px; + --text-title-xl: 60px; + --text-title-xl--line-height: 72px; + --text-title-lg: 48px; + --text-title-lg--line-height: 60px; + --text-title-md: 36px; + --text-title-md--line-height: 44px; + --text-title-sm: 30px; + --text-title-sm--line-height: 38px; + --text-theme-xl: 20px; + --text-theme-xl--line-height: 30px; + --text-theme-sm: 14px; + --text-theme-sm--line-height: 20px; + --text-theme-xs: 12px; + --text-theme-xs--line-height: 18px; + + --color-current: currentColor; + --color-transparent: transparent; + --color-white: #ffffff; + --color-black: #101828; + + --color-brand-25: #f2f7ff; + --color-brand-50: #ecf3ff; + --color-brand-100: #dde9ff; + --color-brand-200: #c2d6ff; + --color-brand-300: #9cb9ff; + --color-brand-400: #7592ff; + --color-brand-500: #465fff; + --color-brand-600: #3641f5; + --color-brand-700: #2a31d8; + --color-brand-800: #252dae; + --color-brand-900: #262e89; + --color-brand-950: #161950; + + --color-blue-light-25: #f5fbff; + --color-blue-light-50: #f0f9ff; + --color-blue-light-100: #e0f2fe; + --color-blue-light-200: #b9e6fe; + --color-blue-light-300: #7cd4fd; + --color-blue-light-400: #36bffa; + --color-blue-light-500: #0ba5ec; + --color-blue-light-600: #0086c9; + --color-blue-light-700: #026aa2; + --color-blue-light-800: #065986; + --color-blue-light-900: #0b4a6f; + --color-blue-light-950: #062c41; + + --color-gray-25: #fcfcfd; + --color-gray-50: #f9fafb; + --color-gray-100: #f2f4f7; + --color-gray-200: #e4e7ec; + --color-gray-300: #d0d5dd; + --color-gray-400: #98a2b3; + --color-gray-500: #667085; + --color-gray-600: #475467; + --color-gray-700: #344054; + --color-gray-800: #1d2939; + --color-gray-900: #101828; + --color-gray-950: #0c111d; + --color-gray-dark: #1a2231; + + --color-orange-25: #fffaf5; + --color-orange-50: #fff6ed; + --color-orange-100: #ffead5; + --color-orange-200: #fddcab; + --color-orange-300: #feb273; + --color-orange-400: #fd853a; + --color-orange-500: #fb6514; + --color-orange-600: #ec4a0a; + --color-orange-700: #c4320a; + --color-orange-800: #9c2a10; + --color-orange-900: #7e2410; + --color-orange-950: #511c10; + + --color-success-25: #f6fef9; + --color-success-50: #ecfdf3; + --color-success-100: #d1fadf; + --color-success-200: #a6f4c5; + --color-success-300: #6ce9a6; + --color-success-400: #32d583; + --color-success-500: #12b76a; + --color-success-600: #039855; + --color-success-700: #027a48; + --color-success-800: #05603a; + --color-success-900: #054f31; + --color-success-950: #053321; + + --color-error-25: #fffbfa; + --color-error-50: #fef3f2; + --color-error-100: #fee4e2; + --color-error-200: #fecdca; + --color-error-300: #fda29b; + --color-error-400: #f97066; + --color-error-500: #f04438; + --color-error-600: #d92d20; + --color-error-700: #b42318; + --color-error-800: #912018; + --color-error-900: #7a271a; + --color-error-950: #55160c; + + --color-warning-25: #fffcf5; + --color-warning-50: #fffaeb; + --color-warning-100: #fef0c7; + --color-warning-200: #fedf89; + --color-warning-300: #fec84b; + --color-warning-400: #fdb022; + --color-warning-500: #f79009; + --color-warning-600: #dc6803; + --color-warning-700: #b54708; + --color-warning-800: #93370d; + --color-warning-900: #7a2e0e; + --color-warning-950: #4e1d09; + + --color-theme-pink-500: #ee46bc; + + --color-theme-purple-500: #7a5af8; + + --shadow-theme-md: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), + 0px 2px 4px -2px rgba(16, 24, 40, 0.06); + --shadow-theme-lg: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), + 0px 4px 6px -2px rgba(16, 24, 40, 0.03); + --shadow-theme-sm: 0px 1px 3px 0px rgba(16, 24, 40, 0.1), + 0px 1px 2px 0px rgba(16, 24, 40, 0.06); + --shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + --shadow-theme-xl: 0px 20px 24px -4px rgba(16, 24, 40, 0.08), + 0px 8px 8px -4px rgba(16, 24, 40, 0.03); + --shadow-datepicker: -5px 0 0 #262d3c, 5px 0 0 #262d3c; + --shadow-focus-ring: 0px 0px 0px 4px rgba(70, 95, 255, 0.12); + --shadow-slider-navigation: 0px 1px 2px 0px rgba(16, 24, 40, 0.1), + 0px 1px 3px 0px rgba(16, 24, 40, 0.1); + --shadow-tooltip: 0px 4px 6px -2px rgba(16, 24, 40, 0.05), + -8px 0px 20px 8px rgba(16, 24, 40, 0.05); + + --drop-shadow-4xl: 0 35px 35px rgba(0, 0, 0, 0.25), + 0 45px 65px rgba(0, 0, 0, 0.15); + + --z-index-1: 1; + --z-index-9: 9; + --z-index-99: 99; + --z-index-999: 999; + --z-index-9999: 9999; + --z-index-99999: 99999; + --z-index-999999: 999999; } -/* Custom styles that don't conflict with Tailwind */ -:root { - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +/* + The default border color has changed to `currentColor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } + + button:not(:disabled), + [role="button"]:not(:disabled) { + cursor: pointer; + } + + body { + @apply relative font-normal font-outfit z-1 bg-gray-50; + } +} + +@utility menu-item { + @apply relative flex items-center w-full gap-3 px-3 py-2 font-medium rounded-lg text-theme-sm; +} + +@utility menu-item-active { + @apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400; +} + +@utility menu-item-inactive { + @apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300; +} + +@utility menu-item-icon { + @apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400; +} + +@utility menu-item-icon-active { + @apply text-brand-500 dark:text-brand-400; +} + +@utility menu-item-icon-size { + & svg { + @apply !size-6; + } +} + +@utility menu-item-icon-inactive { + @apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300; +} + +@utility menu-item-arrow { + @apply relative; +} + +@utility menu-item-arrow-active { + @apply rotate-180 text-brand-500 dark:text-brand-400; +} + +@utility menu-item-arrow-inactive { + @apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300; +} + +@utility menu-dropdown-item { + @apply relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-theme-sm font-medium; +} + +@utility menu-dropdown-item-active { + @apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400; +} + +@utility menu-dropdown-item-inactive { + @apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5; +} + +@utility menu-dropdown-badge { + @apply block rounded-full px-2.5 py-0.5 text-xs font-medium uppercase text-brand-500 dark:text-brand-400; +} + +@utility menu-dropdown-badge-active { + @apply bg-brand-100 dark:bg-brand-500/20; +} + +@utility menu-dropdown-badge-inactive { + @apply bg-brand-50 group-hover:bg-brand-100 dark:bg-brand-500/15 dark:group-hover:bg-brand-500/20; +} + +@utility no-scrollbar { + + /* Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } + + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +@utility custom-scrollbar { + &::-webkit-scrollbar { + @apply size-1.5; + } + + &::-webkit-scrollbar-track { + @apply rounded-full; + } + + &::-webkit-scrollbar-thumb { + @apply bg-gray-200 rounded-full dark:bg-gray-700; + } +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb { + background-color: #344054; } \ No newline at end of file diff --git a/src/lib/autoRecordingManager.ts b/src/lib/autoRecordingManager.ts index 9597534..a085b48 100644 --- a/src/lib/autoRecordingManager.ts +++ b/src/lib/autoRecordingManager.ts @@ -226,8 +226,8 @@ export class AutoRecordingManager { private async startAutoRecording(cameraName: string, machineName: string): Promise { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') - const filename = `auto_${machineName}_${timestamp}.avi` - + const filename = `auto_${machineName}_${timestamp}.mp4` + const result = await visionApi.startRecording(cameraName, { filename }) if (result.success) { diff --git a/src/utils/videoFileUtils.ts b/src/utils/videoFileUtils.ts index 08d8a4a..7330b36 100644 --- a/src/utils/videoFileUtils.ts +++ b/src/utils/videoFileUtils.ts @@ -136,12 +136,12 @@ export function getRecommendedVideoSettings(useCase: 'production' | 'storage-opt const settings = { production: { video_format: 'mp4', - video_codec: 'mp4v', + video_codec: 'h264', video_quality: 95, }, 'storage-optimized': { video_format: 'mp4', - video_codec: 'mp4v', + video_codec: 'h264', video_quality: 85, }, legacy: { diff --git a/tailwind.config.js b/tailwind.config.js index dca8ba0..bd66d92 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,7 +5,92 @@ export default { "./src/**/*.{js,ts,jsx,tsx}", ], theme: { - extend: {}, + extend: { + colors: { + brand: { + 25: '#f2f7ff', + 50: '#ecf3ff', + 100: '#dde9ff', + 200: '#c2d6ff', + 300: '#9cb9ff', + 400: '#7592ff', + 500: '#465fff', + 600: '#3641f5', + 700: '#2a31d8', + 800: '#252dae', + 900: '#262e89', + 950: '#161950', + }, + gray: { + 25: '#fcfcfd', + 50: '#f9fafb', + 100: '#f2f4f7', + 200: '#e4e7ec', + 300: '#d0d5dd', + 400: '#98a2b3', + 500: '#667085', + 600: '#475467', + 700: '#344054', + 800: '#1d2939', + 900: '#101828', + 950: '#0c111d', + }, + success: { + 25: '#f6fef9', + 50: '#ecfdf3', + 100: '#d1fadf', + 200: '#a6f4c5', + 300: '#6ce9a6', + 400: '#32d583', + 500: '#12b76a', + 600: '#039855', + 700: '#027a48', + 800: '#05603a', + 900: '#054f31', + 950: '#053321', + }, + error: { + 25: '#fffbfa', + 50: '#fef3f2', + 100: '#fee4e2', + 200: '#fecdca', + 300: '#fda29b', + 400: '#f97066', + 500: '#f04438', + 600: '#d92d20', + 700: '#b42318', + 800: '#912018', + 900: '#7a271a', + 950: '#55160c', + }, + warning: { + 25: '#fffcf5', + 50: '#fffaeb', + 100: '#fef0c7', + 200: '#fedf89', + 300: '#fec84b', + 400: '#fdb022', + 500: '#f79009', + 600: '#dc6803', + 700: '#b54708', + 800: '#93370d', + 900: '#7a2e0e', + 950: '#4e1d09', + }, + }, + boxShadow: { + 'theme-xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)', + 'theme-sm': '0px 1px 3px 0px rgba(16, 24, 40, 0.1), 0px 1px 2px 0px rgba(16, 24, 40, 0.06)', + 'theme-md': '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)', + 'theme-lg': '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)', + 'theme-xl': '0px 20px 24px -4px rgba(16, 24, 40, 0.08), 0px 8px 8px -4px rgba(16, 24, 40, 0.03)', + }, + fontSize: { + 'theme-xs': ['12px', '18px'], + 'theme-sm': ['14px', '20px'], + 'theme-xl': ['20px', '30px'], + }, + }, }, plugins: [], }