Card bulk operations, remove expand/collapse, graph color picker fix

- Bulk selection mode: Ctrl+Click or toggle button to enter, Escape to exit
- Shift+Click for range select, bottom toolbar with SVG icon action buttons
- All CardSections wired with bulk actions: Delete everywhere,
  Start/Stop for targets, Enable/Disable for automations
- Remove expand/collapse all buttons (no collapsible sections remain)
- Fix graph node color picker overlay persisting after outside click
- Add Icons section to frontend.md conventions
- Add trash2, listChecks, circleOff icons to icon system
- Backend: processing loop performance improvements (monotonic timestamps,
  deque-based FPS tracking)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 01:21:27 +03:00
parent f4647027d2
commit 122e95545c
18 changed files with 771 additions and 149 deletions
@@ -595,7 +595,13 @@ class WledTargetProcessor(TargetProcessor):
fps_samples: collections.deque = collections.deque(maxlen=10)
_fps_sum = 0.0
send_timestamps: collections.deque = collections.deque(maxlen=target_fps + 10)
send_timestamps: collections.deque = collections.deque(maxlen=self._target_fps + 10)
def _fps_current_from_timestamps():
"""Count timestamps within the last second."""
cutoff = time.perf_counter() - 1.0
return sum(1 for ts in send_timestamps if ts > cutoff)
last_send_time = 0.0
_last_preview_broadcast = 0.0
prev_frame_time_stamp = time.perf_counter()
@@ -839,7 +845,7 @@ class WledTargetProcessor(TargetProcessor):
send_timestamps.append(now)
self._metrics.frames_keepalive += 1
self._metrics.frames_skipped += 1
self._metrics.fps_current = len(send_timestamps)
self._metrics.fps_current = _fps_current_from_timestamps()
await asyncio.sleep(SKIP_REPOLL)
continue
@@ -864,7 +870,7 @@ class WledTargetProcessor(TargetProcessor):
await self._broadcast_led_preview(send_colors, cur_brightness)
_last_preview_broadcast = now
self._metrics.frames_skipped += 1
self._metrics.fps_current = len(send_timestamps)
self._metrics.fps_current = _fps_current_from_timestamps()
is_animated = stream.is_animated
repoll = SKIP_REPOLL if is_animated else frame_time
await asyncio.sleep(repoll)
@@ -923,7 +929,7 @@ class WledTargetProcessor(TargetProcessor):
processing_time = now - loop_start
self._metrics.fps_potential = 1.0 / processing_time if processing_time > 0 else 0
self._metrics.fps_current = len(send_timestamps)
self._metrics.fps_current = _fps_current_from_timestamps()
except Exception as e:
self._metrics.errors_count += 1