refactor: comprehensive code quality, security, and release readiness improvements
Lint & Test / test (push) Failing after 48s
Lint & Test / test (push) Failing after 48s
Security: tighten CORS defaults, add webhook rate limiting, fix XSS in automations, guard WebSocket JSON.parse, validate ADB address input, seal debug exception leak, URL-encode WS tokens, CSS.escape in selectors. Code quality: add Pydantic models for brightness/power endpoints, fix thread safety and name uniqueness in DeviceStore, immutable update pattern, split 6 oversized files into 16 focused modules, enable TypeScript strictNullChecks (741→102 errors), type state variables, add dom-utils helper, migrate 3 modules from inline onclick to event delegation, ProcessorDependencies dataclass. Performance: async store saves, health endpoint log level, command palette debounce, optimized entity-events comparison, fix service worker precache list. Testing: expand from 45 to 293 passing tests — add store tests (141), route tests (25), core logic tests (42), E2E flow tests (33), organize into tests/api/, tests/storage/, tests/core/, tests/e2e/. DevOps: CI test pipeline, pre-commit config, Dockerfile multi-stage build with non-root user and health check, docker-compose improvements, version bump to 0.2.0. Docs: rewrite CLAUDE.md (202→56 lines), server/CLAUDE.md (212→76), create contexts/server-operations.md, fix .js→.ts references, fix env var prefix in README, rewrite INSTALLATION.md, add CONTRIBUTING.md and .env.example.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Base class for JSON entity stores — eliminates boilerplate across 12+ stores."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
from pathlib import Path
|
||||
@@ -106,6 +107,23 @@ class BaseJsonStore(Generic[T]):
|
||||
logger.error(f"Failed to save {self._json_key} to {self.file_path}: {e}")
|
||||
raise
|
||||
|
||||
async def _save_async(self) -> None:
|
||||
"""Async wrapper around ``_save()`` — runs file I/O in a thread.
|
||||
|
||||
Use from ``async def`` route handlers to avoid blocking the event loop.
|
||||
Caller must hold ``self._lock`` (same contract as ``_save``).
|
||||
"""
|
||||
await asyncio.to_thread(self._save)
|
||||
|
||||
async def async_delete(self, item_id: str) -> None:
|
||||
"""Async version of ``delete()`` — offloads file I/O to a thread."""
|
||||
with self._lock:
|
||||
if item_id not in self._items:
|
||||
raise EntityNotFoundError(f"{self._entity_name} not found: {item_id}")
|
||||
del self._items[item_id]
|
||||
await self._save_async()
|
||||
logger.info(f"Deleted {self._entity_name}: {item_id}")
|
||||
|
||||
# ── Common CRUD ────────────────────────────────────────────────
|
||||
|
||||
def get_all(self) -> List[T]:
|
||||
|
||||
@@ -177,10 +177,17 @@ class Device:
|
||||
|
||||
|
||||
# Fields that can be updated (all Device.__init__ params except identity/timestamps)
|
||||
_UPDATABLE_FIELDS = {
|
||||
k for k in Device.__init__.__code__.co_varnames
|
||||
if k not in ('self', 'device_id', 'created_at', 'updated_at')
|
||||
}
|
||||
_UPDATABLE_FIELDS: frozenset[str] = frozenset({
|
||||
"name", "url", "led_count", "enabled", "device_type",
|
||||
"baud_rate", "software_brightness", "auto_shutdown",
|
||||
"send_latency_ms", "rgbw", "zone_mode", "tags",
|
||||
"dmx_protocol", "dmx_start_universe", "dmx_start_channel",
|
||||
"espnow_peer_mac", "espnow_channel",
|
||||
"hue_username", "hue_client_key", "hue_entertainment_group_id",
|
||||
"spi_speed_hz", "spi_led_type",
|
||||
"chroma_device_type", "gamesense_device_type",
|
||||
"default_css_processing_template_id",
|
||||
})
|
||||
|
||||
|
||||
class DeviceStore(BaseJsonStore[Device]):
|
||||
@@ -235,43 +242,46 @@ class DeviceStore(BaseJsonStore[Device]):
|
||||
gamesense_device_type: str = "keyboard",
|
||||
) -> Device:
|
||||
"""Create a new device."""
|
||||
device_id = f"device_{uuid.uuid4().hex[:8]}"
|
||||
with self._lock:
|
||||
self._check_name_unique(name)
|
||||
|
||||
# Mock devices use their device ID as the URL authority
|
||||
if device_type == "mock":
|
||||
url = f"mock://{device_id}"
|
||||
device_id = f"device_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
device = Device(
|
||||
device_id=device_id,
|
||||
name=name,
|
||||
url=url,
|
||||
led_count=led_count,
|
||||
device_type=device_type,
|
||||
baud_rate=baud_rate,
|
||||
auto_shutdown=auto_shutdown,
|
||||
send_latency_ms=send_latency_ms,
|
||||
rgbw=rgbw,
|
||||
zone_mode=zone_mode,
|
||||
tags=tags or [],
|
||||
dmx_protocol=dmx_protocol,
|
||||
dmx_start_universe=dmx_start_universe,
|
||||
dmx_start_channel=dmx_start_channel,
|
||||
espnow_peer_mac=espnow_peer_mac,
|
||||
espnow_channel=espnow_channel,
|
||||
hue_username=hue_username,
|
||||
hue_client_key=hue_client_key,
|
||||
hue_entertainment_group_id=hue_entertainment_group_id,
|
||||
spi_speed_hz=spi_speed_hz,
|
||||
spi_led_type=spi_led_type,
|
||||
chroma_device_type=chroma_device_type,
|
||||
gamesense_device_type=gamesense_device_type,
|
||||
)
|
||||
# Mock devices use their device ID as the URL authority
|
||||
if device_type == "mock":
|
||||
url = f"mock://{device_id}"
|
||||
|
||||
self._items[device_id] = device
|
||||
self._save()
|
||||
device = Device(
|
||||
device_id=device_id,
|
||||
name=name,
|
||||
url=url,
|
||||
led_count=led_count,
|
||||
device_type=device_type,
|
||||
baud_rate=baud_rate,
|
||||
auto_shutdown=auto_shutdown,
|
||||
send_latency_ms=send_latency_ms,
|
||||
rgbw=rgbw,
|
||||
zone_mode=zone_mode,
|
||||
tags=tags or [],
|
||||
dmx_protocol=dmx_protocol,
|
||||
dmx_start_universe=dmx_start_universe,
|
||||
dmx_start_channel=dmx_start_channel,
|
||||
espnow_peer_mac=espnow_peer_mac,
|
||||
espnow_channel=espnow_channel,
|
||||
hue_username=hue_username,
|
||||
hue_client_key=hue_client_key,
|
||||
hue_entertainment_group_id=hue_entertainment_group_id,
|
||||
spi_speed_hz=spi_speed_hz,
|
||||
spi_led_type=spi_led_type,
|
||||
chroma_device_type=chroma_device_type,
|
||||
gamesense_device_type=gamesense_device_type,
|
||||
)
|
||||
|
||||
logger.info(f"Created device {device_id}: {name}")
|
||||
return device
|
||||
self._items[device_id] = device
|
||||
self._save()
|
||||
|
||||
logger.info(f"Created device {device_id}: {name}")
|
||||
return device
|
||||
|
||||
def update_device(self, device_id: str, **kwargs) -> Device:
|
||||
"""Update device fields.
|
||||
@@ -279,17 +289,37 @@ class DeviceStore(BaseJsonStore[Device]):
|
||||
Pass any updatable Device field as a keyword argument.
|
||||
``None`` values are ignored (no change).
|
||||
"""
|
||||
device = self.get(device_id) # raises ValueError if not found
|
||||
with self._lock:
|
||||
device = self.get(device_id) # raises ValueError if not found
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and key in _UPDATABLE_FIELDS:
|
||||
setattr(device, key, value)
|
||||
# Collect updates (ignore None values and unknown fields)
|
||||
updates = {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if value is not None and key in _UPDATABLE_FIELDS
|
||||
}
|
||||
|
||||
device.updated_at = datetime.now(timezone.utc)
|
||||
self._save()
|
||||
# Check name uniqueness if name is being changed
|
||||
new_name = updates.get("name")
|
||||
if new_name is not None and new_name != device.name:
|
||||
self._check_name_unique(new_name, exclude_id=device_id)
|
||||
|
||||
logger.info(f"Updated device {device_id}")
|
||||
return device
|
||||
# Build new Device from existing fields + updates (immutable pattern)
|
||||
device_fields = device.to_dict()
|
||||
# Map 'id' back to 'device_id' for the constructor
|
||||
device_fields["device_id"] = device_fields.pop("id")
|
||||
# Restore datetime objects (to_dict serializes them as ISO strings)
|
||||
device_fields["created_at"] = device.created_at
|
||||
device_fields["updated_at"] = datetime.now(timezone.utc)
|
||||
# Apply updates
|
||||
device_fields.update(updates)
|
||||
|
||||
new_device = Device(**device_fields)
|
||||
self._items[device_id] = new_device
|
||||
self._save()
|
||||
|
||||
logger.info(f"Updated device {device_id}")
|
||||
return new_device
|
||||
|
||||
# ── Unique helpers ───────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user