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>
123 lines
4.5 KiB
Python
123 lines
4.5 KiB
Python
"""
|
|
CrossSwitchRoute model for storing cross-switching history and current state
|
|
"""
|
|
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Index
|
|
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
|
from datetime import datetime
|
|
import uuid
|
|
|
|
from models import Base
|
|
|
|
|
|
class CrossSwitchRoute(Base):
|
|
"""
|
|
Model for cross-switch routing records
|
|
|
|
Stores both current routing state and historical routing changes
|
|
"""
|
|
__tablename__ = "crossswitch_routes"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
|
|
# Route information
|
|
camera_id = Column(Integer, nullable=False, index=True, comment="Camera ID (source)")
|
|
monitor_id = Column(Integer, nullable=False, index=True, comment="Monitor ID (destination)")
|
|
mode = Column(Integer, default=0, comment="Cross-switch mode (0=normal, other modes per SDK)")
|
|
|
|
# Execution tracking
|
|
executed_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True)
|
|
executed_by = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
|
|
|
# Status tracking
|
|
is_active = Column(Integer, default=1, nullable=False, index=True, comment="1=active route, 0=cleared/historical")
|
|
cleared_at = Column(DateTime, nullable=True, comment="When this route was cleared (if cleared)")
|
|
cleared_by = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
|
|
|
# Additional metadata
|
|
details = Column(JSONB, nullable=True, comment="Additional route details (camera name, monitor name, etc.)")
|
|
|
|
# SDK response tracking
|
|
sdk_success = Column(Integer, default=1, nullable=False, comment="1=SDK reported success, 0=SDK reported failure")
|
|
sdk_error = Column(String(500), nullable=True, comment="SDK error message if failed")
|
|
|
|
# Indexes for common queries
|
|
__table_args__ = (
|
|
# Index for getting current active routes
|
|
Index('idx_active_routes', 'is_active', 'monitor_id'),
|
|
# Index for getting route history by camera
|
|
Index('idx_camera_history', 'camera_id', 'executed_at'),
|
|
# Index for getting route history by monitor
|
|
Index('idx_monitor_history', 'monitor_id', 'executed_at'),
|
|
# Index for getting user's routing actions
|
|
Index('idx_user_routes', 'executed_by', 'executed_at'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<CrossSwitchRoute(camera={self.camera_id}, monitor={self.monitor_id}, active={self.is_active})>"
|
|
|
|
@classmethod
|
|
def create_route(
|
|
cls,
|
|
camera_id: int,
|
|
monitor_id: int,
|
|
executed_by: uuid.UUID,
|
|
mode: int = 0,
|
|
sdk_success: bool = True,
|
|
sdk_error: str = None,
|
|
details: dict = None
|
|
):
|
|
"""
|
|
Factory method to create a new route record
|
|
|
|
Args:
|
|
camera_id: Camera ID
|
|
monitor_id: Monitor ID
|
|
executed_by: User ID who executed the route
|
|
mode: Cross-switch mode (default: 0)
|
|
sdk_success: Whether SDK reported success
|
|
sdk_error: SDK error message if failed
|
|
details: Additional metadata
|
|
|
|
Returns:
|
|
CrossSwitchRoute instance
|
|
"""
|
|
return cls(
|
|
camera_id=camera_id,
|
|
monitor_id=monitor_id,
|
|
mode=mode,
|
|
executed_by=executed_by,
|
|
executed_at=datetime.utcnow(),
|
|
is_active=1 if sdk_success else 0,
|
|
sdk_success=1 if sdk_success else 0,
|
|
sdk_error=sdk_error,
|
|
details=details or {}
|
|
)
|
|
|
|
def clear_route(self, cleared_by: uuid.UUID):
|
|
"""
|
|
Mark this route as cleared
|
|
|
|
Args:
|
|
cleared_by: User ID who cleared the route
|
|
"""
|
|
self.is_active = 0
|
|
self.cleared_at = datetime.utcnow()
|
|
self.cleared_by = cleared_by
|
|
|
|
def to_dict(self):
|
|
"""Convert to dictionary for API responses"""
|
|
return {
|
|
"id": str(self.id),
|
|
"camera_id": self.camera_id,
|
|
"monitor_id": self.monitor_id,
|
|
"mode": self.mode,
|
|
"executed_at": self.executed_at.isoformat() if self.executed_at else None,
|
|
"executed_by": str(self.executed_by) if self.executed_by else None,
|
|
"is_active": bool(self.is_active),
|
|
"cleared_at": self.cleared_at.isoformat() if self.cleared_at else None,
|
|
"cleared_by": str(self.cleared_by) if self.cleared_by else None,
|
|
"details": self.details,
|
|
"sdk_success": bool(self.sdk_success),
|
|
"sdk_error": self.sdk_error
|
|
}
|