Codebase review: stability, performance, usability, and i18n fixes
Stability: - Fix race condition: set _is_running before create_task in target processors - Await probe task after cancel in wled_target_processor - Replace raw fetch() with fetchWithAuth() across devices, kc-targets, pattern-templates - Add try/catch to showTestTemplateModal in streams.js - Wrap blocking I/O in asyncio.to_thread (picture_targets, system restore) - Fix dashboardStopAll to filter only running targets with ok guard Performance: - Vectorize fire effect spark loop with numpy in effect_stream - Vectorize FFT band binning with cumulative sum in analysis.py - Rewrite pixel_processor with vectorized numpy (accept ndarray or list) - Add httpx.AsyncClient connection pooling with lock in wled_provider - Optimize _send_pixels_http to avoid np.hstack allocation in wled_client - Mutate chart arrays in-place in dashboard, perf-charts, targets - Merge dashboard 2-batch fetch into single Promise.all - Hoist frame_time outside loop in mapped_stream Usability: - Fix health check interval load/save in device settings - Swap confirm modal button classes (No=secondary, Yes=danger) - Add aria-modal to audio/value source editors, fix close button aria-labels - Add modal footer close button to settings modal - Add dedicated calibration LED count validation error keys i18n: - Replace ~50 hardcoded English strings with t() calls across 12 JS files - Add 50 new keys to en.json, ru.json, zh.json - Localize inline toasts in index.html with window.t fallback - Add data-i18n to command palette footer - Add localization policy to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -356,12 +356,17 @@ class EffectColorStripStream(ColorStripStream):
|
||||
new_heat[-1] = heat[-1] * 0.5
|
||||
heat[:] = new_heat
|
||||
|
||||
# Sparks at the bottom
|
||||
# Sparks at the bottom (vectorized)
|
||||
spark_zone = max(1, n // 8)
|
||||
spark_prob = 0.3 * intensity
|
||||
for i in range(spark_zone):
|
||||
if np.random.random() < spark_prob:
|
||||
heat[i] = min(1.0, heat[i] + 0.4 + 0.6 * np.random.random())
|
||||
rng = np.random.random(spark_zone)
|
||||
mask = rng < spark_prob
|
||||
if mask.any():
|
||||
heat[:spark_zone] = np.where(
|
||||
mask,
|
||||
np.minimum(1.0, heat[:spark_zone] + 0.4 + 0.6 * np.random.random(spark_zone)),
|
||||
heat[:spark_zone],
|
||||
)
|
||||
|
||||
# Map heat to palette (pre-allocated scratch)
|
||||
np.multiply(heat, 255, out=self._s_f32_a)
|
||||
|
||||
@@ -174,8 +174,8 @@ class KCTargetProcessor(TargetProcessor):
|
||||
self._latest_colors = None
|
||||
|
||||
# Start processing task
|
||||
self._task = asyncio.create_task(self._processing_loop())
|
||||
self._is_running = True
|
||||
self._task = asyncio.create_task(self._processing_loop())
|
||||
|
||||
logger.info(f"Started KC processing for target {self._target_id}")
|
||||
self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": True})
|
||||
|
||||
@@ -152,10 +152,10 @@ class MappedColorStripStream(ColorStripStream):
|
||||
# ── Processing loop ─────────────────────────────────────────
|
||||
|
||||
def _processing_loop(self) -> None:
|
||||
frame_time = 1.0 / self._fps
|
||||
try:
|
||||
while self._running:
|
||||
loop_start = time.perf_counter()
|
||||
frame_time = 1.0 / self._fps
|
||||
|
||||
try:
|
||||
target_n = self._led_count
|
||||
|
||||
@@ -160,8 +160,8 @@ class WledTargetProcessor(TargetProcessor):
|
||||
|
||||
# Reset metrics and start loop
|
||||
self._metrics = ProcessingMetrics(start_time=datetime.utcnow())
|
||||
self._task = asyncio.create_task(self._processing_loop())
|
||||
self._is_running = True
|
||||
self._task = asyncio.create_task(self._processing_loop())
|
||||
|
||||
logger.info(f"Started processing for target {self._target_id}")
|
||||
self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": True})
|
||||
@@ -889,6 +889,10 @@ class WledTargetProcessor(TargetProcessor):
|
||||
await _probe_client.aclose()
|
||||
if _probe_task is not None and not _probe_task.done():
|
||||
_probe_task.cancel()
|
||||
try:
|
||||
await _probe_task
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
self._device_reachable = None
|
||||
self._metrics.device_streaming_reachable = None
|
||||
logger.info(f"Processing loop ended for target {self._target_id}")
|
||||
|
||||
Reference in New Issue
Block a user