feat: roadmap batch (2026-06-19) — solar/linear-light/dither/nanoleaf + integrations

Eight roadmap features from the 2026-06-19 review, each a full vertical
(backend + tests + frontend + i18n en/ru/zh); ~67 new unit tests:

- automations: SolarRule sunrise/sunset trigger (new utils/solar.py, shared
  with the daylight cycle; window logic mirrors TimeOfDayRule)
- ci: best-effort arm64 multi-arch Docker manifest via QEMU + docker manifest
  (release.yml; amd64 path untouched, continue-on-error)
- game-integration: wire the orphaned LoLPoller via a LoLPollManager + a shared
  runtime_state module (poll lifecycle on enable/CRUD/startup/shutdown)
- ui: color-harmony gradient generator (complementary/analogous/triadic/...)
- effects: audio-reactive palette modulation (new audio_energy_tap; brightness/
  saturation modulation across all 12 procedural effects)
- capture: linear-light blending + spatio-temporal dithering, opt-in per
  calibration (new utils/linear_light.py, utils/dither.py)
- devices: Nanoleaf extControl v2 per-panel UDP streaming (per_panel mode)

Also bundles the pending 2026-06-18 production-review fixes and other
in-progress work already in the working tree (manual-trigger rule, etc.),
since they share files and could not be cleanly separated.

Gate: ruff + tsc clean; pytest 2654 passed / 2 skipped. The single failing
test (automation manual_trigger handler coverage) is a separate in-progress
item owned elsewhere, intentionally left as-is.
This commit is contained in:
2026-06-22 23:21:24 +03:00
parent 126d8f2449
commit 6745e25b20
91 changed files with 4390 additions and 540 deletions
@@ -13,6 +13,7 @@ Coverage targets
from __future__ import annotations
import asyncio
from unittest.mock import MagicMock, patch
import pytest
@@ -233,7 +234,7 @@ class TestAuthInstrumentation:
req = self._make_mock_request()
with pytest.raises(Exception): # HTTPException 401
verify_api_key(req, None)
asyncio.run(verify_api_key(req, None))
# At least one warning record about auth
warnings = [e for e in persisted if e.severity == ActivitySeverity.WARNING]
@@ -251,7 +252,7 @@ class TestAuthInstrumentation:
req = self._make_mock_request(client_ip="127.0.0.1")
with pytest.raises(Exception): # HTTPException 401
verify_api_key(req, creds)
asyncio.run(verify_api_key(req, creds))
# At least one warning-level auth record
warnings = [e for e in persisted if e.severity == ActivitySeverity.WARNING]
@@ -286,7 +287,7 @@ class TestAuthInstrumentation:
req = self._make_mock_request(client_ip="192.168.1.100")
with pytest.raises(Exception): # HTTPException 401
verify_api_key(req, None)
asyncio.run(verify_api_key(req, None))
warnings = [e for e in persisted if e.severity == ActivitySeverity.WARNING]
assert len(warnings) >= 1
@@ -302,7 +303,7 @@ class TestAuthInstrumentation:
req = self._make_mock_request(client_ip="10.0.0.5")
with pytest.raises(Exception):
verify_api_key(req, creds)
asyncio.run(verify_api_key(req, creds))
auth_records = [e for e in persisted if e.category == ActivityCategory.AUTH]
assert len(auth_records) >= 1