refactor: comprehensive code quality, security, and release readiness improvements
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:
2026-03-22 00:38:28 +03:00
parent 07bb89e9b7
commit f2871319cb
115 changed files with 9808 additions and 5818 deletions
@@ -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 ───────────────────────────────────────────