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>
69 lines
3.1 KiB
Python
69 lines
3.1 KiB
Python
"""Add crossswitch_routes table
|
|
|
|
Revision ID: 20251209_crossswitch
|
|
Revises: 20251208_initial_schema
|
|
Create Date: 2025-12-09 12:00:00.000000
|
|
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision = '20251209_crossswitch'
|
|
down_revision = '20251208_initial_schema'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Create crossswitch_routes table"""
|
|
|
|
# Create crossswitch_routes table
|
|
op.create_table(
|
|
'crossswitch_routes',
|
|
sa.Column('id', UUID(as_uuid=True), primary_key=True, nullable=False),
|
|
sa.Column('camera_id', sa.Integer(), nullable=False, comment='Camera ID (source)'),
|
|
sa.Column('monitor_id', sa.Integer(), nullable=False, comment='Monitor ID (destination)'),
|
|
sa.Column('mode', sa.Integer(), nullable=True, default=0, comment='Cross-switch mode (0=normal)'),
|
|
sa.Column('executed_at', sa.DateTime(), nullable=False),
|
|
sa.Column('executed_by', UUID(as_uuid=True), nullable=True),
|
|
sa.Column('is_active', sa.Integer(), nullable=False, default=1, comment='1=active route, 0=cleared/historical'),
|
|
sa.Column('cleared_at', sa.DateTime(), nullable=True, comment='When this route was cleared'),
|
|
sa.Column('cleared_by', UUID(as_uuid=True), nullable=True),
|
|
sa.Column('details', JSONB, nullable=True, comment='Additional route details'),
|
|
sa.Column('sdk_success', sa.Integer(), nullable=False, default=1, comment='1=SDK success, 0=SDK failure'),
|
|
sa.Column('sdk_error', sa.String(500), nullable=True, comment='SDK error message if failed'),
|
|
|
|
# Foreign keys
|
|
sa.ForeignKeyConstraint(['executed_by'], ['users.id'], ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['cleared_by'], ['users.id'], ondelete='SET NULL'),
|
|
)
|
|
|
|
# Create indexes for common queries
|
|
op.create_index('idx_active_routes', 'crossswitch_routes', ['is_active', 'monitor_id'])
|
|
op.create_index('idx_camera_history', 'crossswitch_routes', ['camera_id', 'executed_at'])
|
|
op.create_index('idx_monitor_history', 'crossswitch_routes', ['monitor_id', 'executed_at'])
|
|
op.create_index('idx_user_routes', 'crossswitch_routes', ['executed_by', 'executed_at'])
|
|
|
|
# Create index for single-column lookups
|
|
op.create_index('idx_camera_id', 'crossswitch_routes', ['camera_id'])
|
|
op.create_index('idx_monitor_id', 'crossswitch_routes', ['monitor_id'])
|
|
op.create_index('idx_executed_at', 'crossswitch_routes', ['executed_at'])
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Drop crossswitch_routes table"""
|
|
|
|
# Drop indexes
|
|
op.drop_index('idx_executed_at', table_name='crossswitch_routes')
|
|
op.drop_index('idx_monitor_id', table_name='crossswitch_routes')
|
|
op.drop_index('idx_camera_id', table_name='crossswitch_routes')
|
|
op.drop_index('idx_user_routes', table_name='crossswitch_routes')
|
|
op.drop_index('idx_monitor_history', table_name='crossswitch_routes')
|
|
op.drop_index('idx_camera_history', table_name='crossswitch_routes')
|
|
op.drop_index('idx_active_routes', table_name='crossswitch_routes')
|
|
|
|
# Drop table
|
|
op.drop_table('crossswitch_routes')
|