mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-29 07:09:03 +03:00
Compare commits
7
Commits
dev
...
fdc621e3e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc621e3e9 | ||
|
|
4a22448408 | ||
|
|
f0271a802f | ||
|
|
e2128e09ae | ||
|
|
9e7ad7a9bd | ||
|
|
58d66bf17a | ||
|
|
14318357c6 |
@@ -1,439 +0,0 @@
|
|||||||
# GitHub Copilot Instructions for Frigate NVR
|
|
||||||
|
|
||||||
This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Frigate NVR is a realtime object detection system for IP cameras that uses:
|
|
||||||
|
|
||||||
- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX
|
|
||||||
- **Frontend**: React with TypeScript, Vite, TailwindCSS
|
|
||||||
- **Architecture**: Multiprocessing design with ZMQ and MQTT communication
|
|
||||||
- **Focus**: Minimal resource usage with maximum performance
|
|
||||||
|
|
||||||
## Code Review Guidelines
|
|
||||||
|
|
||||||
When reviewing code, do NOT comment on:
|
|
||||||
|
|
||||||
- Missing imports - Static analysis tooling catches these
|
|
||||||
- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting
|
|
||||||
- Minor style inconsistencies already enforced by linters
|
|
||||||
|
|
||||||
## Python Backend Standards
|
|
||||||
|
|
||||||
### Python Requirements
|
|
||||||
|
|
||||||
- **Compatibility**: Python 3.13+
|
|
||||||
- **Language Features**: Use modern Python features:
|
|
||||||
- Pattern matching
|
|
||||||
- Type hints (comprehensive typing preferred)
|
|
||||||
- f-strings (preferred over `%` or `.format()`)
|
|
||||||
- Dataclasses
|
|
||||||
- Async/await patterns
|
|
||||||
|
|
||||||
### Code Quality Standards
|
|
||||||
|
|
||||||
- **Formatting**: Ruff (configured in `pyproject.toml`)
|
|
||||||
- **Linting**: Ruff with rules defined in project config
|
|
||||||
- **Type Checking**: Use type hints consistently
|
|
||||||
- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests
|
|
||||||
- **Language**: American English for all code, comments, and documentation
|
|
||||||
|
|
||||||
### Logging Standards
|
|
||||||
|
|
||||||
- **Logger Pattern**: Use module-level logger
|
|
||||||
|
|
||||||
```python
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Format Guidelines**:
|
|
||||||
- No periods at end of log messages
|
|
||||||
- No sensitive data (keys, tokens, passwords)
|
|
||||||
- Use lazy logging: `logger.debug("Message with %s", variable)`
|
|
||||||
- **Log Levels**:
|
|
||||||
- `debug`: Development and troubleshooting information
|
|
||||||
- `info`: Important runtime events (startup, shutdown, state changes)
|
|
||||||
- `warning`: Recoverable issues that should be addressed
|
|
||||||
- `error`: Errors that affect functionality but don't crash the app
|
|
||||||
- `exception`: Use in except blocks to include traceback
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- **Exception Types**: Choose most specific exception available
|
|
||||||
- **Try/Catch Best Practices**:
|
|
||||||
- Only wrap code that can throw exceptions
|
|
||||||
- Keep try blocks minimal - process data after the try/except
|
|
||||||
- Avoid bare exceptions except in background tasks
|
|
||||||
|
|
||||||
Bad pattern:
|
|
||||||
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
data = await device.get_data() # Can throw
|
|
||||||
# ❌ Don't process data inside try block
|
|
||||||
processed = data.get("value", 0) * 100
|
|
||||||
result = processed
|
|
||||||
except DeviceError:
|
|
||||||
logger.error("Failed to get data")
|
|
||||||
```
|
|
||||||
|
|
||||||
Good pattern:
|
|
||||||
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
data = await device.get_data() # Can throw
|
|
||||||
except DeviceError:
|
|
||||||
logger.error("Failed to get data")
|
|
||||||
return
|
|
||||||
|
|
||||||
# ✅ Process data outside try block
|
|
||||||
processed = data.get("value", 0) * 100
|
|
||||||
result = processed
|
|
||||||
```
|
|
||||||
|
|
||||||
### Async Programming
|
|
||||||
|
|
||||||
- **External I/O**: All external I/O operations must be async
|
|
||||||
- **Best Practices**:
|
|
||||||
- Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()`
|
|
||||||
- Avoid awaiting in loops - use `asyncio.gather()` instead
|
|
||||||
- No blocking calls in async functions
|
|
||||||
- Use `asyncio.create_task()` for background operations
|
|
||||||
- **Thread Safety**: Use proper synchronization for shared state
|
|
||||||
|
|
||||||
### Documentation Standards
|
|
||||||
|
|
||||||
- **Module Docstrings**: Concise descriptions at top of files
|
|
||||||
```python
|
|
||||||
"""Utilities for motion detection and analysis."""
|
|
||||||
```
|
|
||||||
- **Function Docstrings**: Required for public functions and methods
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def process_frame(frame: ndarray, config: Config) -> Detection:
|
|
||||||
"""Process a video frame for object detection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The video frame as numpy array
|
|
||||||
config: Detection configuration
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Detection results with bounding boxes
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Comment Style**:
|
|
||||||
- Explain the "why" not just the "what"
|
|
||||||
- Keep lines under 88 characters when possible
|
|
||||||
- Use clear, descriptive comments
|
|
||||||
|
|
||||||
### File Organization
|
|
||||||
|
|
||||||
- **API Endpoints**: `frigate/api/` - FastAPI route handlers
|
|
||||||
- **Configuration**: `frigate/config/` - Configuration parsing and validation
|
|
||||||
- **Detectors**: `frigate/detectors/` - Object detection backends
|
|
||||||
- **Events**: `frigate/events/` - Event management and storage
|
|
||||||
- **Utilities**: `frigate/util/` - Shared utility functions
|
|
||||||
|
|
||||||
## Frontend (React/TypeScript) Standards
|
|
||||||
|
|
||||||
### Internationalization (i18n)
|
|
||||||
|
|
||||||
- **CRITICAL**: Never write user-facing strings directly in components
|
|
||||||
- **Always use react-i18next**: Import and use the `t()` function
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const { t } = useTranslation(["views/live"]);
|
|
||||||
return <div>{t("camera_not_found")}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en`
|
|
||||||
- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`)
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
|
|
||||||
- **Linting**: ESLint (see `web/.eslintrc.cjs`)
|
|
||||||
- **Formatting**: Prettier with Tailwind CSS plugin
|
|
||||||
- **Type Safety**: TypeScript strict mode enabled
|
|
||||||
|
|
||||||
### Component Patterns
|
|
||||||
|
|
||||||
- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`)
|
|
||||||
- **Styling**: TailwindCSS with `cn()` utility for class merging
|
|
||||||
- **State Management**: React hooks (useState, useEffect, useCallback, useMemo)
|
|
||||||
- **Data Fetching**: Custom hooks with proper loading and error states
|
|
||||||
|
|
||||||
### ESLint Rules
|
|
||||||
|
|
||||||
Key rules enforced:
|
|
||||||
|
|
||||||
- `react-hooks/rules-of-hooks`: error
|
|
||||||
- `react-hooks/exhaustive-deps`: error
|
|
||||||
- `no-console`: error (use proper logging or remove)
|
|
||||||
- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`)
|
|
||||||
- Unused variables must be prefixed with `_`
|
|
||||||
- Comma dangles required for multiline objects/arrays
|
|
||||||
|
|
||||||
### File Organization
|
|
||||||
|
|
||||||
- **Pages**: `web/src/pages/` - Route components
|
|
||||||
- **Views**: `web/src/views/` - Complex view components
|
|
||||||
- **Components**: `web/src/components/` - Reusable components
|
|
||||||
- **Hooks**: `web/src/hooks/` - Custom React hooks
|
|
||||||
- **API**: `web/src/api/` - API client functions
|
|
||||||
- **Types**: `web/src/types/` - TypeScript type definitions
|
|
||||||
|
|
||||||
## Testing Requirements
|
|
||||||
|
|
||||||
### Backend Testing
|
|
||||||
|
|
||||||
- **Framework**: Python unittest
|
|
||||||
- **Run Command**: `python3 -u -m unittest`
|
|
||||||
- **Location**: `frigate/test/`
|
|
||||||
- **Coverage**: Aim for comprehensive test coverage of core functionality
|
|
||||||
- **Pattern**: Use `TestCase` classes with descriptive test method names
|
|
||||||
```python
|
|
||||||
class TestMotionDetection(unittest.TestCase):
|
|
||||||
def test_detects_motion_above_threshold(self):
|
|
||||||
# Test implementation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Best Practices
|
|
||||||
|
|
||||||
- Always have a way to test your work and confirm your changes
|
|
||||||
- Write tests for bug fixes to prevent regressions
|
|
||||||
- Test edge cases and error conditions
|
|
||||||
- Mock external dependencies (cameras, APIs, hardware)
|
|
||||||
- Use fixtures for test data
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
### Python Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
python3 -u -m unittest
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
python3 -u -m unittest frigate.test.test_ffmpeg_presets
|
|
||||||
|
|
||||||
# Check formatting (Ruff)
|
|
||||||
ruff format --check frigate/
|
|
||||||
|
|
||||||
# Apply formatting
|
|
||||||
ruff format frigate/
|
|
||||||
|
|
||||||
# Run linter
|
|
||||||
ruff check frigate/
|
|
||||||
|
|
||||||
# Type check
|
|
||||||
python3 -u -m mypy --config-file frigate/mypy.ini frigate
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend (from web/ directory)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start dev server (AI agents should never run this directly unless asked)
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Run linter
|
|
||||||
npm run lint
|
|
||||||
|
|
||||||
# Fix linting issues
|
|
||||||
npm run lint:fix
|
|
||||||
|
|
||||||
# Format code
|
|
||||||
npm run prettier:write
|
|
||||||
|
|
||||||
# E2E: first-time setup
|
|
||||||
npm install
|
|
||||||
npx playwright install chromium
|
|
||||||
|
|
||||||
# E2E: build the app and run all tests
|
|
||||||
npm run e2e:build && npm run e2e
|
|
||||||
|
|
||||||
# E2E: interactive UI for debugging
|
|
||||||
npm run e2e:ui
|
|
||||||
|
|
||||||
# E2E: run a specific spec
|
|
||||||
npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts
|
|
||||||
|
|
||||||
# E2E: filter by name, or run only desktop/mobile
|
|
||||||
npx playwright test --config e2e/playwright.config.ts --grep="severity tab"
|
|
||||||
npx playwright test --config e2e/playwright.config.ts --project=desktop
|
|
||||||
|
|
||||||
# E2E: regenerate mock data after backend model changes (from repo root)
|
|
||||||
PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py
|
|
||||||
|
|
||||||
# Regenerate config translations from Pydantic models — outputs to
|
|
||||||
# web/public/locales/en/config/{global,cameras}.json. NEVER edit those
|
|
||||||
# JSON files by hand; change the Pydantic field title/description and
|
|
||||||
# re-run this script. (from repo root)
|
|
||||||
python3 generate_config_translations.py
|
|
||||||
|
|
||||||
# Extract i18n keys from source into the locale files after adding
|
|
||||||
# new t() calls. Use the :ci variant to verify the locale files are
|
|
||||||
# in sync with source (fails if extraction would change anything).
|
|
||||||
npm run i18n:extract
|
|
||||||
npm run i18n:extract:ci
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Development
|
|
||||||
|
|
||||||
AI agents should never run these commands directly unless instructed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build local image
|
|
||||||
make local
|
|
||||||
|
|
||||||
# Build debug image
|
|
||||||
make debug
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### API Endpoint Pattern
|
|
||||||
|
|
||||||
```python
|
|
||||||
from fastapi import APIRouter, Request
|
|
||||||
from frigate.api.defs.tags import Tags
|
|
||||||
|
|
||||||
router = APIRouter(tags=[Tags.Events])
|
|
||||||
|
|
||||||
@router.get("/events")
|
|
||||||
async def get_events(request: Request, limit: int = 100):
|
|
||||||
"""Retrieve events from the database."""
|
|
||||||
# Implementation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration Access
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Access Frigate configuration
|
|
||||||
config: FrigateConfig = request.app.frigate_config
|
|
||||||
camera_config = config.cameras["front_door"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Queries
|
|
||||||
|
|
||||||
```python
|
|
||||||
from frigate.models import Event
|
|
||||||
|
|
||||||
# Use Peewee ORM for database access
|
|
||||||
events = (
|
|
||||||
Event.select()
|
|
||||||
.where(Event.camera == camera_name)
|
|
||||||
.order_by(Event.start_time.desc())
|
|
||||||
.limit(limit)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Anti-Patterns to Avoid
|
|
||||||
|
|
||||||
### ❌ Avoid These
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Blocking operations in async functions
|
|
||||||
data = requests.get(url) # ❌ Use async HTTP client
|
|
||||||
time.sleep(5) # ❌ Use asyncio.sleep()
|
|
||||||
|
|
||||||
# Hardcoded strings in React components
|
|
||||||
<div>Camera not found</div> # ❌ Use t("camera_not_found")
|
|
||||||
|
|
||||||
# Missing error handling
|
|
||||||
data = await api.get_data() # ❌ No exception handling
|
|
||||||
|
|
||||||
# Bare exceptions in regular code
|
|
||||||
try:
|
|
||||||
value = await sensor.read()
|
|
||||||
except Exception: # ❌ Too broad
|
|
||||||
logger.error("Failed")
|
|
||||||
|
|
||||||
# Returning exceptions in JSON responses
|
|
||||||
except ValueError as e:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": str(e)},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### ✅ Use These Instead
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Async operations
|
|
||||||
import aiohttp
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(url) as response:
|
|
||||||
data = await response.json()
|
|
||||||
|
|
||||||
await asyncio.sleep(5) # ✅ Non-blocking
|
|
||||||
|
|
||||||
# Translatable strings in React
|
|
||||||
const { t } = useTranslation();
|
|
||||||
<div>{t("camera_not_found")}</div> # ✅ Translatable
|
|
||||||
|
|
||||||
# Proper error handling
|
|
||||||
try:
|
|
||||||
data = await api.get_data()
|
|
||||||
except ApiException as err:
|
|
||||||
logger.error("API error: %s", err)
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Specific exceptions
|
|
||||||
try:
|
|
||||||
value = await sensor.read()
|
|
||||||
except SensorException as err: # ✅ Specific
|
|
||||||
logger.exception("Failed to read sensor")
|
|
||||||
|
|
||||||
# Safe error responses
|
|
||||||
except ValueError:
|
|
||||||
logger.exception("Invalid parameters for API request")
|
|
||||||
return JSONResponse(
|
|
||||||
content={
|
|
||||||
"success": False,
|
|
||||||
"message": "Invalid request parameters",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebSocket Broadcasts
|
|
||||||
|
|
||||||
Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear.
|
|
||||||
|
|
||||||
## Project-Specific Conventions
|
|
||||||
|
|
||||||
### Configuration Files
|
|
||||||
|
|
||||||
- Main config: `config/config.yml`
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
|
|
||||||
- Backend code: `frigate/`
|
|
||||||
- Frontend code: `web/`
|
|
||||||
- Docker files: `docker/`
|
|
||||||
- Documentation: `docs/`
|
|
||||||
- Database migrations: `migrations/`
|
|
||||||
|
|
||||||
### Code Style Conformance
|
|
||||||
|
|
||||||
Always conform new and refactored code to the existing coding style in the project:
|
|
||||||
|
|
||||||
- Follow established patterns in similar files
|
|
||||||
- Match indentation and formatting of surrounding code
|
|
||||||
- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript)
|
|
||||||
- Maintain the same level of verbosity in comments and docstrings
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- Documentation: https://docs.frigate.video
|
|
||||||
- Main Repository: https://github.com/blakeblackshear/frigate
|
|
||||||
- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
AGENTS.md
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
# Agent Instructions for Frigate NVR
|
||||||
|
|
||||||
|
This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Frigate NVR is a realtime object detection system for IP cameras that uses:
|
||||||
|
|
||||||
|
- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX
|
||||||
|
- **Frontend**: React with TypeScript, Vite, TailwindCSS
|
||||||
|
- **Architecture**: Multiprocessing design with ZMQ and MQTT communication
|
||||||
|
- **Focus**: Minimal resource usage with maximum performance
|
||||||
|
|
||||||
|
## Code Review Guidelines
|
||||||
|
|
||||||
|
When reviewing code, do NOT comment on:
|
||||||
|
|
||||||
|
- Missing imports - Static analysis tooling catches these
|
||||||
|
- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting
|
||||||
|
- Minor style inconsistencies already enforced by linters
|
||||||
|
|
||||||
|
## Python Backend Standards
|
||||||
|
|
||||||
|
### Python Requirements
|
||||||
|
|
||||||
|
- **Compatibility**: Python 3.13+
|
||||||
|
- **Language Features**: Use modern Python features:
|
||||||
|
- Pattern matching
|
||||||
|
- Type hints (comprehensive typing preferred)
|
||||||
|
- f-strings (preferred over `%` or `.format()`)
|
||||||
|
- Dataclasses
|
||||||
|
- Async/await patterns
|
||||||
|
|
||||||
|
### Code Quality Standards
|
||||||
|
|
||||||
|
- **Formatting**: Ruff (configured in `pyproject.toml`)
|
||||||
|
- **Linting**: Ruff with rules defined in project config
|
||||||
|
- **Type Checking**: Use type hints consistently
|
||||||
|
- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests
|
||||||
|
- **Language**: American English for all code, comments, and documentation
|
||||||
|
|
||||||
|
### Logging Standards
|
||||||
|
|
||||||
|
- **Logger Pattern**: Use module-level logger
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Format Guidelines**:
|
||||||
|
- No periods at end of log messages
|
||||||
|
- No sensitive data (keys, tokens, passwords)
|
||||||
|
- Use lazy logging: `logger.debug("Message with %s", variable)`
|
||||||
|
- **Log Levels**:
|
||||||
|
- `debug`: Development and troubleshooting information
|
||||||
|
- `info`: Important runtime events (startup, shutdown, state changes)
|
||||||
|
- `warning`: Recoverable issues that should be addressed
|
||||||
|
- `error`: Errors that affect functionality but don't crash the app
|
||||||
|
- `exception`: Use in except blocks to include traceback
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- **Exception Types**: Choose most specific exception available
|
||||||
|
- **Try/Catch Best Practices**:
|
||||||
|
- Only wrap code that can throw exceptions
|
||||||
|
- Keep try blocks minimal - process data after the try/except
|
||||||
|
- Avoid bare exceptions except in background tasks
|
||||||
|
|
||||||
|
Bad pattern:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
data = await device.get_data() # Can throw
|
||||||
|
# ❌ Don't process data inside try block
|
||||||
|
processed = data.get("value", 0) * 100
|
||||||
|
result = processed
|
||||||
|
except DeviceError:
|
||||||
|
logger.error("Failed to get data")
|
||||||
|
```
|
||||||
|
|
||||||
|
Good pattern:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
data = await device.get_data() # Can throw
|
||||||
|
except DeviceError:
|
||||||
|
logger.error("Failed to get data")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ✅ Process data outside try block
|
||||||
|
processed = data.get("value", 0) * 100
|
||||||
|
result = processed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Programming
|
||||||
|
|
||||||
|
- **External I/O**: All external I/O operations must be async
|
||||||
|
- **Best Practices**:
|
||||||
|
- Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()`
|
||||||
|
- Avoid awaiting in loops - use `asyncio.gather()` instead
|
||||||
|
- No blocking calls in async functions
|
||||||
|
- Use `asyncio.create_task()` for background operations
|
||||||
|
- **Thread Safety**: Use proper synchronization for shared state
|
||||||
|
|
||||||
|
### Documentation Standards
|
||||||
|
|
||||||
|
- **Module Docstrings**: Concise descriptions at top of files
|
||||||
|
```python
|
||||||
|
"""Utilities for motion detection and analysis."""
|
||||||
|
```
|
||||||
|
- **Function Docstrings**: Required for public functions and methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def process_frame(frame: ndarray, config: Config) -> Detection:
|
||||||
|
"""Process a video frame for object detection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The video frame as numpy array
|
||||||
|
config: Detection configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Detection results with bounding boxes
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Comment Style**:
|
||||||
|
- Explain the "why" not just the "what"
|
||||||
|
- Keep lines under 88 characters when possible
|
||||||
|
- Use clear, descriptive comments
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
|
||||||
|
- **API Endpoints**: `frigate/api/` - FastAPI route handlers
|
||||||
|
- **Configuration**: `frigate/config/` - Configuration parsing and validation
|
||||||
|
- **Detectors**: `frigate/detectors/` - Object detection backends
|
||||||
|
- **Events**: `frigate/events/` - Event management and storage
|
||||||
|
- **Utilities**: `frigate/util/` - Shared utility functions
|
||||||
|
|
||||||
|
## Frontend (React/TypeScript) Standards
|
||||||
|
|
||||||
|
### Internationalization (i18n)
|
||||||
|
|
||||||
|
- **CRITICAL**: Never write user-facing strings directly in components
|
||||||
|
- **Always use react-i18next**: Import and use the `t()` function
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { t } = useTranslation(["views/live"]);
|
||||||
|
return <div>{t("camera_not_found")}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en`
|
||||||
|
- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`)
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- **Linting**: ESLint (see `web/.eslintrc.cjs`)
|
||||||
|
- **Formatting**: Prettier with Tailwind CSS plugin
|
||||||
|
- **Type Safety**: TypeScript strict mode enabled
|
||||||
|
|
||||||
|
### Component Patterns
|
||||||
|
|
||||||
|
- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`)
|
||||||
|
- **Styling**: TailwindCSS with `cn()` utility for class merging
|
||||||
|
- **State Management**: React hooks (useState, useEffect, useCallback, useMemo)
|
||||||
|
- **Data Fetching**: Custom hooks with proper loading and error states
|
||||||
|
|
||||||
|
### ESLint Rules
|
||||||
|
|
||||||
|
Key rules enforced:
|
||||||
|
|
||||||
|
- `react-hooks/rules-of-hooks`: error
|
||||||
|
- `react-hooks/exhaustive-deps`: error
|
||||||
|
- `no-console`: error (use proper logging or remove)
|
||||||
|
- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`)
|
||||||
|
- Unused variables must be prefixed with `_`
|
||||||
|
- Comma dangles required for multiline objects/arrays
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
|
||||||
|
- **Pages**: `web/src/pages/` - Route components
|
||||||
|
- **Views**: `web/src/views/` - Complex view components
|
||||||
|
- **Components**: `web/src/components/` - Reusable components
|
||||||
|
- **Hooks**: `web/src/hooks/` - Custom React hooks
|
||||||
|
- **API**: `web/src/api/` - API client functions
|
||||||
|
- **Types**: `web/src/types/` - TypeScript type definitions
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
### Backend Testing
|
||||||
|
|
||||||
|
- **Framework**: Python unittest
|
||||||
|
- **Run Command**: `python3 -u -m unittest`
|
||||||
|
- **Location**: `frigate/test/`
|
||||||
|
- **Coverage**: Aim for comprehensive test coverage of core functionality
|
||||||
|
- **Pattern**: Use `TestCase` classes with descriptive test method names
|
||||||
|
```python
|
||||||
|
class TestMotionDetection(unittest.TestCase):
|
||||||
|
def test_detects_motion_above_threshold(self):
|
||||||
|
# Test implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Best Practices
|
||||||
|
|
||||||
|
- Always have a way to test your work and confirm your changes
|
||||||
|
- Write tests for bug fixes to prevent regressions
|
||||||
|
- Test edge cases and error conditions
|
||||||
|
- Mock external dependencies (cameras, APIs, hardware)
|
||||||
|
- Use fixtures for test data
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Python Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
python3 -u -m unittest
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
python3 -u -m unittest frigate.test.test_ffmpeg_presets
|
||||||
|
|
||||||
|
# Check formatting (Ruff)
|
||||||
|
ruff format --check frigate/
|
||||||
|
|
||||||
|
# Apply formatting
|
||||||
|
ruff format frigate/
|
||||||
|
|
||||||
|
# Run linter
|
||||||
|
ruff check frigate/
|
||||||
|
|
||||||
|
# Type check
|
||||||
|
python3 -u -m mypy --config-file frigate/mypy.ini frigate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (from web/ directory)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dev server (AI agents should never run this directly unless asked)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run linter
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Fix linting issues
|
||||||
|
npm run lint:fix
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
npm run prettier:write
|
||||||
|
|
||||||
|
# E2E: first-time setup
|
||||||
|
npm install
|
||||||
|
npx playwright install chromium
|
||||||
|
|
||||||
|
# E2E: build the app and run all tests
|
||||||
|
npm run e2e:build && npm run e2e
|
||||||
|
|
||||||
|
# E2E: interactive UI for debugging
|
||||||
|
npm run e2e:ui
|
||||||
|
|
||||||
|
# E2E: run a specific spec
|
||||||
|
npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts
|
||||||
|
|
||||||
|
# E2E: filter by name, or run only desktop/mobile
|
||||||
|
npx playwright test --config e2e/playwright.config.ts --grep="severity tab"
|
||||||
|
npx playwright test --config e2e/playwright.config.ts --project=desktop
|
||||||
|
|
||||||
|
# E2E: regenerate mock data after backend model changes (from repo root)
|
||||||
|
PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py
|
||||||
|
|
||||||
|
# Regenerate config translations from Pydantic models — outputs to
|
||||||
|
# web/public/locales/en/config/{global,cameras}.json. NEVER edit those
|
||||||
|
# JSON files by hand; change the Pydantic field title/description and
|
||||||
|
# re-run this script. (from repo root)
|
||||||
|
python3 generate_config_translations.py
|
||||||
|
|
||||||
|
# Extract i18n keys from source into the locale files after adding
|
||||||
|
# new t() calls. Use the :ci variant to verify the locale files are
|
||||||
|
# in sync with source (fails if extraction would change anything).
|
||||||
|
npm run i18n:extract
|
||||||
|
npm run i18n:extract:ci
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
|
||||||
|
AI agents should never run these commands directly unless instructed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build local image
|
||||||
|
make local
|
||||||
|
|
||||||
|
# Build debug image
|
||||||
|
make debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### API Endpoint Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from frigate.api.defs.tags import Tags
|
||||||
|
|
||||||
|
router = APIRouter(tags=[Tags.Events])
|
||||||
|
|
||||||
|
@router.get("/events")
|
||||||
|
async def get_events(request: Request, limit: int = 100):
|
||||||
|
"""Retrieve events from the database."""
|
||||||
|
# Implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Access
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Access Frigate configuration
|
||||||
|
config: FrigateConfig = request.app.frigate_config
|
||||||
|
camera_config = config.cameras["front_door"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Queries
|
||||||
|
|
||||||
|
```python
|
||||||
|
from frigate.models import Event
|
||||||
|
|
||||||
|
# Use Peewee ORM for database access
|
||||||
|
events = (
|
||||||
|
Event.select()
|
||||||
|
.where(Event.camera == camera_name)
|
||||||
|
.order_by(Event.start_time.desc())
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
### ❌ Avoid These
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Blocking operations in async functions
|
||||||
|
data = requests.get(url) # ❌ Use async HTTP client
|
||||||
|
time.sleep(5) # ❌ Use asyncio.sleep()
|
||||||
|
|
||||||
|
# Hardcoded strings in React components
|
||||||
|
<div>Camera not found</div> # ❌ Use t("camera_not_found")
|
||||||
|
|
||||||
|
# Missing error handling
|
||||||
|
data = await api.get_data() # ❌ No exception handling
|
||||||
|
|
||||||
|
# Bare exceptions in regular code
|
||||||
|
try:
|
||||||
|
value = await sensor.read()
|
||||||
|
except Exception: # ❌ Too broad
|
||||||
|
logger.error("Failed")
|
||||||
|
|
||||||
|
# Returning exceptions in JSON responses
|
||||||
|
except ValueError as e:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": str(e)},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Use These Instead
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Async operations
|
||||||
|
import aiohttp
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as response:
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
await asyncio.sleep(5) # ✅ Non-blocking
|
||||||
|
|
||||||
|
# Translatable strings in React
|
||||||
|
const { t } = useTranslation();
|
||||||
|
<div>{t("camera_not_found")}</div> # ✅ Translatable
|
||||||
|
|
||||||
|
# Proper error handling
|
||||||
|
try:
|
||||||
|
data = await api.get_data()
|
||||||
|
except ApiException as err:
|
||||||
|
logger.error("API error: %s", err)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Specific exceptions
|
||||||
|
try:
|
||||||
|
value = await sensor.read()
|
||||||
|
except SensorException as err: # ✅ Specific
|
||||||
|
logger.exception("Failed to read sensor")
|
||||||
|
|
||||||
|
# Safe error responses
|
||||||
|
except ValueError:
|
||||||
|
logger.exception("Invalid parameters for API request")
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": "Invalid request parameters",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket Broadcasts
|
||||||
|
|
||||||
|
Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear.
|
||||||
|
|
||||||
|
## Project-Specific Conventions
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
- Main config: `config/config.yml`
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
- Backend code: `frigate/`
|
||||||
|
- Frontend code: `web/`
|
||||||
|
- Docker files: `docker/`
|
||||||
|
- Documentation: `docs/`
|
||||||
|
- Database migrations: `migrations/`
|
||||||
|
|
||||||
|
### Code Style Conformance
|
||||||
|
|
||||||
|
Always conform new and refactored code to the existing coding style in the project:
|
||||||
|
|
||||||
|
- Follow established patterns in similar files
|
||||||
|
- Match indentation and formatting of surrounding code
|
||||||
|
- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript)
|
||||||
|
- Maintain the same level of verbosity in comments and docstrings
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- Documentation: https://docs.frigate.video
|
||||||
|
- Main Repository: https://github.com/blakeblackshear/frigate
|
||||||
|
- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration
|
||||||
@@ -129,8 +129,14 @@ test.describe("Replay — active session @medium", () => {
|
|||||||
);
|
);
|
||||||
await actionGroup.first().click();
|
await actionGroup.first().click();
|
||||||
|
|
||||||
const dialog = frigateApp.page.getByRole("dialog");
|
// On mobile PlatformAwareSheet renders a MobilePage (full-screen panel)
|
||||||
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
// instead of a Radix Dialog, so assert the panel title heading is visible.
|
||||||
|
await expect(
|
||||||
|
frigateApp.page.getByRole("heading", {
|
||||||
|
level: 2,
|
||||||
|
name: /^Configuration$/i,
|
||||||
|
}),
|
||||||
|
).toBeVisible({ timeout: 5_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Objects tab renders with the camera_activity objects list", async ({
|
test("Objects tab renders with the camera_activity objects list", async ({
|
||||||
|
|||||||
@@ -1288,11 +1288,6 @@ export function ConfigSection({
|
|||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<div className="flex cursor-pointer items-center justify-between">
|
<div className="flex cursor-pointer items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{isOpen ? (
|
|
||||||
<LuChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<LuChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
<Heading as="h4">{title}</Heading>
|
<Heading as="h4">{title}</Heading>
|
||||||
{showOverrideIndicator &&
|
{showOverrideIndicator &&
|
||||||
effectiveLevel === "camera" &&
|
effectiveLevel === "camera" &&
|
||||||
@@ -1323,12 +1318,17 @@ export function ConfigSection({
|
|||||||
})}
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{isOpen ? (
|
||||||
|
<LuChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<LuChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="pl-7">{sectionContent}</div>
|
<div className="pl-0">{sectionContent}</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@@ -171,7 +171,20 @@ function modifyObjectsSchema(
|
|||||||
ctx.fullConfig.objects?.track ??
|
ctx.fullConfig.objects?.track ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
if (track.length === 0) return schema;
|
// Also promote any label that has a saved filter entry but isn't in
|
||||||
|
// `track` (e.g. the user toggled an object off but left a customized
|
||||||
|
// filter in YAML). Without this, RJSF falls back to the additional-
|
||||||
|
// properties Key/Value editor for those orphans.
|
||||||
|
const filtersSaved =
|
||||||
|
(ctx.level !== "global"
|
||||||
|
? ctx.fullCameraConfig?.objects?.filters
|
||||||
|
: undefined) ??
|
||||||
|
ctx.fullConfig.objects?.filters ??
|
||||||
|
{};
|
||||||
|
|
||||||
|
if (track.length === 0 && Object.keys(filtersSaved).length === 0) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
const schemaProperties = isJsonObject(
|
const schemaProperties = isJsonObject(
|
||||||
(schema as { properties?: unknown }).properties,
|
(schema as { properties?: unknown }).properties,
|
||||||
@@ -199,16 +212,27 @@ function modifyObjectsSchema(
|
|||||||
? (filtersSchema as { properties: Record<string, RJSFSchema> }).properties
|
? (filtersSchema as { properties: Record<string, RJSFSchema> }).properties
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
// Promote every tracked label to an explicit property entry so RJSF
|
// Promote every tracked label (and any orphaned filter entry) to an
|
||||||
// renders it as a normal collapsible (no additionalProperties key/value
|
// explicit property entry so RJSF renders it as a normal collapsible
|
||||||
// editor UI). Attribute labels get a restricted shape with only
|
// (no additionalProperties key/value editor UI). Attribute labels get a
|
||||||
// `min_score`; non-attribute labels get the full FilterConfig. Sorted
|
// restricted shape with only `min_score`/`min_area`/`max_area`;
|
||||||
// alphabetically so the filter collapsibles match the order of the
|
// non-attribute labels get the full FilterConfig. Sorted alphabetically
|
||||||
// sibling `track` switches.
|
// so the filter collapsibles match the order of the sibling `track`
|
||||||
const sortedTrackedLabels = track
|
// switches.
|
||||||
.filter((label): label is string => typeof label === "string")
|
const labelsToPromote = new Set<string>();
|
||||||
.slice()
|
for (const label of track) {
|
||||||
.sort((a, b) => a.localeCompare(b));
|
if (typeof label === "string") labelsToPromote.add(label);
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(filtersSaved)) {
|
||||||
|
// Skip attribute labels that aren't tracked — those are hidden
|
||||||
|
// entirely via hideAttributeFilters; promoting them would surface a
|
||||||
|
// collapsible we then have to hide separately.
|
||||||
|
if (attributeSet.has(key) && !labelsToPromote.has(key)) continue;
|
||||||
|
labelsToPromote.add(key);
|
||||||
|
}
|
||||||
|
const sortedTrackedLabels = [...labelsToPromote].sort((a, b) =>
|
||||||
|
a.localeCompare(b),
|
||||||
|
);
|
||||||
const updatedFilterProperties: Record<string, RJSFSchema> = {
|
const updatedFilterProperties: Record<string, RJSFSchema> = {
|
||||||
...existingProperties,
|
...existingProperties,
|
||||||
};
|
};
|
||||||
|
|||||||
+59
-59
@@ -27,13 +27,7 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import {
|
import { PlatformAwareSheet } from "@/components/overlay/dialog/PlatformAwareDialog";
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { useCameraActivity } from "@/hooks/use-camera-activity";
|
import { useCameraActivity } from "@/hooks/use-camera-activity";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Heading from "@/components/ui/heading";
|
import Heading from "@/components/ui/heading";
|
||||||
@@ -333,15 +327,64 @@ export default function Replay() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<PlatformAwareSheet
|
||||||
variant="outline"
|
trigger={
|
||||||
size="sm"
|
<Button
|
||||||
className="flex items-center gap-2"
|
variant="outline"
|
||||||
onClick={() => setConfigDialogOpen(true)}
|
size="sm"
|
||||||
>
|
className="flex items-center gap-2"
|
||||||
<LuSettings className="size-4" />
|
>
|
||||||
<span className="hidden md:inline">{t("page.configuration")}</span>
|
<LuSettings className="size-4" />
|
||||||
</Button>
|
<span className="hidden md:inline">
|
||||||
|
{t("page.configuration")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={t("page.configuration")}
|
||||||
|
titleClassName="text-lg font-semibold"
|
||||||
|
contentClassName="scrollbar-container flex flex-col gap-0 overflow-y-auto px-6 pb-6 sm:max-w-xl md:max-w-2xl xl:max-w-3xl"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p className="mb-5 text-sm text-muted-foreground">
|
||||||
|
{t("page.configurationDesc")}
|
||||||
|
</p>
|
||||||
|
{configSchema == null ? (
|
||||||
|
<div className="flex h-40 items-center justify-center">
|
||||||
|
<ActivityIndicator />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<ConfigSectionTemplate
|
||||||
|
sectionKey="motion"
|
||||||
|
level="replay"
|
||||||
|
cameraName={status.replay_camera ?? undefined}
|
||||||
|
skipSave
|
||||||
|
noStickyButtons
|
||||||
|
requiresRestart={false}
|
||||||
|
collapsible
|
||||||
|
defaultCollapsed={false}
|
||||||
|
showTitle
|
||||||
|
showOverrideIndicator={false}
|
||||||
|
/>
|
||||||
|
<ConfigSectionTemplate
|
||||||
|
sectionKey="objects"
|
||||||
|
level="replay"
|
||||||
|
cameraName={status.replay_camera ?? undefined}
|
||||||
|
skipSave
|
||||||
|
noStickyButtons
|
||||||
|
requiresRestart={false}
|
||||||
|
collapsible
|
||||||
|
defaultCollapsed={false}
|
||||||
|
showTitle
|
||||||
|
showOverrideIndicator={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
open={configDialogOpen}
|
||||||
|
onOpenChange={setConfigDialogOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
@@ -644,49 +687,6 @@ export default function Replay() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={configDialogOpen} onOpenChange={setConfigDialogOpen}>
|
|
||||||
<DialogContent className="scrollbar-container max-h-[90dvh] overflow-y-auto sm:max-w-xl md:max-w-3xl lg:max-w-4xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("page.configuration")}</DialogTitle>
|
|
||||||
<DialogDescription className="mb-5">
|
|
||||||
{t("page.configurationDesc")}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{configSchema == null ? (
|
|
||||||
<div className="flex h-40 items-center justify-center">
|
|
||||||
<ActivityIndicator />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<ConfigSectionTemplate
|
|
||||||
sectionKey="motion"
|
|
||||||
level="replay"
|
|
||||||
cameraName={status.replay_camera ?? undefined}
|
|
||||||
skipSave
|
|
||||||
noStickyButtons
|
|
||||||
requiresRestart={false}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
showTitle
|
|
||||||
showOverrideIndicator={false}
|
|
||||||
/>
|
|
||||||
<ConfigSectionTemplate
|
|
||||||
sectionKey="objects"
|
|
||||||
level="replay"
|
|
||||||
cameraName={status.replay_camera ?? undefined}
|
|
||||||
skipSave
|
|
||||||
noStickyButtons
|
|
||||||
requiresRestart={false}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
showTitle
|
|
||||||
showOverrideIndicator={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ const allSettingsViews = [
|
|||||||
"cameraLpr",
|
"cameraLpr",
|
||||||
"cameraMqttConfig",
|
"cameraMqttConfig",
|
||||||
"cameraOnvif",
|
"cameraOnvif",
|
||||||
"cameraUi",
|
|
||||||
"cameraTimestampStyle",
|
"cameraTimestampStyle",
|
||||||
"cameraManagement",
|
"cameraManagement",
|
||||||
"masksAndZones",
|
"masksAndZones",
|
||||||
@@ -292,9 +291,6 @@ const CameraMqttConfigSettingsPage = createSectionPage("mqtt", "camera", {
|
|||||||
const CameraOnvifSettingsPage = createSectionPage("onvif", "camera", {
|
const CameraOnvifSettingsPage = createSectionPage("onvif", "camera", {
|
||||||
showOverrideIndicator: false,
|
showOverrideIndicator: false,
|
||||||
});
|
});
|
||||||
const CameraUiSettingsPage = createSectionPage("ui", "camera", {
|
|
||||||
showOverrideIndicator: false,
|
|
||||||
});
|
|
||||||
const CameraTimestampStyleSettingsPage = createSectionPage(
|
const CameraTimestampStyleSettingsPage = createSectionPage(
|
||||||
"timestamp_style",
|
"timestamp_style",
|
||||||
"camera",
|
"camera",
|
||||||
@@ -361,7 +357,6 @@ const settingsGroups = [
|
|||||||
{ key: "cameraLpr", component: CameraLprSettingsPage },
|
{ key: "cameraLpr", component: CameraLprSettingsPage },
|
||||||
{ key: "cameraOnvif", component: CameraOnvifSettingsPage },
|
{ key: "cameraOnvif", component: CameraOnvifSettingsPage },
|
||||||
{ key: "cameraMqttConfig", component: CameraMqttConfigSettingsPage },
|
{ key: "cameraMqttConfig", component: CameraMqttConfigSettingsPage },
|
||||||
{ key: "cameraUi", component: CameraUiSettingsPage },
|
|
||||||
{
|
{
|
||||||
key: "cameraTimestampStyle",
|
key: "cameraTimestampStyle",
|
||||||
component: CameraTimestampStyleSettingsPage,
|
component: CameraTimestampStyleSettingsPage,
|
||||||
@@ -467,7 +462,6 @@ const CAMERA_SELECT_BUTTON_PAGES = [
|
|||||||
"cameraLpr",
|
"cameraLpr",
|
||||||
"cameraMqttConfig",
|
"cameraMqttConfig",
|
||||||
"cameraOnvif",
|
"cameraOnvif",
|
||||||
"cameraUi",
|
|
||||||
"cameraTimestampStyle",
|
"cameraTimestampStyle",
|
||||||
"masksAndZones",
|
"masksAndZones",
|
||||||
"motionTuner",
|
"motionTuner",
|
||||||
@@ -495,7 +489,6 @@ const CAMERA_SECTION_MAPPING: Record<string, SettingsType> = {
|
|||||||
lpr: "cameraLpr",
|
lpr: "cameraLpr",
|
||||||
mqtt: "cameraMqttConfig",
|
mqtt: "cameraMqttConfig",
|
||||||
onvif: "cameraOnvif",
|
onvif: "cameraOnvif",
|
||||||
ui: "cameraUi",
|
|
||||||
timestamp_style: "cameraTimestampStyle",
|
timestamp_style: "cameraTimestampStyle",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user