mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 17:16:16 +00:00
The TestDocsConsistency walk only iterated [".", "guides", "reference",
"analysis"] — concepts/ was silently invisible, which is why
amazon-music-oauth.md slipped into the tree without a SUMMARY entry.
Refactored to walk the entire docs/ tree, with a small dirsToSkip
allow-list (_includes, archive, diagrams, images) for asset trees.
New top-level narrative directories are picked up automatically;
only asset dirs need an explicit entry.
The wider walk surfaced six previously-hidden concepts/* files. Five
older planning artefacts ("Enhanced State Management System",
"Upstream Bose Service Simulation") moved into docs/archive/ where
the dirsToSkip already excludes them; concepts/README.md renamed to
upstream-service-simulation-overview.md since "README.md" inside
archive/ would be misleading. Spotify Overview and Amazon Music
OAuth are user-facing narrative docs and are now linked under
Concepts in SUMMARY.md.
Note: concepts/streborn-patterns.md is internal review notes (its
own opening line says so) and is currently unlinked from SUMMARY.md;
will be handled separately by the maintainer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
984 lines
30 KiB
Markdown
984 lines
30 KiB
Markdown
# Technical Specification - Enhanced State Management System
|
|
|
|
## Table of Contents
|
|
|
|
1. [System Architecture](#system-architecture)
|
|
2. [Data Models](#data-models)
|
|
3. [API Specifications](#api-specifications)
|
|
4. [File Format Specifications](#file-format-specifications)
|
|
5. [State Machine Definitions](#state-machine-definitions)
|
|
6. [Event Processing](#event-processing)
|
|
7. [Performance Requirements](#performance-requirements)
|
|
8. [Security Considerations](#security-considerations)
|
|
9. [Error Handling](#error-handling)
|
|
10. [Monitoring and Observability](#monitoring-and-observability)
|
|
|
|
## System Architecture
|
|
|
|
### Component Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ SoundTouch Service │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ HTTP Router & Middleware │
|
|
│ ├── Recorder Middleware │
|
|
│ ├── Disparity Detection │
|
|
│ └── Health Check Middleware │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ Service Layer │
|
|
│ ├── Account Manager ├── Lifecycle Manager │
|
|
│ ├── Migration Controller ├── Data Source Router │
|
|
│ ├── Event Processor ├── Health Monitor │
|
|
│ └── Export Manager └── Analytics Engine │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ Data Layer │
|
|
│ ├── Enhanced DataStore ├── Event Store │
|
|
│ └── Configuration Store └── Session Store │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ External Integrations │
|
|
│ ├── Device Discovery ├── BMX/TuneIn Services │
|
|
│ └── SSH/Setup Manager │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Package Structure
|
|
|
|
```
|
|
pkg/service/
|
|
├── account/ # Account management
|
|
│ ├── manager.go
|
|
│ ├── persistence.go
|
|
│ └── validation.go
|
|
├── lifecycle/ # Device lifecycle management
|
|
│ ├── manager.go
|
|
│ ├── states.go
|
|
│ ├── transitions.go
|
|
│ └── events.go
|
|
├── migration/ # Enhanced migration (extends existing)
|
|
│ ├── controller.go
|
|
│ ├── strategies.go
|
|
│ └── progress.go
|
|
├── datasource/ # Data source routing
|
|
│ ├── router.go
|
|
│ ├── preferences.go
|
|
│ └── fallback.go
|
|
├── events/ # Event processing system
|
|
│ ├── processor.go
|
|
│ ├── queue.go
|
|
│ └── storage.go
|
|
├── health/ # System monitoring
|
|
│ ├── monitor.go
|
|
│ ├── metrics.go
|
|
│ └── alerts.go
|
|
└── export/ # Data export and backup
|
|
├── exporter.go
|
|
├── formats.go
|
|
└── backup.go
|
|
```
|
|
|
|
## Data Models
|
|
|
|
### Account Model
|
|
|
|
```go
|
|
type Account struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Status AccountStatus `json:"status"`
|
|
DeviceCount int `json:"device_count"`
|
|
MigrationInfo *MigrationInfo `json:"migration_info,omitempty"`
|
|
BoseAccountID string `json:"bose_account_id,omitempty"`
|
|
DataSources DataSourceConfig `json:"data_sources"`
|
|
Settings AccountSettings `json:"settings"`
|
|
}
|
|
|
|
type AccountStatus string
|
|
const (
|
|
AccountStatusActive AccountStatus = "active"
|
|
AccountStatusMigrating AccountStatus = "migrating"
|
|
AccountStatusSuspended AccountStatus = "suspended"
|
|
AccountStatusArchived AccountStatus = "archived"
|
|
)
|
|
|
|
type MigrationInfo struct {
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
DevicesMigrated int `json:"devices_migrated"`
|
|
DevicesPending int `json:"devices_pending"`
|
|
Strategy string `json:"strategy"`
|
|
RollbackData string `json:"rollback_data,omitempty"`
|
|
}
|
|
|
|
type DataSourceConfig struct {
|
|
Local bool `json:"local"`
|
|
Primary string `json:"primary"` // "local" or "bose"
|
|
}
|
|
|
|
type AccountSettings struct {
|
|
AutoMigration bool `json:"auto_migration"`
|
|
RetentionDays int `json:"retention_days"`
|
|
}
|
|
```
|
|
|
|
### Device Lifecycle Model
|
|
|
|
```go
|
|
type DeviceLifecycle struct {
|
|
DeviceID string `json:"device_id"`
|
|
AccountID string `json:"account_id"`
|
|
State DeviceState `json:"state"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
StateHistory []StateTransition `json:"state_history"`
|
|
Metadata DeviceMetadata `json:"metadata"`
|
|
DataSources DataSourceConfig `json:"data_sources"`
|
|
Migration *DeviceMigration `json:"migration,omitempty"`
|
|
Health DeviceHealth `json:"health"`
|
|
}
|
|
|
|
type DeviceState string
|
|
const (
|
|
DeviceStateUnregistered DeviceState = "unregistered"
|
|
DeviceStateDiscovered DeviceState = "discovered"
|
|
DeviceStateRegistering DeviceState = "registering"
|
|
DeviceStateActive DeviceState = "active"
|
|
DeviceStateMigrating DeviceState = "migrating"
|
|
DeviceStateOffline DeviceState = "offline"
|
|
DeviceStateError DeviceState = "error"
|
|
DeviceStateRetired DeviceState = "retired"
|
|
)
|
|
|
|
type StateTransition struct {
|
|
From DeviceState `json:"from"`
|
|
To DeviceState `json:"to"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Reason string `json:"reason"`
|
|
Source string `json:"source"`
|
|
Context map[string]interface{} `json:"context,omitempty"`
|
|
}
|
|
|
|
type DeviceMetadata struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
SerialNumber string `json:"serial_number"`
|
|
FirmwareVersion string `json:"firmware_version"`
|
|
MACAddress string `json:"mac_address"`
|
|
IPAddress string `json:"ip_address"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
IsLegacyID bool `json:"is_legacy_id"`
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
}
|
|
|
|
type DeviceMigration struct {
|
|
FromBoseAccount string `json:"from_bose_account,omitempty"`
|
|
MigratedAt *time.Time `json:"migrated_at,omitempty"`
|
|
Method string `json:"method"`
|
|
RollbackAvailable bool `json:"rollback_available"`
|
|
DataPreserved []string `json:"data_preserved"`
|
|
}
|
|
|
|
type DeviceHealth struct {
|
|
Status string `json:"status"` // "healthy", "warning", "error"
|
|
LastCheck time.Time `json:"last_check"`
|
|
ResponseTime int `json:"response_time_ms"`
|
|
Connectivity string `json:"connectivity"` // "online", "offline", "intermittent"
|
|
ErrorCount int `json:"error_count"`
|
|
LastError string `json:"last_error,omitempty"`
|
|
}
|
|
```
|
|
|
|
### Event Model
|
|
|
|
```go
|
|
type DeviceEvent struct {
|
|
ID string `json:"id"`
|
|
DeviceID string `json:"device_id"`
|
|
AccountID string `json:"account_id"`
|
|
Type DeviceEventType `json:"type"`
|
|
Data map[string]interface{} `json:"data"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Source EventSource `json:"source"`
|
|
Processed bool `json:"processed"`
|
|
Context EventContext `json:"context"`
|
|
}
|
|
|
|
type DeviceEventType string
|
|
const (
|
|
EventTypeNowPlaying DeviceEventType = "now_playing"
|
|
EventTypePresetChanged DeviceEventType = "preset_changed"
|
|
EventTypeVolumeChanged DeviceEventType = "volume_changed"
|
|
EventTypeSourceChanged DeviceEventType = "source_changed"
|
|
EventTypeDeviceOnline DeviceEventType = "device_online"
|
|
EventTypeDeviceOffline DeviceEventType = "device_offline"
|
|
EventTypeZoneChanged DeviceEventType = "zone_changed"
|
|
EventTypeDisparityFound DeviceEventType = "disparity_found"
|
|
EventTypeMigrationStart DeviceEventType = "migration_start"
|
|
EventTypeMigrationEnd DeviceEventType = "migration_end"
|
|
EventTypeHealthCheck DeviceEventType = "health_check"
|
|
EventTypeErrorOccurred DeviceEventType = "error_occurred"
|
|
)
|
|
|
|
type EventSource string
|
|
const (
|
|
EventSourceWebSocket EventSource = "websocket"
|
|
EventSourceDiscovery EventSource = "discovery"
|
|
EventSourceSystem EventSource = "system"
|
|
EventSourceAPI EventSource = "api"
|
|
EventSourceUser EventSource = "user"
|
|
)
|
|
|
|
type EventContext struct {
|
|
RequestID string `json:"request_id,omitempty"`
|
|
UserAgent string `json:"user_agent,omitempty"`
|
|
IPAddress string `json:"ip_address,omitempty"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
Additional map[string]interface{} `json:"additional,omitempty"`
|
|
}
|
|
```
|
|
|
|
### Disparity Model
|
|
|
|
```go
|
|
type Disparity struct {
|
|
ID string `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
DeviceID string `json:"device_id"`
|
|
AccountID string `json:"account_id"`
|
|
Endpoint string `json:"endpoint"`
|
|
Type DisparityType `json:"type"`
|
|
Severity DisparitySeverity `json:"severity"`
|
|
LocalHash string `json:"local_hash"`
|
|
UpstreamHash string `json:"upstream_hash"`
|
|
Details DisparityDetails `json:"details"`
|
|
Context map[string]interface{} `json:"context"`
|
|
Resolved bool `json:"resolved"`
|
|
}
|
|
|
|
type DisparityType string
|
|
const (
|
|
DisparityTypeContentMismatch DisparityType = "content_mismatch"
|
|
DisparityTypeStructureDiff DisparityType = "structure_diff"
|
|
DisparityTypeTimestampFormat DisparityType = "timestamp_format"
|
|
DisparityTypeFieldMissing DisparityType = "field_missing"
|
|
DisparityTypeValueMismatch DisparityType = "value_mismatch"
|
|
DisparityTypeCountMismatch DisparityType = "count_mismatch"
|
|
)
|
|
|
|
type DisparitySeverity string
|
|
const (
|
|
DisparitySeverityLow DisparitySeverity = "low"
|
|
DisparitySeverityMedium DisparitySeverity = "medium"
|
|
DisparitySeverityHigh DisparitySeverity = "high"
|
|
DisparitySeverityCritical DisparitySeverity = "critical"
|
|
)
|
|
|
|
type DisparityDetails struct {
|
|
FieldPath string `json:"field_path"`
|
|
LocalValue interface{} `json:"local_value"`
|
|
UpstreamValue interface{} `json:"upstream_value"`
|
|
Description string `json:"description"`
|
|
}
|
|
```
|
|
|
|
## API Specifications
|
|
|
|
### Account Management APIs
|
|
|
|
#### Create Account
|
|
```http
|
|
POST /api/v1/accounts
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"name": "User Account",
|
|
"email": "user@example.com",
|
|
"settings": {
|
|
"auto_migration": false,
|
|
"retention_days": 30
|
|
}
|
|
}
|
|
|
|
Response: 201 Created
|
|
{
|
|
"id": "acc_12345",
|
|
"name": "User Account",
|
|
"email": "user@example.com",
|
|
"created_at": "2024-01-20T10:00:00Z",
|
|
"status": "active",
|
|
"device_count": 0
|
|
}
|
|
```
|
|
|
|
#### Get Account
|
|
```http
|
|
GET /api/v1/accounts/{account_id}
|
|
|
|
Response: 200 OK
|
|
{
|
|
"id": "acc_12345",
|
|
"name": "User Account",
|
|
"status": "active",
|
|
"device_count": 2,
|
|
"migration_info": {
|
|
"started_at": "2024-01-18T09:00:00Z",
|
|
"devices_migrated": 1,
|
|
"devices_pending": 1
|
|
},
|
|
"data_sources": {
|
|
"local": true,
|
|
"primary": "bose"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### List Accounts
|
|
```http
|
|
GET /api/v1/accounts?status=active&limit=10&offset=0
|
|
|
|
Response: 200 OK
|
|
{
|
|
"accounts": [...],
|
|
"total": 5,
|
|
"limit": 10,
|
|
"offset": 0
|
|
}
|
|
```
|
|
|
|
### Device Lifecycle APIs
|
|
|
|
#### Register Device
|
|
```http
|
|
POST /api/v1/accounts/{account_id}/devices
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"device_id": "AABBCCDDEEFF",
|
|
"name": "Living Room Speaker",
|
|
"registration_type": "fresh"
|
|
}
|
|
|
|
Response: 201 Created
|
|
{
|
|
"device_id": "AABBCCDDEEFF",
|
|
"account_id": "acc_12345",
|
|
"state": "registering",
|
|
"created_at": "2024-01-20T10:00:00Z"
|
|
}
|
|
```
|
|
|
|
#### Get Device State
|
|
```http
|
|
GET /api/v1/accounts/{account_id}/devices/{device_id}/state
|
|
|
|
Response: 200 OK
|
|
{
|
|
"device_id": "AABBCCDDEEFF",
|
|
"account_id": "acc_12345",
|
|
"state": "active",
|
|
"metadata": {
|
|
"name": "Living Room Speaker",
|
|
"type": "SoundTouch 30",
|
|
"last_seen": "2024-01-20T15:30:00Z"
|
|
},
|
|
"health": {
|
|
"status": "healthy",
|
|
"connectivity": "online",
|
|
"response_time": 45
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Migrate Device
|
|
```http
|
|
POST /api/v1/accounts/{account_id}/devices/{device_id}/migrate
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"from_bose_account": "bose-acc-xyz",
|
|
"preserve_data": true,
|
|
"method": "gradual"
|
|
}
|
|
|
|
Response: 202 Accepted
|
|
{
|
|
"migration_id": "mig_67890",
|
|
"status": "started",
|
|
"estimated_completion": "2024-01-20T11:00:00Z"
|
|
}
|
|
```
|
|
|
|
### Event APIs
|
|
|
|
#### Get Device Events
|
|
```http
|
|
GET /api/v1/accounts/{account_id}/devices/{device_id}/events?since=2024-01-20T00:00:00Z&type=now_playing&limit=50
|
|
|
|
Response: 200 OK
|
|
{
|
|
"events": [
|
|
{
|
|
"id": "evt_12345",
|
|
"type": "now_playing",
|
|
"timestamp": "2024-01-20T15:30:00Z",
|
|
"data": {
|
|
"source": "SPOTIFY",
|
|
"track": "Song Name",
|
|
"artist": "Artist Name"
|
|
}
|
|
}
|
|
],
|
|
"total": 125,
|
|
"has_more": true
|
|
}
|
|
```
|
|
|
|
#### Stream Events
|
|
```http
|
|
GET /api/v1/accounts/{account_id}/devices/{device_id}/events/stream
|
|
Accept: text/event-stream
|
|
|
|
Response: 200 OK
|
|
Content-Type: text/event-stream
|
|
|
|
data: {"id":"evt_12346","type":"volume_changed","timestamp":"2024-01-20T15:31:00Z","data":{"volume":50}}
|
|
|
|
data: {"id":"evt_12347","type":"now_playing","timestamp":"2024-01-20T15:32:00Z","data":{"source":"TUNEIN"}}
|
|
```
|
|
|
|
### Monitoring APIs
|
|
|
|
#### System Health
|
|
```http
|
|
GET /api/v1/system/health
|
|
|
|
Response: 200 OK
|
|
{
|
|
"status": "healthy",
|
|
"timestamp": "2024-01-20T15:30:00Z",
|
|
"services": {
|
|
"account_manager": "healthy",
|
|
"lifecycle_manager": "healthy",
|
|
"event_processor": "healthy"
|
|
},
|
|
"statistics": {
|
|
"total_accounts": 5,
|
|
"total_devices": 12,
|
|
"active_devices": 10,
|
|
"events_processed_24h": 1547
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Disparity Analysis
|
|
```http
|
|
GET /api/v1/system/disparities?since=2024-01-20T00:00:00Z&severity=high
|
|
|
|
Response: 200 OK
|
|
{
|
|
"disparities": [
|
|
{
|
|
"id": "disp_12345",
|
|
"timestamp": "2024-01-20T14:30:00Z",
|
|
"endpoint": "/v1/presets",
|
|
"type": "count_mismatch",
|
|
"severity": "high",
|
|
"details": {
|
|
"field_path": "preset_count",
|
|
"local_value": 5,
|
|
"upstream_value": 4
|
|
}
|
|
}
|
|
],
|
|
"summary": {
|
|
"total": 15,
|
|
"by_severity": {
|
|
"high": 2,
|
|
"medium": 8,
|
|
"low": 5
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## File Format Specifications
|
|
|
|
### Account Metadata (account.json)
|
|
```json
|
|
{
|
|
"version": "1.0",
|
|
"id": "acc_12345",
|
|
"name": "User Account",
|
|
"email": "user@example.com",
|
|
"created_at": "2024-01-20T10:00:00Z",
|
|
"updated_at": "2024-01-20T15:30:00Z",
|
|
"status": "active",
|
|
"device_count": 2,
|
|
"migration_info": {
|
|
"started_at": "2024-01-18T09:00:00Z",
|
|
"devices_migrated": 1,
|
|
"devices_pending": 1,
|
|
"strategy": "gradual"
|
|
},
|
|
"bose_account_id": "bose-original-id",
|
|
"data_sources": {
|
|
"local": true,
|
|
"primary": "bose"
|
|
},
|
|
"settings": {
|
|
"auto_migration": false,
|
|
"retention_days": 30
|
|
}
|
|
}
|
|
```
|
|
|
|
### Device Lifecycle (lifecycle.json)
|
|
```json
|
|
{
|
|
"version": "1.0",
|
|
"device_id": "AABBCCDDEEFF",
|
|
"account_id": "acc_12345",
|
|
"state": "active",
|
|
"created_at": "2024-01-20T10:00:00Z",
|
|
"updated_at": "2024-01-20T15:30:00Z",
|
|
"state_history": [
|
|
{
|
|
"from": "unregistered",
|
|
"to": "discovered",
|
|
"timestamp": "2024-01-20T10:00:00Z",
|
|
"reason": "mdns_discovery",
|
|
"source": "discovery",
|
|
"context": {
|
|
"ip_address": "192.0.2.100",
|
|
"discovery_method": "mdns"
|
|
}
|
|
},
|
|
{
|
|
"from": "discovered",
|
|
"to": "active",
|
|
"timestamp": "2024-01-20T10:05:00Z",
|
|
"reason": "registration_complete",
|
|
"source": "system"
|
|
}
|
|
],
|
|
"metadata": {
|
|
"name": "Living Room Speaker",
|
|
"type": "SoundTouch 30",
|
|
"serial_number": "I6332527703739342000020",
|
|
"firmware_version": "4.8.1.25341.2677643.1597353330",
|
|
"mac_address": "AA:BB:CC:DD:EE:FF",
|
|
"ip_address": "192.0.2.100",
|
|
"last_seen": "2024-01-20T15:30:00Z",
|
|
"is_legacy_id": false,
|
|
"capabilities": ["multiroom", "bluetooth", "aux"]
|
|
},
|
|
"data_sources": {
|
|
"presets": "local",
|
|
"recents": "local",
|
|
"sources": "local"
|
|
},
|
|
"migration": {
|
|
"from_bose_account": "bose-acc-xyz",
|
|
"migrated_at": "2024-01-18T14:30:00Z",
|
|
"method": "gradual",
|
|
"rollback_available": true,
|
|
"data_preserved": ["presets", "recents", "sources"]
|
|
},
|
|
"health": {
|
|
"status": "healthy",
|
|
"last_check": "2024-01-20T15:30:00Z",
|
|
"response_time": 45,
|
|
"connectivity": "online",
|
|
"error_count": 0
|
|
}
|
|
}
|
|
```
|
|
|
|
### Event Log Format (events.log)
|
|
```
|
|
# SoundTouch Service Event Log - Device AABBCCDDEEFF
|
|
# Format: TIMESTAMP|EVENT_ID|EVENT_TYPE|SOURCE|DATA_JSON
|
|
# Version: 1.0
|
|
|
|
2024-01-20T15:30:00.123Z|evt_12345|now_playing|websocket|{"source":"SPOTIFY","track":"Song Name","artist":"Artist Name","album":"Album Name"}
|
|
2024-01-20T15:30:30.456Z|evt_12346|volume_changed|websocket|{"volume":45,"muted":false,"previous_volume":40}
|
|
2024-01-20T15:31:00.789Z|evt_12347|preset_selected|websocket|{"preset":1,"source":"SPOTIFY","location":"spotify:track:123abc"}
|
|
2024-01-20T15:32:00.345Z|evt_12349|health_check|system|{"response_time":42,"status":"healthy","connectivity":"online"}
|
|
```
|
|
|
|
### Disparity Log Format (disparities.log)
|
|
```
|
|
# SoundTouch Service Disparity Log
|
|
# Format: TIMESTAMP|DISPARITY_ID|DEVICE_ID|ACCOUNT_ID|ENDPOINT|TYPE|SEVERITY|DETAILS_JSON
|
|
# Version: 1.0
|
|
|
|
2024-01-20T15:31:15.012Z|disp_12345|AABBCCDDEEFF|acc_12345|/v1/presets|count_mismatch|medium|{"field_path":"preset_count","local_value":5,"upstream_value":4,"description":"Local has one additional preset"}
|
|
2024-01-20T15:32:45.678Z|disp_12346|AABBCCDDEEFF|acc_12345|/v1/recents|timestamp_format|low|{"field_path":"recent[0].utc_time","local_value":"2024-01-20T15:30:00Z","upstream_value":"1705761000","description":"Timestamp format difference"}
|
|
2024-01-20T15:35:20.901Z|disp_12347|B92C7B647B09|acc_12345|/v1/account/full|structure_diff|high|{"field_path":"device[1].ip_address","local_value":"present","upstream_value":"missing","description":"IP address field missing in upstream response"}
|
|
```
|
|
|
|
## State Machine Definitions
|
|
|
|
### Device State Transitions
|
|
|
|
```
|
|
Unregistered → Discovered (via discovery)
|
|
Discovered → Registering (via user action/auto-registration)
|
|
Registering → Active (via successful registration)
|
|
Registering → Error (via registration failure)
|
|
Active → Migrating (via migration start)
|
|
Active → Offline (via connectivity loss)
|
|
Migrating → Active (via migration success)
|
|
Migrating → Error (via migration failure)
|
|
Offline → Active (via connectivity restored)
|
|
Error → Active (via error resolution)
|
|
Any State → Retired (via explicit retirement)
|
|
```
|
|
|
|
### State Transition Rules
|
|
|
|
```go
|
|
var StateTransitionRules = map[DeviceState][]DeviceState{
|
|
DeviceStateUnregistered: {DeviceStateDiscovered},
|
|
DeviceStateDiscovered: {DeviceStateRegistering, DeviceStateOffline},
|
|
DeviceStateRegistering: {DeviceStateActive, DeviceStateError},
|
|
DeviceStateActive: {DeviceStateMigrating, DeviceStateOffline, DeviceStateRetired},
|
|
DeviceStateMigrating: {DeviceStateActive, DeviceStateError},
|
|
DeviceStateOffline: {DeviceStateActive, DeviceStateError, DeviceStateRetired},
|
|
DeviceStateError: {DeviceStateActive, DeviceStateOffline, DeviceStateRetired},
|
|
DeviceStateRetired: {}, // Terminal state
|
|
}
|
|
```
|
|
|
|
### Transition Triggers
|
|
|
|
```go
|
|
type TransitionTrigger struct {
|
|
Event DeviceEventType
|
|
Condition func(*DeviceLifecycle, *DeviceEvent) bool
|
|
Target DeviceState
|
|
Reason string
|
|
}
|
|
|
|
var TransitionTriggers = []TransitionTrigger{
|
|
{
|
|
Event: EventTypeDeviceOnline,
|
|
Condition: isOfflineDevice,
|
|
Target: DeviceStateActive,
|
|
Reason: "connectivity_restored",
|
|
},
|
|
{
|
|
Event: EventTypeDeviceOffline,
|
|
Condition: isActiveDevice,
|
|
Target: DeviceStateOffline,
|
|
Reason: "connectivity_lost",
|
|
},
|
|
{
|
|
Event: EventTypeMigrationStart,
|
|
Condition: isActiveDevice,
|
|
Target: DeviceStateMigrating,
|
|
Reason: "migration_initiated",
|
|
},
|
|
// ... additional triggers
|
|
}
|
|
```
|
|
|
|
## Event Processing
|
|
|
|
### Event Queue Implementation
|
|
|
|
```go
|
|
type EventQueue struct {
|
|
buffer chan DeviceEvent
|
|
processors []EventProcessor
|
|
storage EventStorage
|
|
config EventQueueConfig
|
|
}
|
|
|
|
type EventQueueConfig struct {
|
|
BufferSize int `json:"buffer_size"`
|
|
ProcessorCount int `json:"processor_count"`
|
|
FlushInterval time.Duration `json:"flush_interval"`
|
|
RetryAttempts int `json:"retry_attempts"`
|
|
DeadLetterQueue bool `json:"dead_letter_queue"`
|
|
}
|
|
|
|
type EventProcessor interface {
|
|
ProcessEvent(event DeviceEvent) error
|
|
CanHandle(eventType DeviceEventType) bool
|
|
}
|
|
```
|
|
|
|
### Event Processing Flow
|
|
|
|
```
|
|
Event Input → Validation → Queue → Processing → Storage → Notification
|
|
↓ ↓ ↓ ↓ ↓ ↓
|
|
Websocket Schema Buffer Parallel Files Webhooks
|
|
Discovery Check Memory Workers Logs SSE
|
|
API Call Format Retry DB Metrics
|
|
System Enrich DLQ
|
|
```
|
|
|
|
### Event Retention Policy
|
|
|
|
```go
|
|
type RetentionPolicy struct {
|
|
EventType DeviceEventType `json:"event_type"`
|
|
RetentionDays int `json:"retention_days"`
|
|
MaxCount int `json:"max_count"`
|
|
Compression bool `json:"compression"`
|
|
}
|
|
|
|
var DefaultRetentionPolicies = []RetentionPolicy{
|
|
{EventTypeNowPlaying, 7, 1000, true},
|
|
{EventTypeVolumeChanged, 1, 100, false},
|
|
{EventTypeDisparityFound, 30, 10000, true},
|
|
{EventTypeMigrationStart, 365, -1, false}, // Keep forever
|
|
{EventTypeHealthCheck, 7, 1000, true},
|
|
}
|
|
```
|
|
|
|
### Development Requirements
|
|
|
|
#### KISS Principle (Keep It Simple, Stupid)
|
|
- Prioritize simplicity and readability over performance optimization
|
|
- Use standard Go idioms and patterns
|
|
- Avoid premature abstraction and optimization
|
|
- Build the simplest thing that works first
|
|
|
|
#### Quality Gates
|
|
Every change must pass these checks:
|
|
- `golangci-lint run --fix` - no linting issues
|
|
- `go test ./...` - all tests pass with no failures
|
|
- Integration tests verify existing functionality intact
|
|
- Code coverage maintained or improved
|
|
|
|
#### Testing Requirements
|
|
- Unit tests for all new functions
|
|
- Integration tests for modified workflows
|
|
- Regression tests for existing functionality
|
|
- Mock external dependencies appropriately
|
|
|
|
### Simplicity-First Performance Approach
|
|
|
|
| Aspect | Simple Approach | Optimization Only When Needed |
|
|
|--------|----------------|-------------------------------|
|
|
| Memory Usage | Direct file operations, minimal caching | Add caching if performance issues arise |
|
|
| CPU Usage | Synchronous processing initially | Add async processing if bottlenecks occur |
|
|
| Storage | Simple append operations | Add rotation/compression when files grow large |
|
|
| Networking | Reuse existing patterns | Optimize only if latency becomes problematic |
|
|
|
|
## Quality Assurance
|
|
|
|
### Testing Strategy
|
|
|
|
#### Unit Testing
|
|
```go
|
|
// Example test structure
|
|
func TestAccountManager_CreateAccount(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input CreateAccountRequest
|
|
want *Account
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid account creation",
|
|
input: CreateAccountRequest{Name: "Test Account"},
|
|
want: &Account{Name: "Test Account", Status: "active"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty name should fail",
|
|
input: CreateAccountRequest{Name: ""},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := manager.CreateAccount(tt.input)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("CreateAccount() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
// Additional assertions...
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Integration Testing
|
|
- Test with real file operations in temporary directories
|
|
- Verify HTTP endpoints work with actual HTTP requests
|
|
- Test interaction with existing WebSocket system
|
|
- Ensure existing XML endpoints remain functional
|
|
|
|
#### Quality Gates
|
|
```bash
|
|
# Required before each commit
|
|
golangci-lint run --fix
|
|
go test ./...
|
|
go test -race ./...
|
|
|
|
# Required before milestone completion
|
|
go test ./... -v -cover
|
|
go test -bench=. ./...
|
|
```
|
|
|
|
## Performance Requirements
|
|
|
|
### Response Time Targets
|
|
- Local API requests: < 100ms (95th percentile)
|
|
- Discovery time: < 5s for network scan
|
|
|
|
### Resource Constraints
|
|
- Memory usage: < 64MB for small deployments
|
|
- CPU usage: < 5% on dual-core ARM systems (idle)
|
|
- Storage: < 100MB for interaction logs (rotatable)
|
|
|
|
### Security Considerations
|
|
|
|
#### Simple Security Model
|
|
- Reuse existing authentication mechanisms
|
|
- Basic input validation with standard Go validation
|
|
- Simple file permissions (0755 for directories, 0644 for files)
|
|
- No complex authorization initially - build incrementally
|
|
|
|
#### Input Validation
|
|
```go
|
|
// Simple validation approach
|
|
func ValidateAccount(account *Account) error {
|
|
if account.Name == "" {
|
|
return errors.New("account name cannot be empty")
|
|
}
|
|
if len(account.Name) > 100 {
|
|
return errors.New("account name too long")
|
|
}
|
|
if account.Email != "" && !isValidEmail(account.Email) {
|
|
return errors.New("invalid email format")
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Error Categories
|
|
|
|
```go
|
|
type ErrorCategory string
|
|
const (
|
|
ErrorCategoryValidation ErrorCategory = "validation"
|
|
ErrorCategorySystem ErrorCategory = "system"
|
|
ErrorCategoryNetwork ErrorCategory = "network"
|
|
ErrorCategoryStorage ErrorCategory = "storage"
|
|
ErrorCategoryTimeout ErrorCategory = "timeout"
|
|
ErrorCategoryAuth ErrorCategory = "authentication"
|
|
)
|
|
|
|
type ServiceError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Category ErrorCategory `json:"category"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Context map[string]interface{} `json:"context"`
|
|
Retryable bool `json:"retryable"`
|
|
Severity string `json:"severity"`
|
|
}
|
|
```
|
|
|
|
### Error Recovery Strategies
|
|
|
|
1. **Transient Errors**: Exponential backoff retry (3 attempts)
|
|
2. **Storage Errors**: Graceful degradation with in-memory fallback
|
|
3. **Network Errors**: Circuit breaker pattern with fallback data
|
|
4. **Validation Errors**: Immediate response with detailed feedback
|
|
5. **System Errors**: Alerting and automatic recovery attempts
|
|
|
|
### Error Response Format
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "DEVICE_NOT_FOUND",
|
|
"message": "Device with ID 'AABBCCDDEEFF' not found in account 'acc_12345'",
|
|
"category": "validation",
|
|
"timestamp": "2024-01-20T15:30:00Z",
|
|
"context": {
|
|
"account_id": "acc_12345",
|
|
"device_id": "AABBCCDDEEFF",
|
|
"request_id": "req_67890"
|
|
},
|
|
"retryable": false,
|
|
"severity": "error"
|
|
},
|
|
"request_id": "req_67890"
|
|
}
|
|
```
|
|
|
|
## Monitoring and Observability
|
|
|
|
### Simple Monitoring Approach
|
|
|
|
#### Basic Health Check
|
|
```go
|
|
// Simple health check implementation
|
|
type HealthStatus struct {
|
|
Status string `json:"status"` // "healthy", "warning", "error"
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Version string `json:"version"`
|
|
Uptime string `json:"uptime"`
|
|
}
|
|
|
|
func (s *Server) HandleHealthCheck(w http.ResponseWriter, r *http.Request) {
|
|
health := HealthStatus{
|
|
Status: "healthy",
|
|
Timestamp: time.Now(),
|
|
Version: version,
|
|
Uptime: time.Since(startTime).String(),
|
|
}
|
|
|
|
// Simple checks
|
|
if !s.canWriteToDataDir() {
|
|
health.Status = "error"
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(health)
|
|
}
|
|
```
|
|
|
|
#### Leverage Existing Systems
|
|
- Extend existing parity mismatch logging for disparity detection
|
|
- Reuse existing interaction recording for request/response tracking
|
|
- Build upon current discovery and migration event logging
|
|
- Use existing WebSocket event system for device state changes
|
|
|
|
#### Simple Metrics
|
|
```go
|
|
// Basic counters - no complex metrics initially
|
|
type SimpleMetrics struct {
|
|
AccountsCreated int `json:"accounts_created"`
|
|
DevicesActive int `json:"devices_active"`
|
|
EventsProcessed int `json:"events_processed_today"`
|
|
LastUpdate time.Time `json:"last_update"`
|
|
}
|
|
|
|
// Update metrics in simple text file
|
|
func (m *SimpleMetrics) Save(dataDir string) error {
|
|
data, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(dataDir, "metrics.json"), data, 0644)
|
|
}
|
|
```
|
|
|
|
This technical specification provides comprehensive details for implementing the enhanced state management system while maintaining compatibility with existing SoundTouch service functionality and meeting the performance requirements for small hardware deployments.
|