diff --git a/CODEBASE_REVIEW.md b/CODEBASE_REVIEW.md deleted file mode 100644 index 38e42b2..0000000 --- a/CODEBASE_REVIEW.md +++ /dev/null @@ -1,54 +0,0 @@ -# Codebase Review — 2026-02-26 - -Findings from full codebase review. Items ordered by priority within each category. - -## Stability (Critical) - -- [x] **Fatal loop exception leaks resources** — Added outer `try/except/finally` with `self._running = False` to all 10 processing loop methods across `live_stream.py`, `color_strip_stream.py`, `effect_stream.py`, `audio_stream.py`, `composite_stream.py`, `mapped_stream.py`. Also added per-iteration `try/except` where missing. -- [x] **`_is_running` flag cleanup** — Fixed via `finally: self._running = False` in all loop methods. *(Race condition via `threading.Event` deferred — current pattern sufficient with the finally block.)* -- [x] **`ColorStripStreamManager` thread safety** — **FALSE POSITIVE**: All access is from the async event loop; methods are synchronous with no `await` points, so no preemption is possible. -- [x] **Audio `stream.stop()` called under lock** — Moved `stream.stop()` outside lock scope in both `release()` and `release_all()` in `audio_capture.py`. -- [x] **WS accept-before-validate** — **FALSE POSITIVE**: All WS endpoints validate auth and resolve configs BEFORE calling `websocket.accept()`. -- [x] **Capture error no-backoff** — Added consecutive error counter with exponential backoff (`min(1.0, 0.1 * (errors - 5))`) in `ScreenCaptureLiveStream._capture_loop()`. -- [ ] **WGC session close not detected** — Deferred (Windows-specific edge case, low priority). -- [x] **`LiveStreamManager.acquire()` not thread-safe** — **FALSE POSITIVE**: Same as ColorStripStreamManager — all access from async event loop, no await in methods. - -## Performance (High Impact) - -- [x] **Per-pixel Python loop in `send_pixels()`** — Replaced per-pixel Python loop with `np.array().tobytes()` in `ddp_client.py`. Hot path already uses `send_pixels_numpy()`. -- [ ] **WGC 6MB frame allocation per callback** — Deferred (Windows-specific, requires WGC API changes). -- [x] **Gradient rendering O(LEDs×Stops) Python loop** — Vectorized with NumPy: `np.searchsorted` for stop lookup + vectorized interpolation in `_compute_gradient_colors()`. -- [x] **`PixelateFilter` nested Python loop** — Replaced with `cv2.resize` down (INTER_AREA) + up (INTER_NEAREST) — pure C++ backend. -- [x] **`DownscalerFilter` double allocation** — **FALSE POSITIVE**: Already uses single `cv2.resize()` call (vectorized C++). -- [x] **`SaturationFilter` ~25MB temp arrays** — **FALSE POSITIVE**: Already uses pre-allocated scratch buffer and vectorized in-place numpy. -- [x] **`FrameInterpolationFilter` copies full image** — **FALSE POSITIVE**: Already uses vectorized numpy integer blending with image pool. -- [x] **`datetime.utcnow()` per frame** — **LOW IMPACT**: ~1-2μs per call, negligible at 60fps. Deprecation tracked under Backend Quality. -- [x] **Unbounded diagnostic lists** — **FALSE POSITIVE**: Lists are cleared every 5 seconds (~300 entries max at 60fps). Trivial memory. - -## Frontend Quality - -- [x] **`lockBody()`/`unlockBody()` not re-entrant** — Added `_lockCount` reference counter and `_savedScrollY` in `ui.js`. First lock saves scroll, last unlock restores. -- [x] **XSS via unescaped engine config keys** — **FALSE POSITIVE**: Both capture template and audio template card renderers already use `escapeHtml()` on keys and values. -- [x] **LED preview WS `onclose` not nulled** — Added `ws.onclose = null` before `ws.close()` in `disconnectLedPreviewWS()` in `targets.js`. -- [x] **`fetchWithAuth` retry adds duplicate listeners** — Added `{ once: true }` to abort signal listener in `api.js`. -- [x] **Audio `requestAnimationFrame` loop continues after WS close** — **FALSE POSITIVE**: Loop already checks `testAudioModal.isOpen` before scheduling next frame, and `_cleanupTest()` cancels the animation frame. - -## Backend Quality - -- [ ] **No thread-safety in `JsonStore`** — Deferred (low risk — all stores are accessed from async event loop). -- [x] **Auth token prefix logged** — Removed token prefix from log message in `auth.py`. Now logs only "Invalid API key attempt". -- [ ] **Duplicate capture/test code** — Deferred (code duplication, not a bug — refactoring would reduce LOC but doesn't fix a defect). -- [x] **Update methods allow duplicate names** — Added name uniqueness checks to `update_template` in `template_store.py`, `postprocessing_template_store.py`, `audio_template_store.py`, `pattern_template_store.py`, and `update_profile` in `profile_store.py`. Also added missing check to `create_profile`. -- [ ] **Routes access `manager._private` attrs** — Deferred (stylistic, not a bug — would require adding public accessor methods). -- [x] **Non-atomic file writes** — Created `utils/file_ops.py` with `atomic_write_json()` helper (tempfile + `os.replace`). Updated all 10 store files. -- [ ] **444 f-string logger calls** — Deferred (performance impact negligible — Python evaluates f-strings very fast; lazy `%s` formatting only matters at very high call rates). -- [x] **`get_source()` silent bug** — Fixed: `color_strip_sources.py:_resolve_display_index()` called `picture_source_store.get_source()` which doesn't exist (should be `get_stream()`). Was silently returning `0` for display index. -- [ ] **`get_config()` race** — Deferred (low risk — config changes are infrequent user-initiated operations). -- [ ] **`datetime.utcnow()` deprecated** — Deferred (functional, deprecation warning only appears in Python 3.12+). -- [x] **Inconsistent DELETE status codes** — Changed `audio_sources.py` and `value_sources.py` DELETE endpoints from 200 to 204 (matching all other DELETE endpoints). - -## Architecture (Observations, no action needed) - -**Strengths**: Clean layered design, plugin registries, reference-counted stream sharing, consistent API patterns. - -**Weaknesses**: No backpressure (slow consumers buffer frames), thread count grows linearly, config global singleton, reference counting races. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..44cb450 --- /dev/null +++ b/TODO.md @@ -0,0 +1,38 @@ +# Pending Features & Issues + +Priority: `P1` quick win · `P2` moderate · `P3` large effort + +## Processing Pipeline + +- [ ] `P1` **Noise gate** — Suppress small color changes below threshold, preventing shimmer on static content +- [ ] `P1` **Color temperature filter** — Warm/cool shift separate from hue shift (circadian/mood) +- [ ] `P1` **Zone grouping** — Merge adjacent LEDs into logical groups sharing one averaged color +- [ ] `P2` **Palette quantization** — Force output to match a user-defined palette +- [ ] `P2` **Drag-and-drop filter ordering** — Reorder postprocessing filter chains visually +- [ ] `P3` **Transition effects** — Crossfade, wipe, or dissolve between sources/profiles instead of instant cut + +## Output Targets + +- [ ] `P1` **Rename `picture-targets` to `output-targets`** — Rename API endpoints and internal references for clarity +- [ ] `P2` **OpenRGB** — Control PC peripherals (keyboard, mouse, RAM, fans) as ambient targets +- [ ] `P2` **Art-Net / sACN (E1.31)** — Stage/theatrical lighting protocols, DMX controllers + +## Automation & Integration + +- [ ] `P2` **Webhook/MQTT trigger** — Let external systems activate profiles without HA integration +- [ ] `P2` **WebSocket event bus** — Broadcast all state changes over a single WS channel +- [ ] `P3` **Notification reactive** — Flash/pulse on OS notifications (optional app filter) + +## Multi-Display + +- [ ] `P2` **Investigate multimonitor support** — Research and plan support for multi-monitor setups +- [ ] `P3` **Multi-display unification** — Treat 2-3 monitors as single virtual display for seamless ambilight + +## Capture Engines + +- [ ] `P3` **SCRCPY capture engine** — Implement SCRCPY-based screen capture for Android devices +- [ ] `P3` **Camera / webcam** — Border-sampling from camera feed for video calls or room-reactive lighting + +## UX + +- [ ] `P3` **PWA / mobile layout** — Mobile-first layout + "Add to Home Screen" manifest diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index 4a28b27..085a086 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -682,10 +682,22 @@ flex-shrink: 0; } -.cs-collapsed .cs-filter-wrap { +.cs-collapsed .cs-filter-wrap, +.cs-collapsed .cs-header-extra { display: none; } +.cs-header-extra { + display: flex; + align-items: center; +} + +.cs-header-extra .btn { + padding: 2px 8px; + font-size: 0.75rem; + line-height: 1; +} + .cs-filter-wrap { position: relative; margin-left: auto; diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index b101b82..1f894a2 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -97,6 +97,7 @@ import { loadTargetsTab, switchTargetSubTab, showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor, startTargetProcessing, stopTargetProcessing, + stopAllLedTargets, stopAllKCTargets, startTargetOverlay, stopTargetOverlay, deleteTarget, cloneTarget, toggleLedPreview, toggleTargetAutoStart, expandAllTargetSections, collapseAllTargetSections, @@ -343,6 +344,8 @@ Object.assign(window, { saveTargetEditor, startTargetProcessing, stopTargetProcessing, + stopAllLedTargets, + stopAllKCTargets, startTargetOverlay, stopTargetOverlay, deleteTarget, diff --git a/server/src/wled_controller/static/js/core/card-sections.js b/server/src/wled_controller/static/js/core/card-sections.js index 02b5413..22c1a2d 100644 --- a/server/src/wled_controller/static/js/core/card-sections.js +++ b/server/src/wled_controller/static/js/core/card-sections.js @@ -43,13 +43,15 @@ export class CardSection { * @param {string} opts.gridClass CSS class for the card grid: 'devices-grid' | 'templates-grid' * @param {string} [opts.addCardOnclick] onclick handler string for the "+" add card * @param {string} [opts.keyAttr] data attribute that uniquely identifies cards (e.g. 'data-device-id') + * @param {string} [opts.headerExtra] Extra HTML injected between count badge and filter (e.g. action buttons) */ - constructor(sectionKey, { titleKey, gridClass, addCardOnclick, keyAttr }) { + constructor(sectionKey, { titleKey, gridClass, addCardOnclick, keyAttr, headerExtra }) { this.sectionKey = sectionKey; this.titleKey = titleKey; this.gridClass = gridClass; this.addCardOnclick = addCardOnclick || ''; this.keyAttr = keyAttr || ''; + this.headerExtra = headerExtra || ''; this._filterValue = ''; this._lastItems = null; this._dragState = null; @@ -86,6 +88,7 @@ export class CardSection { ▶ ${t(this.titleKey)} ${count} + ${this.headerExtra ? `${this.headerExtra}` : ''}