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:
203
src/api/schemas/crossswitch.py
Normal file
203
src/api/schemas/crossswitch.py
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user