Files
geutebruck-api/src/api/models/crossswitch_route.py
Geutebruck API Developer aa6f7ec947 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>
2025-12-09 13:39:53 +01:00

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
}