Enhance camera management features: add debug endpoint for camera manager state, implement live camera routes without authentication, and improve logging for camera initialization and status checks. Update Docker configuration to include environment variables for the web app.

This commit is contained in:
salirezav
2025-09-02 15:31:47 -04:00
parent 62dd0d162b
commit 5bdb070173
138 changed files with 672 additions and 18 deletions

View File

@@ -688,6 +688,29 @@ class APIServer:
except Exception as e:
self.logger.error(f"Failed to add video routes: {e}")
@self.app.get("/debug/camera-manager")
async def debug_camera_manager():
"""Debug endpoint to check camera manager state"""
try:
if not self.camera_manager:
return {"error": "Camera manager not available"}
return {
"available_cameras": len(self.camera_manager.available_cameras),
"camera_recorders": list(self.camera_manager.camera_recorders.keys()),
"camera_streamers": list(self.camera_manager.camera_streamers.keys()),
"streamer_states": {
name: {
"exists": streamer is not None,
"is_streaming": streamer.is_streaming() if streamer else False,
"streaming": getattr(streamer, 'streaming', False) if streamer else False
}
for name, streamer in self.camera_manager.camera_streamers.items()
}
}
except Exception as e:
return {"error": str(e)}
def _setup_event_subscriptions(self):
"""Setup event subscriptions for WebSocket broadcasting"""

View File

@@ -460,12 +460,16 @@ class CameraManager:
def _initialize_streamers(self) -> None:
"""Initialize camera streamers for configured cameras"""
self.logger.info("Starting camera streamer initialization...")
with self._lock:
for camera_config in self.config.cameras:
if not camera_config.enabled:
self.logger.debug(f"Skipping disabled camera: {camera_config.name}")
continue
try:
self.logger.info(f"Initializing streamer for camera: {camera_config.name}")
# Find matching physical camera
device_info = self._find_camera_device(camera_config.name)
if device_info is None:
@@ -481,6 +485,10 @@ class CameraManager:
except Exception as e:
self.logger.error(f"Error initializing streamer for {camera_config.name}: {e}")
import traceback
self.logger.error(f"Traceback: {traceback.format_exc()}")
self.logger.info(f"Camera streamer initialization complete. Created {len(self.camera_streamers)} streamers: {list(self.camera_streamers.keys())}")
def get_camera_streamer(self, camera_name: str) -> Optional[CameraStreamer]:
"""Get camera streamer for a specific camera"""

View File

@@ -172,14 +172,37 @@ class CameraMonitor:
if not device_info:
return "disconnected", "Camera device not found", None
# 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}")
if streamer and streamer.is_streaming():
self.logger.info(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")
return "available", "Camera recording (in use by system)", self._get_device_info_dict(device_info)
# Check if camera is already opened by another process
if mvsdk.CameraIsOpened(device_info):
# Camera is opened - check if it's our recorder that's currently recording
recorder = self.camera_manager.camera_recorders.get(camera_name)
if recorder and recorder.hCamera and recorder.recording:
return "available", "Camera recording (in use by system)", self._get_device_info_dict(device_info)
else:
try:
self.logger.info(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}")
if is_opened:
self.logger.info(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")
# 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}")
# Try to initialize camera briefly to test availability
try:

View File

@@ -28,6 +28,7 @@ class CameraStatus(Enum):
UNKNOWN = "unknown"
AVAILABLE = "available"
BUSY = "busy"
STREAMING = "streaming" # New status for when camera is streaming
ERROR = "error"
DISCONNECTED = "disconnected"

View File

@@ -45,6 +45,8 @@ services:
web:
image: node:20-alpine
working_dir: /app
env_file:
- ./management-dashboard-web-app/.env
volumes:
- ./management-dashboard-web-app:/app
environment:

0
management-dashboard-web-app/.env.example Normal file → Executable file
View File

0
management-dashboard-web-app/.gitignore vendored Normal file → Executable file
View File

0
management-dashboard-web-app/.vscode/extensions.json vendored Normal file → Executable file
View File

View File

View File

View File

@@ -0,0 +1,210 @@
# 🎥 Camera Route Implementation Guide
This document explains the implementation of the new public camera live view routes (`/camera#/live`) that don't require authentication.
## 🚀 What Was Implemented
### 1. **LiveCameraView Component** (`src/components/LiveCameraView.tsx`)
- Displays live camera feed without authentication requirements
- Handles streaming start/stop automatically
- Provides error handling and loading states
- Full-screen live view with camera label and status indicator
### 2. **CameraRoute Component** (`src/components/CameraRoute.tsx`)
- Validates camera route parameters
- Ensures only valid camera numbers (camera1, camera2, etc.) are accepted
- Renders the LiveCameraView for valid routes
### 3. **Updated App.tsx**
- Added route pattern matching for `/camera#/live`
- Integrated camera routes into existing authentication flow
- Maintains backward compatibility with existing functionality
### 4. **Test Page** (`public/camera-test.html`)
- Simple HTML page to test camera routes
- Provides links to test different camera numbers
- Explains expected behavior
## 📋 Required Dependencies
The following packages need to be installed to complete the implementation:
```bash
# Install React Router
npm install react-router-dom
# Install TypeScript types
npm install --save-dev @types/react-router-dom
```
**Note:** Due to permission issues, these packages couldn't be installed automatically. You'll need to resolve the permissions or install them manually.
## 🔧 How to Complete the Setup
### Option 1: Fix Permissions and Install
```bash
# Fix node_modules permissions
sudo chown -R $USER:$USER node_modules
sudo chmod -R 755 node_modules
# Install dependencies
npm install
```
### Option 2: Manual Installation
```bash
# Remove problematic node_modules
rm -rf node_modules
# Reinstall everything
npm install
```
### Option 3: Use Yarn Instead
```bash
# Install yarn if not available
npm install -g yarn
# Install dependencies with yarn
yarn install
```
## 🧪 Testing the Implementation
### 1. **Start the Development Server**
```bash
npm run dev
```
### 2. **Test Camera Routes**
Open these URLs in your browser:
- `http://localhost:5173/camera1/live` - Live view of camera1
- `http://localhost:5173/camera2/live` - Live view of camera2
- `http://localhost:5173/camera3/live` - Live view of camera3
### 3. **Use the Test Page**
Open `http://localhost:5173/camera-test.html` to access the test interface.
### 4. **Expected Behavior**
-**Valid routes** should show live camera feed
-**Invalid routes** should show error message
- 🔒 **Protected routes** should redirect to login
## 🏗️ Architecture Details
### Route Pattern
```
/camera{number}/live
```
- `{number}` must be a positive integer
- Examples: `/camera1/live`, `/camera2/live`, `/camera10/live`
- Invalid: `/camera/live`, `/camera0/live`, `/camera-1/live`
### Component Flow
```
App.tsx → Route Detection → CameraRoute → LiveCameraView
```
### API Integration
The LiveCameraView component integrates with existing camera API endpoints:
- `POST /cameras/{camera_name}/start-stream` - Start streaming
- `GET /cameras/{camera_name}/stream` - Get MJPEG stream
- `POST /cameras/{camera_name}/stop-stream` - Stop streaming
## 🎯 Key Features
### ✅ **Public Access**
- No authentication required
- Anyone can view live camera feeds
- Perfect for monitoring displays
### ✅ **Non-Blocking Streaming**
- Uses existing CameraStreamer infrastructure
- Separate camera connections for streaming vs. recording
- Doesn't interfere with recording operations
### ✅ **Real-time Video**
- MJPEG format for low latency
- Automatic stream management
- Error handling and retry functionality
### ✅ **Responsive Design**
- Full-screen live view
- Camera identification labels
- Live status indicators
## 🔍 Troubleshooting
### Common Issues
#### 1. **Permission Errors During Installation**
```bash
# Fix ownership
sudo chown -R $USER:$USER .
# Fix permissions
sudo chmod -R 755 .
```
#### 2. **Camera Stream Not Loading**
- Check if camera API is running (`http://localhost:8000`)
- Verify camera configuration in `config.compose.json`
- Check browser console for errors
#### 3. **Route Not Working**
- Ensure React app is running
- Check browser console for routing errors
- Verify component imports are correct
#### 4. **TypeScript Errors**
- Install missing type definitions
- Check import paths
- Verify component interfaces
### Debug Steps
1. Check browser console for errors
2. Verify API endpoints are accessible
3. Test camera streaming directly via API
4. Check component rendering in React DevTools
## 🚀 Next Steps
### Immediate
1. Install required dependencies
2. Test basic functionality
3. Verify camera streaming works
### Future Enhancements
1. **Add React Router** for better routing
2. **Implement URL-based navigation** between cameras
3. **Add camera selection interface**
4. **Implement stream quality controls**
5. **Add recording controls** (if needed)
### Production Considerations
1. **Security**: Consider adding rate limiting
2. **Performance**: Optimize for multiple concurrent viewers
3. **Monitoring**: Add analytics and usage tracking
4. **Access Control**: Implement optional authentication if needed
## 📚 Related Documentation
- [Camera API Documentation](../camera-management-api/docs/API_DOCUMENTATION.md)
- [Streaming Guide](../camera-management-api/docs/guides/STREAMING_GUIDE.md)
- [Vision System README](VISION_SYSTEM_README.md)
## 🤝 Support
If you encounter issues:
1. Check the troubleshooting section above
2. Review browser console for error messages
3. Verify camera API is running and accessible
4. Test API endpoints directly with curl or Postman
---
**Implementation Status**: ✅ Components Created | ⚠️ Dependencies Pending | <20><> Ready for Testing

0
management-dashboard-web-app/README.md Normal file → Executable file
View File

0
management-dashboard-web-app/VISION_SYSTEM_README.md Normal file → Executable file
View File

0
management-dashboard-web-app/api-endpoints.http Normal file → Executable file
View File

View File

View File

View File

0
management-dashboard-web-app/eslint.config.js Normal file → Executable file
View File

0
management-dashboard-web-app/index.html Normal file → Executable file
View File

75
management-dashboard-web-app/package-lock.json generated Normal file → Executable file
View File

@@ -13,19 +13,21 @@
"@tailwindcss/vite": "^4.1.11",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^6.28.0",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"typescript-eslint": "^8.28.1",
"vite": "^7.0.4"
}
},
@@ -1054,6 +1056,15 @@
"node": ">= 8"
}
},
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
@@ -1709,6 +1720,13 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1751,6 +1769,29 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -3562,6 +3603,38 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",

8
management-dashboard-web-app/package.json Normal file → Executable file
View File

@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint": "eslint",
"preview": "vite preview"
},
"dependencies": {
@@ -15,19 +15,21 @@
"@tailwindcss/vite": "^4.1.11",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^6.28.0",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"typescript-eslint": "^8.28.1",
"vite": "^7.0.4"
}
}
}

View File

View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Camera Route Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.test-links {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.test-links h2 {
color: #333;
margin-top: 0;
}
.test-links a {
display: inline-block;
margin: 10px 10px 10px 0;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s;
}
.test-links a:hover {
background-color: #0056b3;
}
.info {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
}
.info h3 {
margin-top: 0;
color: #0066cc;
}
.note {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1>🎥 Camera Route Test</h1>
<div class="info">
<h3>Test the New Camera Routes</h3>
<p>This page helps you test the new camera live view routes that don't require authentication.</p>
<p><strong>Note:</strong> Make sure the React app is running and the camera API is accessible.</p>
</div>
<div class="test-links">
<h2>Test Camera Routes</h2>
<p>Click the links below to test different camera routes:</p>
<a href="/camera1/live" target="_blank">Camera 1 Live View</a>
<a href="/camera2/live" target="_blank">Camera 2 Live View</a>
<a href="/camera3/live" target="_blank">Camera 3 Live View</a>
<a href="/camera10/live" target="_blank">Camera 10 Live View</a>
</div>
<div class="note">
<h3>Expected Behavior</h3>
<ul>
<li><strong>Valid routes</strong> (like /camera1/live) should show the live camera feed</li>
<li><strong>Invalid routes</strong> (like /camera/live) should show an error message</li>
<li>🔒 <strong>Protected routes</strong> (like /) should redirect to login if not authenticated</li>
</ul>
</div>
<div class="info">
<h3>API Endpoints</h3>
<p>The camera routes use these backend API endpoints:</p>
<ul>
<li><code>POST /cameras/{camera_name}/start-stream</code> - Start streaming</li>
<li><code>GET /cameras/{camera_name}/stream</code> - Get MJPEG stream</li>
<li><code>POST /cameras/{camera_name}/stop-stream</code> - Stop streaming</li>
</ul>
</div>
<script>
// Add click tracking for analytics
document.querySelectorAll('.test-links a').forEach(link => {
link.addEventListener('click', function () {
console.log('Testing camera route:', this.href);
});
});
</script>
</body>
</html>

0
management-dashboard-web-app/public/vite.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

0
management-dashboard-web-app/src/App.css Normal file → Executable file
View File

21
management-dashboard-web-app/src/App.tsx Normal file → Executable file
View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { supabase } from './lib/supabase'
import { Login } from './components/Login'
import { Dashboard } from './components/Dashboard'
import { CameraRoute } from './components/CameraRoute'
function App() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null)
@@ -84,6 +85,18 @@ function App() {
}
}
// Check if current route is a camera live route
const isCameraLiveRoute = (route: string) => {
const cameraRoutePattern = /^\/camera(\d+)\/live$/
return cameraRoutePattern.test(route)
}
// Extract camera number from route
const getCameraNumber = (route: string) => {
const match = route.match(/^\/camera(\d+)\/live$/)
return match ? `camera${match[1]}` : null
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
@@ -107,6 +120,14 @@ function App() {
)
}
// Handle camera live routes (no authentication required)
if (isCameraLiveRoute(currentRoute)) {
const cameraNumber = getCameraNumber(currentRoute)
if (cameraNumber) {
return <CameraRoute cameraNumber={cameraNumber} />
}
}
return (
<>
{isAuthenticated ? (

0
management-dashboard-web-app/src/assets/react.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

View File

View File

View File

View File

@@ -0,0 +1,25 @@
import { LiveCameraView } from './LiveCameraView'
interface CameraRouteProps {
cameraNumber: string
}
export function CameraRoute({ cameraNumber }: CameraRouteProps) {
// Validate camera number (only allow camera1, camera2, etc.)
if (!cameraNumber || !/^camera\d+$/.test(cameraNumber)) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-center text-white">
<h1 className="text-2xl font-bold mb-4">Invalid Camera</h1>
<p className="text-gray-300">Camera number must be in format: camera1, camera2, etc.</p>
</div>
</div>
)
}
return <LiveCameraView cameraName={cameraNumber} />
}

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,134 @@
import { useState, useEffect, useRef } from 'react'
interface LiveCameraViewProps {
cameraName: string
}
export function LiveCameraView({ cameraName }: LiveCameraViewProps) {
const [isStreaming, setIsStreaming] = useState(false)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const imgRef = useRef<HTMLImageElement>(null)
const API_BASE = import.meta.env.VITE_VISION_API_URL || 'http://localhost:8000'
useEffect(() => {
startStreaming()
return () => stopStreaming()
}, [cameraName])
const startStreaming = async () => {
try {
setLoading(true)
setError(null)
// Start the stream
const response = await fetch(`${API_BASE}/cameras/${cameraName}/start-stream`, {
method: 'POST'
})
if (response.ok) {
setIsStreaming(true)
// Set the stream source with timestamp to prevent caching
if (imgRef.current) {
imgRef.current.src = `${API_BASE}/cameras/${cameraName}/stream?t=${Date.now()}`
}
} else {
throw new Error(`Failed to start stream: ${response.statusText}`)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to start stream'
setError(errorMessage)
} finally {
setLoading(false)
}
}
const stopStreaming = async () => {
try {
if (isStreaming) {
await fetch(`${API_BASE}/cameras/${cameraName}/stop-stream`, {
method: 'POST'
})
setIsStreaming(false)
}
} catch (err) {
console.error('Error stopping stream:', err)
}
}
const handleImageError = () => {
setError('Failed to load camera stream')
}
const handleImageLoad = () => {
setError(null)
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-center text-white">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto"></div>
<p className="mt-4">Starting camera stream...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-center text-white">
<div className="bg-red-600 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">Stream Error</h2>
<p className="text-gray-300 mb-4">{error}</p>
<button
onClick={startStreaming}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md"
>
Retry
</button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<div className="relative">
{/* Camera Label */}
<div className="absolute top-4 left-4 z-10">
<div className="bg-black bg-opacity-75 text-white px-3 py-1 rounded-md text-sm font-medium">
{cameraName} - Live View
</div>
</div>
{/* Live Stream */}
<img
ref={imgRef}
alt={`Live stream from ${cameraName}`}
className="max-w-full max-h-screen object-contain"
onError={handleImageError}
onLoad={handleImageLoad}
/>
{/* Status Indicator */}
<div className="absolute bottom-4 right-4 z-10">
<div className="flex items-center space-x-2 bg-black bg-opacity-75 text-white px-3 py-1 rounded-md">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-sm">LIVE</span>
</div>
</div>
</div>
</div>
)
}

0
management-dashboard-web-app/src/components/Login.tsx Normal file → Executable file
View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -196,9 +196,10 @@ const CamerasStatus = memo(({
const hasSerial = !!camera.device_info?.serial_number
// Determine if camera is connected based on status
const isConnected = camera.status === 'available' || camera.status === 'connected'
const isConnected = camera.status === 'available' || camera.status === 'connected' || camera.status === 'streaming'
const hasError = camera.status === 'error'
const statusText = camera.status || 'unknown'
const isStreaming = camera.status === 'streaming'
return (
<div key={cameraName} className="border border-gray-200 rounded-lg p-4">
@@ -209,11 +210,12 @@ const CamerasStatus = memo(({
<span className="text-gray-500 text-sm font-normal ml-2">({cameraName})</span>
)}
</h4>
<div className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${isConnected ? 'bg-green-100 text-green-800' :
hasError ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
<div className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${isStreaming ? 'bg-blue-100 text-blue-800' :
isConnected ? 'bg-green-100 text-green-800' :
hasError ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{isConnected ? 'Connected' : hasError ? 'Error' : 'Disconnected'}
{isStreaming ? 'Streaming' : isConnected ? 'Connected' : hasError ? 'Error' : 'Disconnected'}
</div>
</div>
@@ -224,7 +226,8 @@ const CamerasStatus = memo(({
hasError ? 'text-yellow-600' :
'text-red-600'
}`}>
{statusText.charAt(0).toUpperCase() + statusText.slice(1)}
{isStreaming ? 'Streaming' :
statusText.charAt(0).toUpperCase() + statusText.slice(1)}
</span>
</div>
@@ -238,6 +241,16 @@ const CamerasStatus = memo(({
</div>
)}
{isStreaming && (
<div className="flex justify-between">
<span className="text-gray-500">Streaming:</span>
<span className="text-blue-600 font-medium flex items-center">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"></div>
Live
</span>
</div>
)}
{hasDeviceInfo && (
<>
{camera.device_info.model && (
@@ -923,7 +936,7 @@ export function VisionSystem() {
{/* Notification */}
{notification && (
<div className={`fixed top-4 right-4 z-50 p-4 rounded-md shadow-lg ${notification.type === 'success'
<div className={`fixed top-4 right-4 z-[999999] p-4 rounded-md shadow-lg ${notification.type === 'success'
? 'bg-green-50 border border-green-200 text-green-800'
: 'bg-red-50 border border-red-200 text-red-800'
}`}>

View File

Some files were not shown because too many files have changed in this diff Show More