WIP: integrate-old-refactors-of-github #1

Draft
hdh20267 wants to merge 140 commits from integrate-old-refactors-of-github into main
138 changed files with 672 additions and 18 deletions
Showing only changes of commit 0e53034f30 - Show all commits

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