docs: Update specifications to reflect configuration management implementation
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>
This commit is contained in:
@@ -681,35 +681,428 @@ class AuditOutcome(str, Enum):
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────────┐ ┌────────────┐
|
||||
│ User │──1:N──│ Session │ │ AuditLog │
|
||||
└────┬────┘ └─────────────┘ └─────┬──────┘
|
||||
│ │
|
||||
│1:N │N:1
|
||||
│ │
|
||||
▼ │
|
||||
┌─────────────────┐ │
|
||||
│ EventSubscription│ │
|
||||
└─────────────────┘ │
|
||||
│
|
||||
┌────────────┐ ┌─────────┐ ┌──────▼─────┐
|
||||
│ Camera │──1:N──│ Stream │ │ Recording │
|
||||
└─────┬──────┘ └─────────┘ └──────┬─────┘
|
||||
│ │
|
||||
│1:N │N:1
|
||||
▼ │
|
||||
┌─────────────────┐ │
|
||||
│ AnalyticsConfig │ │
|
||||
└─────────────────┘ │
|
||||
│ │
|
||||
│1:N │
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌────────────────────────────┐
|
||||
│ PTZPreset │ │ Event │
|
||||
└─────────────┘ └────────────────────────────┘
|
||||
┌────────────────────┐
|
||||
│ 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 │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
@@ -720,13 +1113,19 @@ class AuditOutcome(str, Enum):
|
||||
|--------|-----------------|
|
||||
| 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 |
|
||||
|
||||
---
|
||||
@@ -764,5 +1163,26 @@ class AuditOutcome(str, Enum):
|
||||
|
||||
---
|
||||
|
||||
**Phase 1 Status**: ✅ Data model complete
|
||||
**Next**: Generate OpenAPI contracts
|
||||
## 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
|
||||
|
||||
Reference in New Issue
Block a user