Files
geutebruck-api/src/api/migrations/versions/20251209_crossswitch_routes.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

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')