Complete Phase 0 and Phase 1 design documentation

- Add comprehensive research.md with SDK integration decisions
- Add complete data-model.md with 7 entities and relationships
- Add OpenAPI 3.0 specification (contracts/openapi.yaml)
- Add developer quickstart.md guide
- Add comprehensive tasks.md with 215 tasks organized by user story
- Update plan.md with complete technical context
- Add SDK_INTEGRATION_LESSONS.md capturing critical knowledge
- Add .gitignore for Python and C# projects
- Include GeViScopeConfigReader and GeViSoftConfigReader tools

Phase 1 Design Complete:
 Architecture: Python FastAPI + C# gRPC Bridge + GeViScope SDK
 10 user stories mapped to tasks (MVP = US1-4)
 Complete API contract with 17 endpoints
 Data model with User, Camera, Stream, Event, Recording, Analytics
 TDD approach enforced with 80+ test tasks

Ready for Phase 2: Implementation

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Geutebruck API Developer
2025-12-09 07:39:55 +01:00
parent edf22b09c2
commit dd2278b39a
25 changed files with 6832 additions and 387 deletions

View File

@@ -0,0 +1,768 @@
# 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
}
}
```
---
## Entity Relationships Diagram
```
┌─────────┐ ┌─────────────┐ ┌────────────┐
│ User │──1:N──│ Session │ │ AuditLog │
└────┬────┘ └─────────────┘ └─────┬──────┘
│ │
│1:N │N:1
│ │
▼ │
┌─────────────────┐ │
│ EventSubscription│ │
└─────────────────┘ │
┌────────────┐ ┌─────────┐ ┌──────▼─────┐
│ Camera │──1:N──│ Stream │ │ Recording │
└─────┬──────┘ └─────────┘ └──────┬─────┘
│ │
│1:N │N:1
▼ │
┌─────────────────┐ │
│ AnalyticsConfig │ │
└─────────────────┘ │
│ │
│1:N │
▼ ▼
┌─────────────┐ ┌────────────────────────────┐
│ PTZPreset │ │ Event │
└─────────────┘ └────────────────────────────┘
```
---
## Validation Rules Summary
| Entity | Key Validations |
|--------|-----------------|
| User | Unique username/email, valid role, bcrypt password |
| Session | Valid JWT, IP address, TTL enforced |
| Camera | Valid channel ID, status from SDK, capabilities match |
| 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 |
| 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]
```
---
**Phase 1 Status**: ✅ Data model complete
**Next**: Generate OpenAPI contracts