Phase 7: Cross-Switching - CORE FUNCTIONALITY (T063-T074)

Implemented complete cross-switching system with database persistence and audit logging:

**Tests:**
- Contract tests for POST /api/v1/crossswitch (execute cross-switch)
- Contract tests for POST /api/v1/crossswitch/clear (clear monitor)
- Contract tests for GET /api/v1/crossswitch/routing (routing state)
- Contract tests for GET /api/v1/crossswitch/history (routing history)
- Integration tests for complete cross-switch workflow
- RBAC tests (operator required for execution, viewer for reading)

**Database:**
- CrossSwitchRoute model with full routing history tracking
- Fields: camera_id, monitor_id, mode, executed_at, executed_by, is_active
- Cleared route tracking: cleared_at, cleared_by
- SDK response tracking: sdk_success, sdk_error
- JSONB details field for camera/monitor names
- Comprehensive indexes for performance

**Migration:**
- 20251209_crossswitch_routes: Creates crossswitch_routes table
- Foreign keys to users table for executed_by and cleared_by
- Indexes: active routes, camera history, monitor history, user routes

**Schemas:**
- CrossSwitchRequest: camera_id, monitor_id, mode validation
- ClearMonitorRequest: monitor_id validation
- RouteInfo: Complete route information with user details
- CrossSwitchResponse, ClearMonitorResponse, RoutingStateResponse
- RouteHistoryResponse: Pagination support

**Services:**
- CrossSwitchService: Complete cross-switching logic
- execute_crossswitch(): Route camera to monitor via SDK Bridge
- clear_monitor(): Remove camera from monitor
- get_routing_state(): Get active routes
- get_routing_history(): Get historical routes with pagination
- Automatic route clearing when new camera assigned to monitor
- Cache invalidation after routing changes
- Integrated audit logging for all operations

**Router Endpoints:**
- POST /api/v1/crossswitch - Execute cross-switch (Operator+)
- POST /api/v1/crossswitch/clear - Clear monitor (Operator+)
- GET /api/v1/crossswitch/routing - Get routing state (Viewer+)
- GET /api/v1/crossswitch/history - Get routing history (Viewer+)

**RBAC:**
- Operator role or higher required for execution (crossswitch, clear)
- Viewer role can read routing state and history
- Administrator has all permissions

**Audit Logging:**
- All cross-switch operations logged to audit_logs table
- Tracks: user, IP address, camera/monitor IDs, success/failure
- SDK errors captured in both audit log and route record

**Integration:**
- Registered crossswitch router in main.py
- SDK Bridge integration for hardware control
- Redis cache invalidation on routing changes
- Database persistence of all routing history

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Geutebruck API Developer
2025-12-09 13:39:53 +01:00
parent 0361826d3e
commit aa6f7ec947
7 changed files with 1489 additions and 5 deletions

View File

@@ -0,0 +1,203 @@
"""
Cross-switch schemas for request/response validation
"""
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from datetime import datetime
class CrossSwitchRequest(BaseModel):
"""Request schema for executing cross-switch"""
camera_id: int = Field(..., gt=0, description="Camera ID (must be positive)")
monitor_id: int = Field(..., gt=0, description="Monitor ID (must be positive)")
mode: int = Field(default=0, ge=0, description="Cross-switch mode (default: 0=normal)")
@field_validator('camera_id', 'monitor_id')
@classmethod
def validate_positive_id(cls, v: int) -> int:
"""Ensure IDs are positive"""
if v <= 0:
raise ValueError('ID must be positive')
return v
model_config = {
"json_schema_extra": {
"examples": [
{
"camera_id": 1,
"monitor_id": 1,
"mode": 0
}
]
}
}
class ClearMonitorRequest(BaseModel):
"""Request schema for clearing a monitor"""
monitor_id: int = Field(..., gt=0, description="Monitor ID to clear (must be positive)")
@field_validator('monitor_id')
@classmethod
def validate_positive_id(cls, v: int) -> int:
"""Ensure monitor ID is positive"""
if v <= 0:
raise ValueError('Monitor ID must be positive')
return v
model_config = {
"json_schema_extra": {
"examples": [
{
"monitor_id": 1
}
]
}
}
class RouteInfo(BaseModel):
"""Route information schema"""
id: str = Field(..., description="Route UUID")
camera_id: int = Field(..., description="Camera ID")
monitor_id: int = Field(..., description="Monitor ID")
mode: int = Field(default=0, description="Cross-switch mode")
executed_at: datetime = Field(..., description="When route was executed")
executed_by: Optional[str] = Field(None, description="User ID who executed the route")
executed_by_username: Optional[str] = Field(None, description="Username who executed the route")
is_active: bool = Field(..., description="Whether route is currently active")
camera_name: Optional[str] = Field(None, description="Camera name")
monitor_name: Optional[str] = Field(None, description="Monitor name")
model_config = {
"from_attributes": True,
"json_schema_extra": {
"examples": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"camera_id": 1,
"monitor_id": 1,
"mode": 0,
"executed_at": "2025-12-09T10:30:00Z",
"executed_by": "550e8400-e29b-41d4-a716-446655440001",
"executed_by_username": "operator",
"is_active": True,
"camera_name": "Entrance Camera",
"monitor_name": "Control Room Monitor 1"
}
]
}
}
class CrossSwitchResponse(BaseModel):
"""Response schema for successful cross-switch execution"""
success: bool = Field(..., description="Whether operation succeeded")
message: str = Field(..., description="Success message")
route: RouteInfo = Field(..., description="Route information")
model_config = {
"json_schema_extra": {
"examples": [
{
"success": True,
"message": "Successfully switched camera 1 to monitor 1",
"route": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"camera_id": 1,
"monitor_id": 1,
"mode": 0,
"executed_at": "2025-12-09T10:30:00Z",
"executed_by": "550e8400-e29b-41d4-a716-446655440001",
"executed_by_username": "operator",
"is_active": True,
"camera_name": "Entrance Camera",
"monitor_name": "Control Room Monitor 1"
}
}
]
}
}
class ClearMonitorResponse(BaseModel):
"""Response schema for successful clear monitor operation"""
success: bool = Field(..., description="Whether operation succeeded")
message: str = Field(..., description="Success message")
monitor_id: int = Field(..., description="Monitor ID that was cleared")
model_config = {
"json_schema_extra": {
"examples": [
{
"success": True,
"message": "Successfully cleared monitor 1",
"monitor_id": 1
}
]
}
}
class RoutingStateResponse(BaseModel):
"""Response schema for routing state query"""
routes: List[RouteInfo] = Field(..., description="List of active routes")
total: int = Field(..., description="Total number of active routes")
model_config = {
"json_schema_extra": {
"examples": [
{
"routes": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"camera_id": 1,
"monitor_id": 1,
"mode": 0,
"executed_at": "2025-12-09T10:30:00Z",
"executed_by": "550e8400-e29b-41d4-a716-446655440001",
"executed_by_username": "operator",
"is_active": True,
"camera_name": "Entrance Camera",
"monitor_name": "Control Room Monitor 1"
}
],
"total": 1
}
]
}
}
class RouteHistoryResponse(BaseModel):
"""Response schema for routing history query"""
history: List[RouteInfo] = Field(..., description="List of historical routes")
total: int = Field(..., description="Total number of historical records")
limit: int = Field(..., description="Pagination limit")
offset: int = Field(..., description="Pagination offset")
model_config = {
"json_schema_extra": {
"examples": [
{
"history": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"camera_id": 1,
"monitor_id": 1,
"mode": 0,
"executed_at": "2025-12-09T10:30:00Z",
"executed_by": "550e8400-e29b-41d4-a716-446655440001",
"executed_by_username": "operator",
"is_active": False,
"camera_name": "Entrance Camera",
"monitor_name": "Control Room Monitor 1"
}
],
"total": 50,
"limit": 10,
"offset": 0
}
]
}
}