Updated all spec-kit documents to document the implemented configuration management features (User Story 12): Changes: - spec.md: Added User Story 12 with implementation status and functional requirements (FR-039 through FR-045) - plan.md: Added Phase 2 (Configuration Management) as completed, updated phase status and last updated date - data-model.md: Added GCoreServer entity with schema, validation rules, CRUD status, and critical implementation details - tasks.md: Added Phase 13 for User Story 12 with implementation summary, updated task counts and dependencies - tasks-revised-mvp.md: Added configuration management completion notice Implementation Highlights: - G-Core Server CRUD (CREATE, READ, DELETE working; UPDATE has known bug) - Action Mapping CRUD (all operations working) - SetupClient integration for .set file operations - Critical cascade deletion bug fix (delete in reverse order) - Comprehensive test scripts and verification tools Documentation: SERVER_CRUD_IMPLEMENTATION.md, CRITICAL_BUG_FIX_DELETE.md 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1189 lines
36 KiB
Markdown
1189 lines
36 KiB
Markdown
# Data Model: Geutebruck Video Surveillance API
|
|
|
|
**Branch**: `001-surveillance-api` | **Date**: 2025-12-08
|
|
**Input**: [spec.md](./spec.md) requirements | [research.md](./research.md) technical decisions
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document defines all data entities, their schemas, relationships, validation rules, and state transitions for the Geutebruck Video Surveillance API.
|
|
|
|
**Entity Categories**:
|
|
- **Authentication**: User, Session, Token
|
|
- **Surveillance**: Camera, Stream, Recording
|
|
- **Events**: Event, EventSubscription
|
|
- **Configuration**: AnalyticsConfig, PTZPreset
|
|
- **Audit**: AuditLog
|
|
|
|
---
|
|
|
|
## 1. Authentication Entities
|
|
|
|
### 1.1 User
|
|
|
|
Represents an API user with authentication credentials and permissions.
|
|
|
|
**Schema**:
|
|
```python
|
|
class User(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
username: str = Field(min_length=3, max_length=50, pattern="^[a-zA-Z0-9_-]+$")
|
|
email: EmailStr
|
|
hashed_password: str # bcrypt hash
|
|
role: UserRole # viewer, operator, administrator
|
|
permissions: List[Permission] # Granular camera-level permissions
|
|
is_active: bool = True
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
last_login: Optional[datetime] = None
|
|
|
|
class UserRole(str, Enum):
|
|
VIEWER = "viewer" # Read-only camera access
|
|
OPERATOR = "operator" # Camera control + viewing
|
|
ADMINISTRATOR = "administrator" # Full system configuration
|
|
|
|
class Permission(BaseModel):
|
|
resource_type: str # "camera", "recording", "analytics"
|
|
resource_id: int # Channel ID or "*" for all
|
|
actions: List[str] # ["view", "ptz", "record", "configure"]
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `username`: Unique, alphanumeric with dash/underscore only
|
|
- `email`: Valid email format, unique
|
|
- `hashed_password`: Never returned in API responses
|
|
- `role`: Must be one of defined roles
|
|
- `permissions`: Empty list defaults to role-based permissions
|
|
|
|
**Relationships**:
|
|
- User → Session (one-to-many): User can have multiple active sessions
|
|
- User → AuditLog (one-to-many): All user actions logged
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"username": "operator1",
|
|
"email": "operator1@example.com",
|
|
"role": "operator",
|
|
"permissions": [
|
|
{
|
|
"resource_type": "camera",
|
|
"resource_id": 1,
|
|
"actions": ["view", "ptz"]
|
|
},
|
|
{
|
|
"resource_type": "camera",
|
|
"resource_id": 2,
|
|
"actions": ["view"]
|
|
}
|
|
],
|
|
"is_active": true,
|
|
"created_at": "2025-12-01T10:00:00Z",
|
|
"last_login": "2025-12-08T14:30:00Z"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 1.2 Session
|
|
|
|
Represents an active authentication session with JWT tokens.
|
|
|
|
**Schema**:
|
|
```python
|
|
class Session(BaseModel):
|
|
session_id: str = Field(...) # JTI from JWT
|
|
user_id: UUID
|
|
access_token_jti: str
|
|
refresh_token_jti: Optional[str] = None
|
|
ip_address: str
|
|
user_agent: str
|
|
created_at: datetime
|
|
last_activity: datetime
|
|
expires_at: datetime
|
|
|
|
class TokenPair(BaseModel):
|
|
access_token: str # JWT token string
|
|
refresh_token: str
|
|
token_type: str = "bearer"
|
|
expires_in: int # Seconds until access token expires
|
|
```
|
|
|
|
**State Transitions**:
|
|
```
|
|
Created → Active → Refreshed → Expired/Revoked
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `session_id`: Unique, UUID format
|
|
- `access_token_jti`: Must match JWT jti claim
|
|
- `ip_address`: Valid IPv4/IPv6 address
|
|
- `expires_at`: Auto-set based on JWT expiration
|
|
|
|
**Storage**: Redis with TTL matching token expiration
|
|
|
|
**Redis Keys**:
|
|
```
|
|
session:{user_id}:{session_id} → Session JSON
|
|
refresh:{user_id}:{refresh_token_jti} → Refresh token metadata
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Surveillance Entities
|
|
|
|
### 2.1 Camera
|
|
|
|
Represents a video input channel/camera with capabilities and status.
|
|
|
|
**Schema**:
|
|
```python
|
|
class Camera(BaseModel):
|
|
id: int # Channel ID from GeViScope
|
|
global_id: str # GeViScope GlobalID (UUID)
|
|
name: str = Field(min_length=1, max_length=100)
|
|
description: Optional[str] = Field(max_length=500)
|
|
location: Optional[str] = Field(max_length=200)
|
|
status: CameraStatus
|
|
capabilities: CameraCapabilities
|
|
stream_info: Optional[StreamInfo] = None
|
|
recording_status: RecordingStatus
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class CameraStatus(str, Enum):
|
|
ONLINE = "online"
|
|
OFFLINE = "offline"
|
|
ERROR = "error"
|
|
MAINTENANCE = "maintenance"
|
|
|
|
class CameraCapabilities(BaseModel):
|
|
has_ptz: bool = False
|
|
has_video_sensor: bool = False # Motion detection
|
|
has_contrast_detection: bool = False
|
|
has_sync_detection: bool = False
|
|
supported_analytics: List[AnalyticsType] = []
|
|
supported_resolutions: List[str] = [] # ["1920x1080", "1280x720"]
|
|
supported_formats: List[str] = [] # ["h264", "mjpeg"]
|
|
|
|
class AnalyticsType(str, Enum):
|
|
VMD = "vmd" # Video Motion Detection
|
|
NPR = "npr" # Number Plate Recognition
|
|
OBTRACK = "obtrack" # Object Tracking
|
|
GTECT = "gtect" # Perimeter Protection
|
|
CPA = "cpa" # Camera Position Analysis
|
|
```
|
|
|
|
**State Transitions**:
|
|
```
|
|
Offline ⟷ Online ⟷ Error
|
|
↓
|
|
Maintenance
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `id`: Positive integer, corresponds to GeViScope channel ID
|
|
- `name`: Required, user-friendly camera identifier
|
|
- `status`: Updated via SDK events
|
|
- `capabilities`: Populated from GeViScope VideoInputInfo
|
|
|
|
**Relationships**:
|
|
- Camera → Stream (one-to-many): Multiple concurrent streams per camera
|
|
- Camera → Recording (one-to-many): Recording segments for this camera
|
|
- Camera → AnalyticsConfig (one-to-one): Analytics configuration
|
|
- Camera → PTZPreset (one-to-many): Saved PTZ positions
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": 5,
|
|
"global_id": "a7b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
|
"name": "Entrance Camera",
|
|
"description": "Main entrance monitoring",
|
|
"location": "Building A - Main Entrance",
|
|
"status": "online",
|
|
"capabilities": {
|
|
"has_ptz": true,
|
|
"has_video_sensor": true,
|
|
"has_contrast_detection": true,
|
|
"has_sync_detection": true,
|
|
"supported_analytics": ["vmd", "obtrack"],
|
|
"supported_resolutions": ["1920x1080", "1280x720"],
|
|
"supported_formats": ["h264", "mjpeg"]
|
|
},
|
|
"recording_status": {
|
|
"is_recording": true,
|
|
"mode": "continuous",
|
|
"start_time": "2025-12-08T00:00:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.2 Stream
|
|
|
|
Represents an active video stream session.
|
|
|
|
**Schema**:
|
|
```python
|
|
class Stream(BaseModel):
|
|
stream_id: UUID = Field(default_factory=uuid4)
|
|
camera_id: int # Channel ID
|
|
user_id: UUID
|
|
stream_url: HttpUrl # Authenticated URL to GeViScope stream
|
|
format: str # "h264", "mjpeg"
|
|
resolution: str # "1920x1080"
|
|
fps: int = Field(ge=1, le=60)
|
|
quality: int = Field(ge=1, le=100) # Quality percentage
|
|
started_at: datetime
|
|
last_activity: datetime
|
|
expires_at: datetime
|
|
status: StreamStatus
|
|
|
|
class StreamStatus(str, Enum):
|
|
INITIALIZING = "initializing"
|
|
ACTIVE = "active"
|
|
PAUSED = "paused"
|
|
STOPPED = "stopped"
|
|
ERROR = "error"
|
|
|
|
class StreamRequest(BaseModel):
|
|
"""Request model for initiating a stream"""
|
|
format: str = "h264"
|
|
resolution: Optional[str] = None # Default: camera's max resolution
|
|
fps: Optional[int] = None # Default: camera's max FPS
|
|
quality: int = Field(default=90, ge=1, le=100)
|
|
|
|
class StreamResponse(BaseModel):
|
|
"""Response containing stream access details"""
|
|
stream_id: UUID
|
|
camera_id: int
|
|
stream_url: HttpUrl # Token-authenticated URL
|
|
format: str
|
|
resolution: str
|
|
fps: int
|
|
expires_at: datetime # Stream URL expiration
|
|
websocket_url: Optional[HttpUrl] = None # For WebSocket-based streams
|
|
```
|
|
|
|
**State Transitions**:
|
|
```
|
|
Initializing → Active ⟷ Paused → Stopped
|
|
↓
|
|
Error
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `camera_id`: Must reference existing, online camera
|
|
- `stream_url`: Contains time-limited JWT token
|
|
- `expires_at`: Default 1 hour from creation
|
|
- `format`, `resolution`, `fps`: Must be supported by camera
|
|
|
|
**Lifecycle**:
|
|
1. Client requests stream: `POST /cameras/{id}/stream`
|
|
2. API generates token-authenticated URL
|
|
3. Client connects directly to GeViScope stream URL
|
|
4. Stream auto-expires after TTL
|
|
5. Client can extend via API: `POST /streams/{id}/extend`
|
|
|
|
---
|
|
|
|
### 2.3 Recording
|
|
|
|
Represents a video recording segment.
|
|
|
|
**Schema**:
|
|
```python
|
|
class Recording(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
camera_id: int
|
|
start_time: datetime
|
|
end_time: Optional[datetime] = None # None if still recording
|
|
duration_seconds: Optional[int] = None
|
|
file_size_bytes: Optional[int] = None
|
|
trigger: RecordingTrigger
|
|
status: RecordingStatus
|
|
export_url: Optional[HttpUrl] = None # If exported
|
|
metadata: RecordingMetadata
|
|
created_at: datetime
|
|
|
|
class RecordingTrigger(str, Enum):
|
|
SCHEDULED = "scheduled" # Time-based schedule
|
|
EVENT = "event" # Triggered by alarm/analytics
|
|
MANUAL = "manual" # User-initiated
|
|
CONTINUOUS = "continuous" # Always recording
|
|
|
|
class RecordingStatus(str, Enum):
|
|
RECORDING = "recording"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
EXPORTING = "exporting"
|
|
EXPORTED = "exported"
|
|
|
|
class RecordingMetadata(BaseModel):
|
|
event_id: Optional[str] = None # If event-triggered
|
|
pre_alarm_seconds: int = 0
|
|
post_alarm_seconds: int = 0
|
|
tags: List[str] = []
|
|
notes: Optional[str] = None
|
|
|
|
class RecordingQueryParams(BaseModel):
|
|
camera_id: Optional[int] = None
|
|
start_time: Optional[datetime] = None
|
|
end_time: Optional[datetime] = None
|
|
trigger: Optional[RecordingTrigger] = None
|
|
limit: int = Field(default=50, ge=1, le=1000)
|
|
offset: int = Field(default=0, ge=0)
|
|
```
|
|
|
|
**State Transitions**:
|
|
```
|
|
Recording → Completed
|
|
↓
|
|
Exporting → Exported
|
|
↓
|
|
Failed
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `camera_id`: Must exist
|
|
- `start_time`: Cannot be in future
|
|
- `end_time`: Must be after start_time
|
|
- `file_size_bytes`: Calculated from ring buffer
|
|
|
|
**Relationships**:
|
|
- Recording → Camera (many-to-one)
|
|
- Recording → Event (many-to-one, optional)
|
|
|
|
**Ring Buffer Handling**:
|
|
- Oldest recordings automatically deleted when buffer full
|
|
- `retention_policy` determines minimum retention period
|
|
- API exposes capacity warnings
|
|
|
|
---
|
|
|
|
## 3. Event Entities
|
|
|
|
### 3.1 Event
|
|
|
|
Represents a surveillance event (alarm, analytics, system).
|
|
|
|
**Schema**:
|
|
```python
|
|
class Event(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
event_type: EventType
|
|
camera_id: Optional[int] = None # None for system events
|
|
timestamp: datetime
|
|
severity: EventSeverity
|
|
data: EventData # Type-specific event data
|
|
foreign_key: Optional[str] = None # External system correlation
|
|
acknowledged: bool = False
|
|
acknowledged_by: Optional[UUID] = None
|
|
acknowledged_at: Optional[datetime] = None
|
|
|
|
class EventType(str, Enum):
|
|
# Analytics events
|
|
MOTION_DETECTED = "motion_detected"
|
|
OBJECT_TRACKED = "object_tracked"
|
|
LICENSE_PLATE = "license_plate"
|
|
PERIMETER_BREACH = "perimeter_breach"
|
|
CAMERA_TAMPER = "camera_tamper"
|
|
|
|
# System events
|
|
CAMERA_ONLINE = "camera_online"
|
|
CAMERA_OFFLINE = "camera_offline"
|
|
RECORDING_STARTED = "recording_started"
|
|
RECORDING_STOPPED = "recording_stopped"
|
|
STORAGE_WARNING = "storage_warning"
|
|
|
|
# Alarm events
|
|
ALARM_TRIGGERED = "alarm_triggered"
|
|
ALARM_CLEARED = "alarm_cleared"
|
|
|
|
class EventSeverity(str, Enum):
|
|
INFO = "info"
|
|
WARNING = "warning"
|
|
ERROR = "error"
|
|
CRITICAL = "critical"
|
|
|
|
class EventData(BaseModel):
|
|
"""Base class for type-specific event data"""
|
|
pass
|
|
|
|
class MotionDetectedData(EventData):
|
|
zone: str
|
|
confidence: float = Field(ge=0.0, le=1.0)
|
|
snapshot_url: Optional[HttpUrl] = None
|
|
|
|
class LicensePlateData(EventData):
|
|
plate_number: str
|
|
country_code: str
|
|
confidence: float = Field(ge=0.0, le=1.0)
|
|
snapshot_url: Optional[HttpUrl] = None
|
|
is_watchlist_match: bool = False
|
|
|
|
class ObjectTrackedData(EventData):
|
|
tracking_id: str
|
|
object_type: str # "person", "vehicle"
|
|
zone_entered: Optional[str] = None
|
|
zone_exited: Optional[str] = None
|
|
dwell_time_seconds: Optional[int] = None
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `event_type`: Must be valid EventType
|
|
- `camera_id`: Required for camera events, None for system events
|
|
- `timestamp`: Auto-set to current time if not provided
|
|
- `severity`: Must match event type severity mapping
|
|
|
|
**Relationships**:
|
|
- Event → Camera (many-to-one, optional)
|
|
- Event → Recording (one-to-one, optional)
|
|
- Event → User (acknowledged_by, many-to-one, optional)
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
"event_type": "motion_detected",
|
|
"camera_id": 5,
|
|
"timestamp": "2025-12-08T14:45:23Z",
|
|
"severity": "warning",
|
|
"data": {
|
|
"zone": "entrance",
|
|
"confidence": 0.95,
|
|
"snapshot_url": "https://api.example.com/snapshots/abc123.jpg"
|
|
},
|
|
"acknowledged": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.2 EventSubscription
|
|
|
|
Represents a WebSocket client's event subscription.
|
|
|
|
**Schema**:
|
|
```python
|
|
class EventSubscription(BaseModel):
|
|
subscription_id: UUID = Field(default_factory=uuid4)
|
|
user_id: UUID
|
|
connection_id: str # WebSocket connection identifier
|
|
filters: EventFilter
|
|
created_at: datetime
|
|
last_heartbeat: datetime
|
|
|
|
class EventFilter(BaseModel):
|
|
event_types: Optional[List[EventType]] = None # None = all types
|
|
camera_ids: Optional[List[int]] = None # None = all cameras
|
|
severity: Optional[EventSeverity] = None # Minimum severity
|
|
include_acknowledged: bool = False
|
|
|
|
class EventNotification(BaseModel):
|
|
"""WebSocket message format"""
|
|
subscription_id: UUID
|
|
event: Event
|
|
sequence_number: int # For detecting missed events
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `filters.camera_ids`: User must have view permission for each camera
|
|
- `last_heartbeat`: Updated every 30 seconds, timeout after 90 seconds
|
|
|
|
**Storage**: Redis with 5-minute retention for reconnection
|
|
|
|
---
|
|
|
|
## 4. Configuration Entities
|
|
|
|
### 4.1 AnalyticsConfig
|
|
|
|
Configuration for video analytics on a camera.
|
|
|
|
**Schema**:
|
|
```python
|
|
class AnalyticsConfig(BaseModel):
|
|
camera_id: int
|
|
analytics_type: AnalyticsType
|
|
enabled: bool = False
|
|
config: AnalyticsTypeConfig # Type-specific configuration
|
|
updated_at: datetime
|
|
updated_by: UUID
|
|
|
|
class VMDConfig(AnalyticsTypeConfig):
|
|
"""Video Motion Detection configuration"""
|
|
zones: List[DetectionZone]
|
|
sensitivity: int = Field(ge=1, le=10, default=5)
|
|
min_object_size: int = Field(ge=1, le=100, default=10) # Percentage
|
|
ignore_zones: List[DetectionZone] = []
|
|
|
|
class DetectionZone(BaseModel):
|
|
name: str
|
|
polygon: List[Point] # List of x,y coordinates
|
|
|
|
class Point(BaseModel):
|
|
x: int = Field(ge=0, le=100) # Percentage of frame width
|
|
y: int = Field(ge=0, le=100) # Percentage of frame height
|
|
|
|
class NPRConfig(AnalyticsTypeConfig):
|
|
"""Number Plate Recognition configuration"""
|
|
zones: List[DetectionZone]
|
|
country_codes: List[str] = ["*"] # ["US", "DE"] or "*" for all
|
|
min_confidence: float = Field(ge=0.0, le=1.0, default=0.7)
|
|
watchlist: List[str] = [] # List of plate numbers to alert on
|
|
|
|
class OBTRACKConfig(AnalyticsTypeConfig):
|
|
"""Object Tracking configuration"""
|
|
object_types: List[str] = ["person", "vehicle"]
|
|
min_dwell_time: int = Field(ge=0, default=5) # Seconds
|
|
count_lines: List[CountLine] = []
|
|
|
|
class CountLine(BaseModel):
|
|
name: str
|
|
point_a: Point
|
|
point_b: Point
|
|
direction: str # "in", "out", "both"
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `camera_id`: Must exist and support analytics_type
|
|
- `zones`: At least one zone required when enabled=True
|
|
- `polygon`: Minimum 3 points, closed polygon
|
|
- `sensitivity`: Higher = more sensitive
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"camera_id": 5,
|
|
"analytics_type": "vmd",
|
|
"enabled": true,
|
|
"config": {
|
|
"zones": [
|
|
{
|
|
"name": "entrance",
|
|
"polygon": [
|
|
{"x": 10, "y": 10},
|
|
{"x": 90, "y": 10},
|
|
{"x": 90, "y": 90},
|
|
{"x": 10, "y": 90}
|
|
]
|
|
}
|
|
],
|
|
"sensitivity": 7,
|
|
"min_object_size": 5
|
|
},
|
|
"updated_at": "2025-12-08T10:00:00Z"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4.2 PTZPreset
|
|
|
|
Saved PTZ camera position.
|
|
|
|
**Schema**:
|
|
```python
|
|
class PTZPreset(BaseModel):
|
|
id: int # Preset ID (1-255)
|
|
camera_id: int
|
|
name: str = Field(min_length=1, max_length=50)
|
|
pan: int = Field(ge=-180, le=180) # Degrees
|
|
tilt: int = Field(ge=-90, le=90) # Degrees
|
|
zoom: int = Field(ge=0, le=100) # Percentage
|
|
created_at: datetime
|
|
created_by: UUID
|
|
updated_at: datetime
|
|
|
|
class PTZCommand(BaseModel):
|
|
"""PTZ control command"""
|
|
action: PTZAction
|
|
speed: Optional[int] = Field(default=50, ge=1, le=100)
|
|
preset_id: Optional[int] = None # For goto_preset action
|
|
|
|
class PTZAction(str, Enum):
|
|
PAN_LEFT = "pan_left"
|
|
PAN_RIGHT = "pan_right"
|
|
TILT_UP = "tilt_up"
|
|
TILT_DOWN = "tilt_down"
|
|
ZOOM_IN = "zoom_in"
|
|
ZOOM_OUT = "zoom_out"
|
|
STOP = "stop"
|
|
GOTO_PRESET = "goto_preset"
|
|
SAVE_PRESET = "save_preset"
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `id`: Unique per camera, 1-255 range
|
|
- `camera_id`: Must exist and have PTZ capability
|
|
- `name`: Unique per camera
|
|
|
|
---
|
|
|
|
## 5. Audit Entities
|
|
|
|
### 5.1 AuditLog
|
|
|
|
Audit trail for all privileged operations.
|
|
|
|
**Schema**:
|
|
```python
|
|
class AuditLog(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
timestamp: datetime
|
|
user_id: UUID
|
|
username: str
|
|
action: str # "camera.ptz", "recording.start", "user.create"
|
|
resource_type: str # "camera", "recording", "user"
|
|
resource_id: str
|
|
outcome: AuditOutcome
|
|
ip_address: str
|
|
user_agent: str
|
|
details: Optional[dict] = None # Action-specific metadata
|
|
|
|
class AuditOutcome(str, Enum):
|
|
SUCCESS = "success"
|
|
FAILURE = "failure"
|
|
PARTIAL = "partial"
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `timestamp`: Auto-set, immutable
|
|
- `user_id`: Must exist
|
|
- `action`: Format "{resource}.{operation}"
|
|
|
|
**Storage**: Append-only, never deleted, indexed by user_id and timestamp
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
|
"timestamp": "2025-12-08T14:50:00Z",
|
|
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"username": "operator1",
|
|
"action": "camera.ptz",
|
|
"resource_type": "camera",
|
|
"resource_id": "5",
|
|
"outcome": "success",
|
|
"ip_address": "192.168.1.100",
|
|
"user_agent": "Mozilla/5.0...",
|
|
"details": {
|
|
"ptz_action": "pan_left",
|
|
"speed": 50
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Unified Architecture Entities
|
|
|
|
### 6.1 GeViScope Instance
|
|
|
|
Represents a configured GeViScope server (GSCServer) connection.
|
|
|
|
**Schema**:
|
|
```python
|
|
class GeViScopeInstance(BaseModel):
|
|
id: str = Field(..., pattern="^[a-z][a-z0-9-]*$") # "main", "parking", "warehouse"
|
|
name: str = Field(min_length=1, max_length=100)
|
|
description: Optional[str] = Field(max_length=500)
|
|
host: str # Hostname or IP address
|
|
port: int = Field(default=7700, ge=1, le=65535)
|
|
username: str
|
|
# Password stored securely (not in database)
|
|
is_default: bool = False
|
|
enabled: bool = True
|
|
connection_status: ConnectionStatus
|
|
last_connected: Optional[datetime] = None
|
|
camera_count: int = 0
|
|
monitor_count: int = 0
|
|
sdk_version: Optional[str] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class ConnectionStatus(str, Enum):
|
|
CONNECTED = "connected"
|
|
DISCONNECTED = "disconnected"
|
|
CONNECTING = "connecting"
|
|
ERROR = "error"
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `id`: Lowercase alphanumeric with dashes, starts with letter
|
|
- `host`: Valid hostname or IP address
|
|
- `is_default`: Only one instance can be default
|
|
- `enabled`: Disabled instances not available for operations
|
|
|
|
**Relationships**:
|
|
- GeViScopeInstance → Camera (one-to-many)
|
|
- GeViScopeInstance → Monitor (one-to-many)
|
|
- GeViScopeInstance → CrossSwitchRoute (one-to-many)
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": "main",
|
|
"name": "Main Building",
|
|
"description": "Primary surveillance system",
|
|
"host": "localhost",
|
|
"port": 7700,
|
|
"username": "sysadmin",
|
|
"is_default": true,
|
|
"enabled": true,
|
|
"connection_status": "connected",
|
|
"last_connected": "2025-12-10T14:00:00Z",
|
|
"camera_count": 13,
|
|
"monitor_count": 256,
|
|
"sdk_version": "7.9.975.68"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6.2 Monitor (Video Output)
|
|
|
|
Represents a video output channel (logical display channel, not physical display).
|
|
|
|
**Schema**:
|
|
```python
|
|
class Monitor(BaseModel):
|
|
id: int # Monitor ID from GeViScope (1-256)
|
|
geviscope_instance_id: str # Which GeViScope instance
|
|
global_id: str # GeViScope GlobalID
|
|
name: str = Field(min_length=1, max_length=100)
|
|
description: Optional[str] = Field(max_length=500)
|
|
is_active: bool = True
|
|
is_enabled: bool = True
|
|
current_camera_id: Optional[int] = None # Currently routed camera
|
|
current_route_id: Optional[UUID] = None
|
|
status: MonitorStatus
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class MonitorStatus(str, Enum):
|
|
ONLINE = "online"
|
|
OFFLINE = "offline"
|
|
ERROR = "error"
|
|
```
|
|
|
|
**Important Notes**:
|
|
- Monitors are **logical routing channels**, not physical displays
|
|
- CrossSwitch routes video to monitors at the server level
|
|
- **Viewer applications (GSCView)** required to actually display monitor video
|
|
- Monitor enumeration may return more IDs than physical outputs exist
|
|
|
|
**Validation Rules**:
|
|
- `id`: Positive integer, unique within GeViScope instance
|
|
- `geviscope_instance_id`: Must reference existing instance
|
|
- `current_camera_id`: If set, must reference existing camera
|
|
|
|
**Relationships**:
|
|
- Monitor → GeViScopeInstance (many-to-one)
|
|
- Monitor → Camera (many-to-one, optional via current_camera_id)
|
|
- Monitor → CrossSwitchRoute (one-to-many)
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": 1,
|
|
"geviscope_instance_id": "main",
|
|
"global_id": "b8c2d4e6-f7a8-49b0-c1d2-e3f4a5b6c7d8",
|
|
"name": "Video Output 1",
|
|
"description": "Main control room display",
|
|
"is_active": true,
|
|
"is_enabled": true,
|
|
"current_camera_id": 101038,
|
|
"current_route_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
"status": "online"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6.3 CrossSwitch Route
|
|
|
|
Represents an active or historical video routing (camera → monitor).
|
|
|
|
**Schema**:
|
|
```python
|
|
class CrossSwitchRoute(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
geviscope_instance_id: str # Which GeViScope instance
|
|
camera_id: int # Video input channel
|
|
monitor_id: int # Video output channel
|
|
mode: int = 0 # 0=normal (sm_Normal)
|
|
is_active: bool = True # False when cleared
|
|
executed_at: datetime
|
|
executed_by: UUID # User who created route
|
|
executed_by_username: str
|
|
cleared_at: Optional[datetime] = None
|
|
cleared_by: Optional[UUID] = None
|
|
sdk_success: bool = True # SDK execution result
|
|
sdk_message: Optional[str] = None
|
|
camera_name: Optional[str] = None # Cached camera info
|
|
monitor_name: Optional[str] = None # Cached monitor info
|
|
|
|
class CrossSwitchRequest(BaseModel):
|
|
"""Request model for cross-switch operation"""
|
|
camera_id: int = Field(ge=1)
|
|
monitor_id: int = Field(ge=1)
|
|
mode: int = Field(default=0, ge=0, le=2)
|
|
|
|
class CrossSwitchResponse(BaseModel):
|
|
"""Response model for cross-switch operation"""
|
|
success: bool
|
|
message: str
|
|
route: CrossSwitchRoute
|
|
```
|
|
|
|
**State Transitions**:
|
|
```
|
|
Created (is_active=True) → Cleared (is_active=False)
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `camera_id`: Must exist and be online in the GeViScope instance
|
|
- `monitor_id`: Must exist in the GeViScope instance
|
|
- `mode`: 0=normal (sm_Normal), other modes reserved
|
|
- `executed_by`: Must be authenticated user
|
|
|
|
**Relationships**:
|
|
- CrossSwitchRoute → GeViScopeInstance (many-to-one)
|
|
- CrossSwitchRoute → Camera (many-to-one)
|
|
- CrossSwitchRoute → Monitor (many-to-one)
|
|
- CrossSwitchRoute → User (executed_by, many-to-one)
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
"geviscope_instance_id": "main",
|
|
"camera_id": 101038,
|
|
"monitor_id": 1,
|
|
"mode": 0,
|
|
"is_active": true,
|
|
"executed_at": "2025-12-10T14:30:00Z",
|
|
"executed_by": "550e8400-e29b-41d4-a716-446655440000",
|
|
"executed_by_username": "admin",
|
|
"sdk_success": true,
|
|
"sdk_message": "Cross-switch executed successfully",
|
|
"camera_name": "Entrance Camera",
|
|
"monitor_name": "Video Output 1"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6.4 Alarm (GeViSoft)
|
|
|
|
Represents a system-wide alarm configuration in GeViSoft.
|
|
|
|
**Schema**:
|
|
```python
|
|
class Alarm(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
alarm_id: int # GeViSoft alarm ID
|
|
name: str = Field(min_length=1, max_length=100)
|
|
description: Optional[str] = Field(max_length=500)
|
|
priority: AlarmPriority
|
|
monitor_group_id: Optional[int] = None
|
|
cameras: List[int] = [] # Camera channels
|
|
start_actions: List[str] = [] # Actions on alarm start
|
|
stop_actions: List[str] = [] # Actions on alarm stop
|
|
acknowledge_actions: List[str] = [] # Actions on acknowledge
|
|
is_active: bool = False
|
|
triggered_at: Optional[datetime] = None
|
|
acknowledged_at: Optional[datetime] = None
|
|
acknowledged_by: Optional[UUID] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class AlarmPriority(str, Enum):
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
CRITICAL = "critical"
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `alarm_id`: Positive integer, unique
|
|
- `cameras`: All camera IDs must exist
|
|
- `start_actions`: Valid GeViSoft action strings
|
|
- `is_active`: Set true when triggered, false when acknowledged/stopped
|
|
|
|
**Relationships**:
|
|
- Alarm → Camera (many-to-many via cameras list)
|
|
- Alarm → User (acknowledged_by, many-to-one, optional)
|
|
|
|
---
|
|
|
|
### 6.5 Action Mapping
|
|
|
|
Represents automation rules in GeViSoft (input action → output actions).
|
|
|
|
**Schema**:
|
|
```python
|
|
class ActionMapping(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
name: str = Field(min_length=1, max_length=100)
|
|
description: Optional[str] = Field(max_length=500)
|
|
input_action: str # GeViSoft action that triggers mapping
|
|
output_actions: List[str] # Actions to execute
|
|
geviscope_instance_scope: Optional[str] = None # Limit to specific instance
|
|
enabled: bool = True
|
|
execution_count: int = 0
|
|
last_executed: Optional[datetime] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
created_by: UUID
|
|
|
|
class ActionMappingExecution(BaseModel):
|
|
"""Execution log for action mappings"""
|
|
id: UUID = Field(default_factory=uuid4)
|
|
mapping_id: UUID
|
|
input_action: str
|
|
output_actions_executed: List[str]
|
|
success: bool
|
|
error_message: Optional[str] = None
|
|
executed_at: datetime
|
|
```
|
|
|
|
**Validation Rules**:
|
|
- `input_action`: Valid GeViSoft action format
|
|
- `output_actions`: Valid GeViSoft action formats
|
|
- `geviscope_instance_scope`: If set, must reference existing instance
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
|
"name": "Motion Detection Alert",
|
|
"description": "Route cameras to monitors when motion detected",
|
|
"input_action": "VMD_Start(101038)",
|
|
"output_actions": [
|
|
"CrossSwitch(101038, 1, 0)",
|
|
"SendMail(security@example.com, Motion Detected)"
|
|
],
|
|
"geviscope_instance_scope": "main",
|
|
"enabled": true,
|
|
"execution_count": 42,
|
|
"last_executed": "2025-12-10T14:00:00Z"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6.6 G-Core Server
|
|
|
|
Represents a configured G-Core server (remote surveillance server connection) in GeViSoft.
|
|
|
|
**Schema**:
|
|
```python
|
|
class GCoreServer(BaseModel):
|
|
id: str = Field(..., pattern="^[0-9]+$") # Numeric string ID
|
|
alias: str = Field(min_length=1, max_length=100) # Display name
|
|
host: str = Field(min_length=1, max_length=255) # IP address or hostname
|
|
user: str = Field(default="admin", max_length=100) # Username
|
|
password: str = Field(max_length=100) # Password (encrypted in storage)
|
|
enabled: bool = True # Enable/disable server
|
|
deactivate_echo: bool = False # Deactivate echo
|
|
deactivate_live_check: bool = False # Deactivate live check
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
created_by: UUID
|
|
|
|
class GCoreServerInput(BaseModel):
|
|
"""Request model for creating/updating G-Core server"""
|
|
alias: str = Field(min_length=1, max_length=100)
|
|
host: str = Field(min_length=1, max_length=255)
|
|
user: str = Field(default="admin", max_length=100)
|
|
password: str = Field(max_length=100)
|
|
enabled: bool = True
|
|
deactivate_echo: bool = False
|
|
deactivate_live_check: bool = False
|
|
```
|
|
|
|
**Implementation Notes**:
|
|
- ID is auto-incremented based on highest existing numeric server ID
|
|
- Field order in configuration tree must be: Alias, DeactivateEcho, DeactivateLiveCheck, Enabled, Host, Password, User
|
|
- Bool fields must use type code 1 (bool) not type code 4 (int32) for GeViSet compatibility
|
|
- Password stored as plain text in GeViSoft configuration (SDK limitation)
|
|
|
|
**Validation Rules**:
|
|
- `id`: Numeric string, auto-generated on CREATE
|
|
- `alias`: Required, display name for server
|
|
- `host`: Required, valid IP address or hostname
|
|
- `user`: Defaults to "admin" if not provided
|
|
- `enabled`: Controls whether server is active
|
|
|
|
**Relationships**:
|
|
- GCoreServer → User (created_by, many-to-one)
|
|
- Configuration stored in GeViSoft .set file under GeViGCoreServer folder
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"id": "2",
|
|
"alias": "Remote Office Server",
|
|
"host": "192.168.1.100",
|
|
"user": "admin",
|
|
"password": "secure_password",
|
|
"enabled": true,
|
|
"deactivate_echo": false,
|
|
"deactivate_live_check": false,
|
|
"created_at": "2025-12-16T10:00:00Z"
|
|
}
|
|
```
|
|
|
|
**CRUD Operations** (2025-12-16):
|
|
- ✅ CREATE: Working - creates server with auto-incremented ID
|
|
- ✅ READ: Working - reads all servers or single server by ID
|
|
- ⚠️ UPDATE: Known bug - requires fix for "Server ID is required" error
|
|
- ✅ DELETE: Working - deletes server by ID
|
|
|
|
**Critical Implementation Details**:
|
|
- **Cascade Deletion Prevention**: When deleting multiple servers, always delete in reverse order (highest ID first) to prevent ID shifting
|
|
- **Bool Type Handling**: Must write as bool type (type code 1) not int32, even though GeViSoft stores as int32
|
|
- **SetupClient Required**: All configuration changes must use SetupClient for download/upload to ensure atomicity
|
|
|
|
---
|
|
|
|
## Entity Relationships Diagram
|
|
|
|
```
|
|
┌────────────────────┐
|
|
│ GeViScopeInstance │──┐
|
|
└────────────────────┘ │
|
|
│1:N
|
|
┌────────────┴──────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌────────────┐ ┌────────────┐ ┌─────────────────┐
|
|
│ Camera │──1:N──│ Stream │ │ Monitor │
|
|
└─────┬──────┘ └─────────┘ └────────┬────────┘
|
|
│ │
|
|
│1:N │1:N
|
|
▼ ▼
|
|
┌─────────────────┐ ┌───────────────────┐
|
|
│ AnalyticsConfig │ │ CrossSwitchRoute │
|
|
└─────────────────┘ └─────────┬─────────┘
|
|
│ │
|
|
│1:N │N:1
|
|
▼ │
|
|
┌─────────────┐ │
|
|
│ PTZPreset │ │
|
|
└─────────────┘ ▼
|
|
┌─────────┐
|
|
┌─────────┐ ┌─────────────┐ │ User │──1:N──┌──────────────────┐
|
|
│ User │──1:N──│ Session │ └────┬────┘ │ EventSubscription│
|
|
└────┬────┘ └─────────────┘ │ └──────────────────┘
|
|
│ │1:N
|
|
│1:N │
|
|
▼ ▼
|
|
┌────────────┐ ┌──────────────┐
|
|
│ AuditLog │ │ Alarm │
|
|
└────────────┘ └──────────────┘
|
|
│M:N
|
|
▼
|
|
┌────────────┐
|
|
│ Camera │
|
|
└────────────┘
|
|
|
|
┌──────────────────┐ ┌──────────┐
|
|
│ ActionMapping │──1:N──│ Event │
|
|
└──────────────────┘ └────┬─────┘
|
|
│N:1
|
|
▼
|
|
┌──────────────┐
|
|
│ Recording │
|
|
└──────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Validation Rules Summary
|
|
|
|
| Entity | Key Validations |
|
|
|--------|-----------------|
|
|
| User | Unique username/email, valid role, bcrypt password |
|
|
| Session | Valid JWT, IP address, TTL enforced |
|
|
| GeViScopeInstance | Unique ID, valid host, only one default |
|
|
| Camera | Valid channel ID, status from SDK, capabilities match |
|
|
| Monitor | Valid ID within instance, scoped by instance ID |
|
|
| CrossSwitchRoute | Camera/monitor exist and online, valid user |
|
|
| Stream | Camera online, token authentication, supported formats |
|
|
| Recording | Valid time range, camera exists, ring buffer aware |
|
|
| Event | Valid type, severity, camera permissions |
|
|
| EventSubscription | User has camera permissions |
|
|
| AnalyticsConfig | Camera supports type, valid zones/settings |
|
|
| PTZPreset | Camera has PTZ, valid coordinates |
|
|
| Alarm (GeViSoft) | Valid alarm ID, cameras exist, valid actions |
|
|
| ActionMapping | Valid action formats, instance scope if specified |
|
|
| GCoreServer | Numeric ID, valid host, bool type handling |
|
|
| AuditLog | Immutable, complete metadata |
|
|
|
|
---
|
|
|
|
## State Machine Definitions
|
|
|
|
### Camera Status State Machine
|
|
```
|
|
[Offline] ──detect_online──▶ [Online] ──detect_offline──▶ [Offline]
|
|
│ ▲ │
|
|
│ └───recover_error────────┘
|
|
│
|
|
└──detect_error──▶ [Error]
|
|
│
|
|
┌─────────────────────┘
|
|
▼
|
|
[Maintenance] ──restore──▶ [Online]
|
|
```
|
|
|
|
### Recording Status State Machine
|
|
```
|
|
[Recording] ──complete──▶ [Completed] ──export──▶ [Exporting] ──finish──▶ [Exported]
|
|
│ │
|
|
└──error──▶ [Failed] ◀──────────────────────────┘
|
|
```
|
|
|
|
### Stream Status State Machine
|
|
```
|
|
[Initializing] ──ready──▶ [Active] ──pause──▶ [Paused] ──resume──▶ [Active]
|
|
│ │
|
|
└──stop──▶ [Stopped] ◀──────────────────┘
|
|
│
|
|
└──error──▶ [Error]
|
|
```
|
|
|
|
---
|
|
|
|
## CrossSwitch Route State Machine
|
|
```
|
|
[Created] (is_active=True) ──clear_monitor──▶ [Cleared] (is_active=False)
|
|
│
|
|
└──new_crossswitch──▶ [Replaced] (old route cleared, new route created)
|
|
```
|
|
|
|
---
|
|
|
|
## Monitor Assignment State Machine
|
|
```
|
|
[Empty] (no camera) ──crossswitch──▶ [Assigned] (camera routed)
|
|
│
|
|
└──clear──▶ [Empty]
|
|
│
|
|
└──crossswitch──▶ [Reassigned] (new camera)
|
|
```
|
|
|
|
---
|
|
|
|
**Phase 1 Status**: ✅ Data model updated with unified architecture and configuration management
|
|
**Last Updated**: 2025-12-16
|
|
**Next**: Implement multi-instance SDK bridge connections
|