# Phase 2: Storage & API — Game Integration Configs **Status:** ✅ Complete **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Create the persistence layer and REST API for managing game integration configurations. Users create/edit/delete game integration configs, and games POST events to an ingestion endpoint. ## Tasks - [x] Task 1: Create `GameIntegrationConfig` dataclass (`storage/game_integration.py`) - Fields: id (gi_<8hex>), name, adapter_type, enabled, adapter_config (dict), event_mappings (list of EventMapping dicts), created_at, updated_at, description, tags - `EventMapping` dataclass: event_type, effect, color ([R,G,B]), duration_ms, intensity, priority - Standard to_dict/from_dict/create_from_kwargs/apply_update methods - [x] Task 2: Create `GameIntegrationStore` (`storage/game_integration_store.py`) - Extend BaseSqliteStore[GameIntegrationConfig] - CRUD methods: create_integration, update_integration, delete (inherited) - Name uniqueness validation - get_references() for cascade prevention - [x] Task 3: Create Pydantic schemas (`api/schemas/game_integration.py`) - GameIntegrationCreate, GameIntegrationUpdate, GameIntegrationResponse, GameIntegrationListResponse - EventMappingSchema - GameEventPayload (for ingestion endpoint) - AdapterInfoResponse, AdapterListResponse (for adapter metadata endpoint) - GameIntegrationStatusResponse (for diagnostics) - [x] Task 4: Create API routes (`api/routes/game_integration.py`) - GET /api/v1/game-integrations — list all configs - POST /api/v1/game-integrations — create config - GET /api/v1/game-integrations/{id} — get config - PUT /api/v1/game-integrations/{id} — update config - DELETE /api/v1/game-integrations/{id} — delete config - POST /api/v1/game-integrations/{id}/event — receive game event payload (ingestion) - GET /api/v1/game-integrations/{id}/status — connection status, last event, event count - GET /api/v1/game-integrations/{id}/events — recent events for debugging - GET /api/v1/game-adapters — list available adapter types + supported events + config schema - [x] Task 5: Wire into main.py — create store, register in dependencies, include router - [x] Task 6: Add to STORE_MAP in api/routes/system.py for backup/restore - N/A — backups are SQLite-level (full database snapshot). Added `game_integrations` to `_ENTITY_TABLES` in `database.py` which automatically includes it in backup/restore. - [x] Task 7: Add game_integrations_file to StorageConfig in config.py (if JSON-based) or ensure DB table - Added `"game_integrations"` to `_ENTITY_TABLES` in `database.py` — table auto-created on startup. - [x] Task 8: Event ingestion logic — parse payload through adapter, publish to EventBus, track per-integration state (for diff detection) - [x] Task 9: Write tests for store (CRUD, validation, uniqueness) - [x] Task 10: Write tests for API routes (create, list, update, delete, event ingestion) ## Files to Modify/Create - `server/src/wled_controller/storage/game_integration.py` — dataclass models - `server/src/wled_controller/storage/game_integration_store.py` — SQLite store - `server/src/wled_controller/api/schemas/game_integration.py` — Pydantic schemas - `server/src/wled_controller/api/routes/game_integration.py` — REST endpoints - `server/src/wled_controller/main.py` — wire store + router - `server/src/wled_controller/api/dependencies.py` — add get_game_integration_store - `server/src/wled_controller/api/routes/system.py` — add to STORE_MAP - `server/src/wled_controller/config.py` — add storage config (if needed) - `server/tests/storage/test_game_integration_store.py` — store tests - `server/tests/api/test_game_integration_routes.py` — route tests ## Acceptance Criteria - Full CRUD for game integration configs via REST API - Event ingestion endpoint parses payload through the correct adapter and publishes to EventBus - Adapter metadata endpoint returns available adapters with supported events and config schemas - Status endpoint shows last event time and event counts - Store validates name uniqueness - Included in backup/restore via STORE_MAP - All tests pass ## Notes - The ingestion endpoint must be low-latency — games send at tick rate (16-64 Hz) - Store per-integration prev_state dict in memory (not persisted) for diff-based trigger detection - Auth for ingestion endpoint: adapter-level auth (e.g. CS2 token) checked before standard API auth ## Review Checklist - [x] All tasks completed - [x] Code follows project conventions - [x] No unintended side effects - [x] Tests pass ## Handoff to Next Phase **Completed:** All 10 tasks implemented and tested. 48 tests pass, 0 failures. Ruff clean. ### What was built - `storage/game_integration.py` — `EventMapping` and `GameIntegrationConfig` dataclasses with full `to_dict()`/`from_dict()`/`create_from_kwargs()`/`apply_update()` methods. `apply_update()` returns a new instance (immutable pattern). - `storage/game_integration_store.py` — `GameIntegrationStore` extending `BaseSqliteStore[GameIntegrationConfig]`. CRUD with name uniqueness, write-through caching. Aliases: `get_all_integrations`, `get_integration`, `delete_integration`. - `api/schemas/game_integration.py` — Full Pydantic schema suite: `EventMappingSchema`, `GameIntegrationCreate/Update/Response/ListResponse`, `GameEventPayload`, `AdapterInfoResponse/AdapterListResponse`, `GameIntegrationStatusResponse`, `GameEventResponse/RecentEventsResponse`. - `api/routes/game_integration.py` — 9 endpoints: full CRUD + event ingestion + status + recent events + adapter listing. Ingestion endpoint skips `AuthRequired` (uses adapter-level auth via `validate_auth()`). Per-integration runtime state tracked in-memory (`_prev_states`, `_integration_stats`) with thread-safe access. - `database.py` — Added `"game_integrations"` to `_ENTITY_TABLES` so the table is auto-created and included in database backups. - `api/dependencies.py` — Added `get_game_integration_store()` and `get_game_event_bus()` getters + `init_dependencies()` params. - `api/__init__.py` — Registered `game_integration_router`. - `main.py` — Creates `GameIntegrationStore(db)` and `GameEventBus()`, passes both to `init_dependencies()`. ### Key design decisions - **Ingestion auth**: The `/event` endpoint does NOT use `AuthRequired`. Instead, it calls `adapter_cls.validate_auth()` with request headers and adapter config. This allows game clients (CS2 GSI, etc.) to authenticate with their own token scheme without needing the app's API key. - **Runtime state**: Per-integration `prev_state` (for diff detection) and event stats are stored in module-level dicts with a threading lock. Not persisted — resets on server restart. - **Connected heuristic**: An integration is "connected" if it received an event within the last 30 seconds (based on monotonic time). - **Immutable updates**: `GameIntegrationConfig.apply_update()` returns a new instance; the store replaces the cache entry. ### What Phase 3+ needs - `GameEventBus` singleton is now available via `get_game_event_bus()` dependency — Phase 4/5 streams can subscribe to it. - `GameIntegrationStore` is available via `get_game_integration_store()` — Phase 6 frontend can call the CRUD API. - Built-in adapters (Phase 3) should call `AdapterRegistry.register(MyAdapter)` at import time to appear in `GET /api/v1/game-adapters`. - The `adapter_config` field on each integration stores adapter-specific secrets (e.g. CS2 auth token). Phase 3 adapters define their config schema via `get_config_schema()`.