Commit Graph

714 Commits

Author SHA1 Message Date
alexei.dolgolyov 580bd692e6 fix(scenes): coerce BindableFloat fps to int when snapshotting
Build Android APK / build-android (push) Failing after 1m41s
Lint & Test / test (push) Successful in 4m32s
OutputTarget.fps is a BindableFloat but TargetSnapshot.fps is a plain
int — capture_current_snapshot was stuffing the BindableFloat directly
into the snapshot, causing json.dumps to fail on recapture (500) and
polluting the in-memory cache so subsequent list calls also 500'd with
pydantic validation errors.
2026-04-14 19:03:58 +03:00
alexei.dolgolyov 7fcb8dd346 feat(devices): Android USB-serial support for Adalight/AmbiLED controllers
Build Android APK / build-android (push) Failing after 1m41s
Lint & Test / test (push) Successful in 4m51s
Adds end-to-end support for driving USB-connected Adalight / AmbiLED
LED controllers from Android TV boxes. Android's security model blocks
direct USB access from Python, so writes route through a Kotlin
UsbSerialBridge singleton via Chaquopy.

Python side:
- New SerialTransport Protocol (serial_transport.py) with open / write /
  flush / close. Desktop uses PySerialTransport (wraps pyserial),
  Android uses AndroidSerialTransport (wraps the Kotlin bridge).
- list_serial_ports() factory returns desktop COM ports on desktop,
  USB devices on Android — callers don't branch.
- URL scheme extended: existing COM3[:baud] and /dev/ttyUSB0[:baud]
  unchanged; new usb:VID:PID[:serial][@baud] for Android (@ is the
  baud separator since : is already used between VID and PID).
- AdalightClient and SerialDeviceProvider refactored to go through
  the transport — no more direct pyserial imports in hot paths.
- 17 new unit tests cover URL parsing, PySerial transport, factory
  selection, platform-branching discovery. Full suite 750 passing.

Kotlin side:
- UsbSerialBridge.kt singleton uses com.hoho.android.usbserial (mik3y)
  which ships drivers for CH340, CP2102, FTDI, Prolific, and CDC-ACM
  (Arduino). Exposes listDevices, open, write, close via @JvmStatic
  for Chaquopy. First open() attempt without permission triggers the
  system USB permission dialog; next call succeeds once user grants.
- usb-serial-for-android is distributed via JitPack — added that repo
  in settings.gradle.kts and the dependency in app/build.gradle.kts.
- AndroidManifest declares uses-feature android.hardware.usb.host
  (required=false so non-USB-host phones still install).
- LedGrabApp.onCreate calls UsbSerialBridge.init(this) so the bridge
  resolves the UsbManager without needing an Activity ref.

Verified: ./gradlew compileDebugKotlin succeeds; off-Android import
of android_serial_transport works. Real-hardware smoke test on a
TV box with a CH340/CP2102/FTDI adapter still pending.

ESP-NOW (espnow_client / espnow_provider) still imports pyserial
directly because it needs bidirectional reads — separate refactor
to extend the transport with read() if that path ever needs Android
USB support.
2026-04-14 16:34:09 +03:00
alexei.dolgolyov ecae05d00b feat(metrics): battery + thermal-zone readings with dashboard temp chart
Build Android APK / build-android (push) Failing after 1m40s
Lint & Test / test (push) Successful in 4m18s
Extends MetricsProvider with thermals() returning a ThermalSnapshot
(battery_percent, battery_temp_c, cpu_temp_c — all optional). Each
provider implements it independently:

- AndroidMetricsProvider reads /sys/class/power_supply/battery/{capacity,
  temp} (battery temp is tenths of degC) and walks
  /sys/class/thermal/thermal_zone*, filtering by zone type
  (cpu/soc/tsens/core) so battery and skin sensors don't dominate the
  reading. Rejects nonsense values like INT_MAX from buggy zones.
- PsutilMetricsProvider uses sensors_battery() and
  sensors_temperatures() when present (Linux+laptops); no-ops on
  Windows/macOS where psutil doesn't expose them.
- NullMetricsProvider returns the empty snapshot.

PerformanceResponse gains battery_percent / battery_temp_c / cpu_temp_c.
The metrics-history ring buffer also carries cpu_temp / battery_pct /
battery_temp per sample so the dashboard can graph them over time.

Frontend dashboard (perf-charts.ts) gets a new Temperature chart card,
hidden by default and revealed only after seed/poll confirms the
backend reports cpu_temp_c. Battery temperature shows inline as a
secondary badge. The GPU card now also hides entirely when the backend
reports gpu=null instead of showing an "unavailable" placeholder.
HOST_ONLY_KEYS prevents the System/App/Both toggle from flipping a
non-existent app dataset for temp.

Tests: 6 new for thermals (battery tenths-of-degC parsing, CPU zone
filtering, fallback when sensors absent, INT_MAX rejection); 18 metrics
tests total; full suite 733 passing.
2026-04-14 13:48:01 +03:00
alexei.dolgolyov 546b24d015 refactor(metrics): MetricsProvider abstraction with Android /proc backend
Build Android APK / build-android (push) Failing after 1m39s
Lint & Test / test (push) Successful in 4m20s
Moves direct psutil.* calls behind a MetricsProvider Protocol so the
codebase no longer needs ad-hoc `if psutil is not None` guards at every
call site. Each provider lives in its own module under
utils/metrics/: PsutilMetricsProvider for desktop, NullMetricsProvider
as a zeroed fallback, AndroidMetricsProvider that reads /proc/stat,
/proc/meminfo, /proc/self/stat, and /proc/self/status directly (psutil
isn't available under Chaquopy). The Android provider tracks the
previous CPU sample so cpu_percent() returns delta-based percentages
matching psutil's interval=None semantics, and degrades to zeros when
any /proc file is unreadable instead of crashing the dashboard.

Factory get_metrics_provider() in utils/metrics/__init__.py picks
Android > psutil > Null. api/routes/system.py and
core/processing/metrics_history.py now go through the factory; psutil
import is confined to one place. 12 new unit tests cover paren-in-comm
parsing of /proc/self/stat, delta CPU%, missing-file resilience, and
factory selection order. Full suite: 727 passing.
2026-04-14 13:34:32 +03:00
alexei.dolgolyov 488df98996 fix(frontend): add autocomplete attrs to credential inputs
Build Android APK / build-android (push) Failing after 1m40s
Lint & Test / test (push) Successful in 3m35s
Silences browser accessibility warnings on the HA token, MQTT username,
and MQTT password fields. Uses new-password for the secret inputs to
discourage Chrome's site-password autofill from leaking into broker /
HA-token configuration.
2026-04-14 12:50:50 +03:00
alexei.dolgolyov 2477e00fae ci: add Android APK row to release downloads table
Lint & Test / test (push) Successful in 1m37s
2026-04-14 12:47:27 +03:00
alexei.dolgolyov 151cea3ecb ci: Android multi-ABI APK pipeline + pydantic-core wheel rebuild
Build Android APK / build-android (push) Failing after 2m31s
Lint & Test / test (push) Successful in 6m15s
Adds .gitea/workflows/build-android.yml — Linux runner installs JDK 17,
Python 3.11, Android SDK/NDK, symlinks server/src/ledgrab into the
Chaquopy python source dir, and runs assembleDebug on master pushes /
assembleRelease on v* tags. APK is uploaded as an artifact and attached
to the Gitea release on tag push. Conditional signing config in
build.gradle.kts reads keystore from env vars (CI secrets) and falls
back to debug signing locally. Gradle wrapper (gradlew/gradlew.bat/
gradle-wrapper.jar) committed so CI can drive the build.

Rebuilds pydantic-core wheels for arm64-v8a and x86_64 — both were
missing libpython3.11.so in NEEDED, which would have crashed at import
on real devices. build-pydantic-core.sh rewritten as a multi-ABI builder:
selects targets via args, sets RUSTFLAGS=-C link-arg=-Wl,--no-as-needed
-C link-arg=-lpython3.11 to force the symbol-resolution dependency,
uses the per-ABI sysconfigdata + libpython staged in
android/.build-cache/, prefers `py -3.11` on Windows (Git Bash's
python3.11 is an MSStore stub), uses the .cmd clang wrapper on Windows
(fixes os error 193), and verifies NEEDED via llvm-readelf after each
build. abiFilters restored to the full triple in build.gradle.kts;
multi-ABI debug APK builds cleanly (~99 MB).
2026-04-14 12:36:13 +03:00
alexei.dolgolyov 8574424fb7 feat: Android TV app embedding Python server via Chaquopy
Lint & Test / test (push) Successful in 2m10s
Adds a native Android TV application that runs the full LedGrab Python
server in-process via Chaquopy. Captures the TV box screen using the
MediaProjection API and exposes the existing web UI on the device's
local network — users configure via phone/tablet browser.

Android (new /android/ module):
- Kotlin shell: MainActivity, CaptureService (foreground service),
  ScreenCapture (MediaProjection + ImageReader), PythonBridge (Chaquopy).
- Polished Leanback-themed UI with QR code for easy web UI access.
- AGP 8.9 + Chaquopy 17 + Gradle 8.11 (avoids the AGP 8.7 thread-lock bug).
- Pre-built pydantic-core wheels for arm64-v8a, x86_64, x86 cross-compiled
  with maturin + Android NDK, linked against Chaquopy's libpython3.11.so.

Python server platform guards:
- New utils/platform.py with is_android()/is_windows()/is_linux() helpers.
- Guard every top-level import of desktop-only packages (mss, psutil,
  sounddevice, pyserial, PyAudioWPatch, etc.) with try/except ImportError.
- Android-incompatible calls gated with None-checks so the server runs on
  reduced capabilities on Android (no CPU/RAM metrics, no mss displays).
- utils/image_codec.py gains a Pillow fallback for resize + JPEG encode
  when cv2 is unavailable; all internal cv2.resize callers migrated.
- New android_entry.py start_server/stop_server invoked from Kotlin.
- get_displays API falls back to best available engine when mss fails.

New capture engines:
- MediaProjectionEngine: receives RGBA frames pushed from Kotlin through
  a thread-safe queue; caches last frame for static-screen previews.
- ScrcpyClientEngine: optional H.264 streaming via scrcpy-client library
  (priority 10, overrides the ADB-screencap engine when installed).

Frontend:
- Tab loaders previously required an apiKey; now correctly treat
  "auth disabled" as authenticated (Android has no auth by default).
- Re-trigger the active tab's loader after loadServerInfo resolves
  authRequired, since initTabs runs earlier.
- Add i18n keys for the demo / mediaprojection / scrcpy_client engines.

Docs:
- TODO.md: follow-ups for multi-ABI wheel rebuilds, CI pipeline, USB
  serial LED controllers, root-only capture, perf metrics abstraction.
- CLAUDE.md: Android dependency sync policy (pip --exclude doesn't exist).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 03:11:43 +03:00
alexei.dolgolyov a0b65e3fcb refactor: move build scripts to build/ directory
Lint & Test / test (push) Successful in 2m21s
Declutters the repo root by consolidating build-common.sh,
build-dist.sh, build-dist-windows.sh, build-dist.ps1, and installer.nsi
into build/. Updates all path references in CI workflows, NSIS installer,
and documentation.
2026-04-12 23:16:40 +03:00
alexei.dolgolyov 02cd9d519c refactor: rename project to LedGrab, split HA integration into separate repo
Lint & Test / test (push) Successful in 1m56s
- Rename Python package: wled_controller -> ledgrab
- Rename env var prefix: WLED_ -> LEDGRAB_ (with auto-migration for old vars)
- Rename localStorage key: wled_api_key -> ledgrab_api_key (with migration)
- Rename HA integration domain: wled_screen_controller -> ledgrab
- Update all imports, build scripts, Docker, installer, config, docs
- Remove HA integration (moved to ledgrab-haos-integration repo)
- Remove hacs.json (belongs in HA repo now)
- Add startup warning for users with old WLED_ env vars
- All tests pass (715/715), ruff clean, tsc clean, frontend builds
2026-04-12 22:45:28 +03:00
alexei.dolgolyov 38f73badbf fix: register pattern-templates API route; add responsive toolbar overflow menu
Lint & Test / test (push) Successful in 2m19s
Pattern templates route existed but was never wired into the router,
dependencies, or database table allowlist — causing 404 on graph tab load.

Graph toolbar now collapses secondary actions into a "more" overflow menu
on viewports narrower than 700px. Primary controls (fit, zoom, add) stay
visible; search, filter, panels, undo/redo, relayout, fullscreen, and
help move into the dropdown.
2026-04-12 21:22:50 +03:00
alexei.dolgolyov e678e5590a docs: update TODO and frontend context docs
Lint & Test / test (push) Successful in 1m54s
Replace TODO-css-improvements.md with TODO-release.md. Add EntityPalette
and IconSelect patterns for dynamic entity lists to frontend context.
2026-04-12 20:55:58 +03:00
alexei.dolgolyov 83ceaeda9d fix: HA Light Target cards flickering on every poll cycle
Lint & Test / test (push) Has been cancelled
Use stable placeholder values in card HTML for volatile metrics (fps,
uptime, HA status, entity swatches) so CardSection.reconcile() skips
unchanged cards. Actual values are patched in-place via
patchHALightTargetMetrics() — same pattern LED target cards already use.
2026-04-12 20:55:30 +03:00
alexei.dolgolyov d3cd48e7a7 fix: EntitySelect not showing selected value in weather/processed CSS editors
Lint & Test / test (push) Successful in 1m46s
After _populateWeatherSourceDropdown() and _populateProcessedSelectors()
create new EntitySelect instances, the subsequent .value = assignment on
the native <select> doesn't trigger _syncTrigger(), so the trigger
button still shows "—". Call .refresh() after setting the value.
2026-04-12 20:47:33 +03:00
alexei.dolgolyov cc9900d801 feat: support nesting for composite color strip sources
Lint & Test / test (push) Successful in 2m17s
Allow composite sources to reference other composite/mapped sources as
layers. Adds cycle detection (via transitive dependency graph walk),
depth limiting (MAX_COMPOSITE_DEPTH=4), and a runtime safety net in the
stream manager. Frontend layer dropdown now shows all source types
except the source being edited.

17 new tests covering cycles, depth limits, and valid nesting — all
715 tests passing.
2026-04-12 20:41:15 +03:00
alexei.dolgolyov 4940007e54 feat: add Group device type for combining multiple devices
Lint & Test / test (push) Successful in 2m19s
Introduces a new "group" device type that aggregates multiple physical
(or nested group) devices into one virtual device. Supports two modes:
- Sequence: LEDs concatenated end-to-end (led_count = sum of children)
- Independent: full pixel array resampled to each child independently

Includes cycle detection (DFS) to prevent circular group references,
delete protection for devices referenced by groups, recursive LED count
resolution for nested groups, and reorder controls (move up/down) for
child devices in the UI.

Backend: Device model, API schemas, GroupLEDClient, GroupDeviceProvider,
route validation, processing pipeline integration.
Frontend: type picker, child device picker with reorder, mode selector,
i18n (en/ru/zh), layers icon, CSS for group child rows.
Tests: 20 unit tests for cycle detection, LED count resolution, and
GroupLEDClient (sequence slicing, independent resampling, cleanup).
2026-04-11 02:26:56 +03:00
alexei.dolgolyov 92585e7c19 fix(build): bundle bettercam/dxcam/windows-capture in installer
Lint & Test / test (push) Successful in 2m5s
The Windows installer was only shipping mss as a screen-capture
backend, so EngineRegistry.get_available_engines() reported just
['mss', 'camera', 'demo'] on installed builds. Picture sources
configured to use bettercam/dxcam/wgc were rejected at the
test/ws handshake with HTTP 403 (close-before-accept on
"Engine '<x>' not available").

Add the three Windows screen-capture wheels to WIN_DEPS so the
installer build matches a 'pip install -e .' dev environment.
2026-04-08 23:15:10 +03:00
alexei.dolgolyov 0e09eaf43b fix(launcher): set TCL_LIBRARY/TK_LIBRARY for embedded Python
Embedded Python ships with tcl8.6/ and tk8.6/ next to python.exe, but
Tcl's auto-detection searches <exe>/../lib/tcl8.6 — a path that doesn't
exist in our layout. Without these env vars, tkinter.Tk() raises
"Can't find a usable init.tcl", breaking the screen overlay (and any
other tk-based UI) on installed builds.
2026-04-08 23:14:58 +03:00
alexei.dolgolyov adfc39f9d1 chore: release v0.3.0
Build Release / create-release (push) Successful in 3s
Build Release / build-linux (push) Successful in 1m23s
Build Release / build-docker (push) Successful in 2m19s
Lint & Test / test (push) Successful in 3m10s
Build Release / build-windows (push) Successful in 3m29s
v0.3.0
2026-04-08 12:41:28 +03:00
alexei.dolgolyov d037a2e929 fix(tray): replace tkinter messagebox with Win32 MessageBoxW
Lint & Test / test (push) Successful in 2m3s
The packaged embedded Python distribution does not ship the tcl/tk
runtime, so tkinter.messagebox.askyesno crashed with 'Can't find a
usable init.tcl' when the user clicked Shutdown or Restart in the
tray menu. Use ctypes + user32.MessageBoxW instead — no tcl/tk,
no extra dependencies.
2026-04-08 12:16:32 +03:00
alexei.dolgolyov fc8ee34369 fix(launcher): start-hidden.vbs must be ASCII + CRLF, use python.exe
Lint & Test / test (push) Successful in 1m40s
Three separate bugs in the VBS launcher wedged together:

1. The previous fix added a UTF-8 em-dash in a comment. wscript.exe
   on Windows refused to execute the file with "Execution of the
   Windows Script Host failed. (Not enough memory resources are
   available to complete this operation.)" — a misleading error that
   actually means "I could not parse this file as ANSI VBScript".
   Fix: keep the file pure ASCII, convert to CRLF.

2. The launcher was invoking pythonw.exe. WshShell.Run spawning
   pythonw.exe inside the wscript host exited immediately (no process,
   no log). python.exe with WindowStyle=0 works reliably and matches
   the pattern used by the Media Server sibling app's VBS launcher,
   which has been running on this machine without issue.

3. The env vars (PYTHONPATH, WLED_CONFIG_PATH) must be set before the
   child process spawns, otherwise config.py falls back to the CWD
   default path that does not exist at install time.
2026-04-08 00:15:49 +03:00
alexei.dolgolyov e262a8b004 fix(launcher): set PYTHONPATH and WLED_CONFIG_PATH in start-hidden.vbs
Lint & Test / test (push) Successful in 2m1s
The VBS launcher (used by Start Menu, desktop, and autostart shortcuts
created by the NSIS installer) ran pythonw.exe without setting any env
vars. LedGrab.bat sets PYTHONPATH and WLED_CONFIG_PATH; the VBS did not.

With CWD set to the install root, config.py fell through to its default
lookup (./config/default_config.yaml), which does not exist there — the
real file is at app/config/default_config.yaml. The server silently ran
with built-in defaults on every shortcut launch: no devices, wrong data
dir, nothing persisted where the user expected.

The fix uses WshShell.Environment("Process") to set env vars on the
current VBS process, which child processes spawned via .Run inherit.
Kept CurrentDirectory = appRoot to preserve prior behavior for anyone
depending on CWD-relative paths inside the app.
2026-04-08 00:02:56 +03:00
alexei.dolgolyov d4ffe2e985 refactor: drop packaging dependency, inline version parsing
Lint & Test / test (push) Successful in 3m9s
The only user of 'packaging' was version_check.py — two small functions
(normalize_version, is_newer) that just need to parse "1.2.3-alpha.1"
and compare PEP 440-style versions. That's well within stdlib reach.

- Inline a NamedTuple-based Version with kind/pre_num ordering
  (dev < alpha < beta < rc < release), same regex-normalized format
- Define a local InvalidVersion exception
- Remove packaging>=23.0 from pyproject.toml dependencies

Why now: the Windows cross-build uses a hard-coded DEPS array in
build-dist-windows.sh, which was never updated when 'packaging' was
added on March 25. Result: importable from pip-installed dev envs,
missing from the portable installer — tray icon appeared but uvicorn
died with ModuleNotFoundError: No module named 'packaging'.

Removing the dep entirely is cleaner than adding one more hard-coded
entry to the Windows DEPS list. Tests (678 passing) and a manual test
matrix covering dev/alpha/beta/rc/release ordering all pass.
2026-04-07 23:54:27 +03:00
alexei.dolgolyov feb91ad281 fix(build): fix shell syntax error in smoke_test_imports heredoc
Lint & Test / test (push) Successful in 2m6s
The 'cmd <<EOF || { ... }' pattern confuses bash's parser — the closing
brace of the inline error block collides with the function's closing
brace. Rewrote to capture the python script into a local var via
$(cat <<EOF), then run it with -c and a plain 'if !' guard.
2026-04-07 23:41:42 +03:00
alexei.dolgolyov 17c5c02993 fix(build): keep .py sources + make smoke test skip uninstalled modules
Lint & Test / test (push) Successful in 1m36s
- compile_and_strip_sources: stop deleting .py files after compileall.
  OpenCV's loader does literal file I/O on cv2/config.py (not a Python
  import), so stripping it breaks `import cv2` with "missing
  configuration file: ['config.py']". Other packages may do similar
  file-based introspection tricks — the ~30% size win isn't worth
  playing whack-a-mole with broken installers. We already hit this
  with numpy.linalg and zeroconf._services; enough incidents.
- smoke_test_imports: only assert importability for modules whose
  top-level dir actually exists in site-packages. Pillow for example
  is a Windows-only dep, and was failing the Linux build spuriously.
  Rewrote as a heredoc for readability.
2026-04-07 23:37:54 +03:00
alexei.dolgolyov fd6776aeac fix(build): stop stripping zeroconf/_services + add import smoke test
Lint & Test / test (push) Successful in 2m39s
- build-common.sh: remove zeroconf/_services from the strip list.
  zeroconf's compiled Cython _listener.pyd imports from _services
  internally, so stripping it broke `import zeroconf` at runtime with
  ModuleNotFoundError — same class of bug as the numpy.linalg strip.
- build-common.sh: add smoke_test_imports() that imports every top-level
  dependency against the stripped site-packages. Catches "we stripped
  something that was actually needed" regressions at build time instead
  of on a user's machine after install.
- build-dist.sh: wire smoke test into the Linux flow (runs real imports).
- build-dist-windows.sh: cross-build can't load win_amd64 .pyd files with
  the host python, so instead verify that the known-required submodule
  dirs (numpy.linalg/lib/matrixlib/ma, zeroconf._services) exist after
  cleanup. Fails loud if any future strip-rule removes them.
2026-04-07 23:32:50 +03:00
alexei.dolgolyov 9f34ffb0a0 fix(build): stop stripping numpy.lib/linalg from site-packages
Lint & Test / test (push) Successful in 2m57s
numpy's own __init__.py imports lib and matrixlib (which in turn imports
numpy.linalg via defmatrix.py). Removing any of these submodules to save
dist size makes `import numpy` raise ModuleNotFoundError on the target,
which cascades into every wled_controller import.

Symptom: v0.0.0.dev0 Windows installer showed a tray icon but uvicorn
died silently in its background thread and port 8080 never listened —
browser got ERR_CONNECTION_REFUSED.

Keep stripping: polynomial, distutils, f2py, typing, _pyinstaller.
These are genuinely unused by numpy's own import chain.
2026-04-07 23:17:12 +03:00
alexei.dolgolyov b5842e6424 fix(build): normalize non-PEP440 versions, fix .py/compileall ordering, wipe NSIS payload dirs
Lint & Test / test (push) Successful in 3m18s
- build-common.sh: detect_version() normalizes non-PEP440 labels (e.g. 'dev',
  'nightly') to 0.0.0.dev0 so stamping pyproject.toml doesn't break pip install.
- build-common.sh: split .py deletion out of cleanup_site_packages into a new
  compile_and_strip_sources() that runs 'compileall -b' FIRST, then removes
  sources. compileall now fails loud instead of silently no-op'ing.
- build-dist.sh: add missing compile_and_strip_sources call on Linux site-packages
  (previous tarballs shipped empty packages with no .py and no .pyc).
- build-dist-windows.sh: reorder so compile_and_strip_sources runs right after
  cleanup_site_packages, not after .py files have already been deleted.
- installer.nsi: RMDir /r payload dirs (python/, app/, scripts/) and delete
  LedGrab.bat at the top of SecCore before File /r. NSIS File /r MERGES into
  existing dirs, so upgrades left half-old/half-new state that surfaced as
  'version mismatch' or duplicate-package ImportErrors. data/ and logs/ remain
  untouched to preserve user config.
2026-04-07 23:04:38 +03:00
alexei.dolgolyov 7a9c368448 refactor: split color-strips.ts into focused modules under color-strips/ folder
Lint & Test / test (push) Successful in 2m6s
Monolithic 3060-line color-strips.ts split into 11 modules:
- index.ts (core orchestrator, modal, type switching, editor, save, CRUD)
- cards.ts (card rendering for all source types)
- game-event.ts (game event mappings, presets, UI)
- gradient.ts (gradient entity modal + CRUD)
- audio.ts (audio viz widgets, load/reset state)
- math-wave.ts (wave layers + waveform selects)
- mapped.ts (mapped zone helpers)
- color-cycle.ts (color cycle add/remove/render)
- composite.ts, notification.ts, test.ts (previously extracted, moved into folder)
2026-04-05 12:54:15 +03:00
alexei.dolgolyov ce53ca6872 feat: add card glare effect to dashboard and perf chart cards
Lint & Test / test (push) Has been cancelled
Extend cursor-tracking spotlight to .dashboard-target and
.perf-chart-card elements with a smaller 100px radius suited
for compact cards.
2026-04-05 12:23:39 +03:00
alexei.dolgolyov b04978af58 feat: add music sync viz modes and auto_gain audio filter
Lint & Test / test (push) Has been cancelled
Add 4 new audio visualization modes powered by MusicAnalyzer:
- pulse_on_beat: BPM-synced pulsing with smooth beat phase
- energy_gradient: bass/mid/treble mapped to scrolling gradient
- spectrum_bands: three VU zones for frequency bands
- strobe_on_drop: state-driven strobe on detected musical drops

MusicAnalyzer provides BPM estimation (median IBI), beat phase tracking,
asymmetric energy envelope, 3-band frequency splitting, and drop
detection state machine (idle/buildup/drop/recovery).

Add auto_gain audio filter for automatic level normalization via rolling
peak tracking with configurable target level and response time.

Deprecate auto_gain on Audio Value Source (use the filter instead).
2026-04-05 01:40:34 +03:00
alexei.dolgolyov 6e8b159126 fix: weather CSS card shows empty source name after hard refresh
Lint & Test / test (push) Has been cancelled
Weather sources cache was not fetched during streams tab load, so the
card renderer could not resolve weather source names. Also widened
lat/lon inputs from 80px to 100px to fit longer coordinates.
2026-04-05 00:49:28 +03:00
alexei.dolgolyov ace24715c8 feat: add math_wave color strip source type
Lint & Test / test (push) Has been cancelled
Mathematical wave generator that produces per-LED colors from
configurable waveform layers (sine, triangle, sawtooth, square) with
superposition, mapped through a gradient palette. Supports sync clocks,
bindable speed, and up to 8 wave layers.

- Storage model with wave validation and apply_update
- Numpy-vectorized stream with gradient LUT color mapping
- API schemas (create/update/response) and route registration
- Frontend editor with dynamic wave layer rows, gradient picker,
  speed widget, and IconSelect waveform selectors
- i18n in en/ru/zh
2026-04-05 00:41:07 +03:00
alexei.dolgolyov edc6d27e2e fix: replace HA test icon with refresh, make automation rules collapsible
Lint & Test / test (push) Has been cancelled
Use refresh icon instead of flask for HA test connection buttons.
Add collapse/expand chevron to automation rule rows (collapsed by default).
2026-04-04 21:28:51 +03:00
alexei.dolgolyov b7da4ab6b5 feat: add Integrations tab and responsive icon-only tabs
Lint & Test / test (push) Successful in 1m48s
Move HA sources, weather sources, game integration, and MQTT settings
into a dedicated Integrations top-level tab with tab-registry pattern.
Collapse tab labels to icon-only at narrow desktop widths (<=1100px)
to prevent toolbar overflow.
2026-04-02 15:29:38 +03:00
alexei.dolgolyov 99460a8043 fix: make pystray a core dependency on Windows instead of optional extra
Lint & Test / test (push) Successful in 2m5s
The tray icon should always appear on Windows, not only when installed
with the [tray] extra.
2026-04-02 14:22:18 +03:00
alexei.dolgolyov 89990f8d63 chore: remove processed-audio-sources plan files 2026-04-02 13:42:37 +03:00
alexei.dolgolyov 0cc0aaa411 feat: processed audio sources with composable filter pipeline
Replace hardcoded MonoAudioSource/BandExtractAudioSource with a
composable ProcessedAudioSource + AudioProcessingTemplate + AudioFilter
system. 11 audio filters: channel extract, band extract, peak hold,
gain, noise gate, envelope follower, spectral smoothing, compressor,
inverter, beat gate, delay. Full frontend UI with filter editor,
tree navigation, and i18n support.
2026-04-02 13:42:18 +03:00
alexei.dolgolyov af2c89c8df fix: audio tree structure, filter i18n, and IconSelect for filter options
Restructure audio tree nav into Capture (Sources + Engine Templates)
and Processed (Sources + Filter Templates) subgroups.
Add missing i18n description keys for all 11 audio filters.
Wrap plain select filter options with IconSelect grids.
2026-04-02 13:37:50 +03:00
alexei.dolgolyov d04192ffb7 fix: add reference check before deleting audio processing template
Prevent deleting templates that are still referenced by
ProcessedAudioSource entities. Returns 400 with source names.
2026-04-01 23:28:58 +03:00
alexei.dolgolyov 992495e2e4 fix: isolate tests from production database
Tests that imported wled_controller.main at module level caused the real
production database (data/ledgrab.db) to be opened before test fixtures
could patch the config. This led to silent data loss.

Patch the global config singleton at conftest module level (before any
test imports main.py) to redirect all DB access to a temp directory.
2026-04-01 19:01:56 +03:00
alexei.dolgolyov 6b0e4e5539 feat(processed-audio-sources): phase 8 - frontend design consistency review
Fix audio source modal error class (modal-error), use Modal.showError(),
reorder audio source card description, remove redundant APT filter count
badge, clean up unused imports in audio-sources.ts.
2026-03-31 23:11:17 +03:00
alexei.dolgolyov ce1f4847f3 feat(processed-audio-sources): phase 7 - testing and polish
Fix test_list_filters test (filter_id field name mismatch).
Add tests for audio filters, template store, and source store.
All 678 tests pass, ruff clean, tsc clean, esbuild clean.
No dead code remaining from old source types.
2026-03-31 22:50:02 +03:00
alexei.dolgolyov 1ce0dc6c61 feat(processed-audio-sources): phase 6 - frontend source type cleanup
Rewrite audio source editor modal for capture/processed types only.
Remove old multichannel/mono/band_extract HTML sections and i18n keys.
Clean up legacy DOM section null-checks in audio-sources.ts.
2026-03-31 19:40:37 +03:00
alexei.dolgolyov 553463935e feat(processed-audio-sources): phase 5 - frontend audio processing templates
Add Audio Processing Templates management UI to Streams tab:
- Template editor modal with filter list via FilterListManager
- CardSection with reconciliation for template cards
- DataCache instances for templates and audio filter defs
- Audio filter icon mappings in filter-list.ts
- i18n keys in en/ru/zh locales
2026-03-31 19:32:17 +03:00
alexei.dolgolyov ab43578049 feat(processed-audio-sources): phase 4 - runtime filter integration
Add AudioFilterPipeline for chained filter execution on AudioAnalysis.
Wire filter pipelines into AudioColorStripStream, AudioValueStream,
and WebSocket test endpoint. Add hot-update support via
ProcessorManager.refresh_audio_filter_pipelines(). Thread
AudioProcessingTemplateStore through dependency injection hierarchy.
2026-03-31 19:15:29 +03:00
alexei.dolgolyov 353c090b42 feat(processed-audio-sources): phase 3 - processed audio source model
Replace MultichannelAudioSource with CaptureAudioSource, add
ProcessedAudioSource (audio_source_id + audio_processing_template_id),
remove MonoAudioSource and BandExtractAudioSource entirely.
Update store resolution to walk processed chains collecting template IDs.
Update all API schemas, routes, and frontend references.
2026-03-31 19:01:46 +03:00
alexei.dolgolyov eb94066386 feat(processed-audio-sources): phase 2 - implement 11 audio filters
Add all audio filters that transform AudioAnalysis data:
- Channel Extract, Band Extract (migration from old source types)
- Peak Hold, Gain, Noise Gate, Envelope Follower
- Spectral Smoothing, Compressor, Inverter, Beat Gate, Delay
All registered via AudioFilterRegistry with option schemas.
2026-03-31 18:43:36 +03:00
alexei.dolgolyov 86a9d344e6 feat(processed-audio-sources): phase 1 - audio filter framework
Add the foundation for audio processing filters, mirroring the existing
picture filter/postprocessing template system:
- AudioFilter base class, AudioFilterRegistry, AudioFilterOptionDef
- AudioProcessingTemplate dataclass + SQLite-backed store
- audio_filter_template meta-filter with recursive resolution
- Full REST API: CRUD templates + filter registry discovery
- Dependency injection wired in dependencies.py and main.py
2026-03-31 17:35:39 +02:00
alexei.dolgolyov c59107c7c7 feat: refactor MQTT from global config to multi-instance entity model
Lint & Test / test (push) Successful in 1m32s
MQTT broker connections are now managed as entities (like HA sources)
instead of a single global config. Each MQTTSource gets its own
runtime with auto-reconnect, ref-counted via MQTTManager.

Backend:
- MQTTSource dataclass + MQTTSourceStore (SQLite)
- MQTTRuntime (per-broker connection, refactored from MQTTService)
- MQTTManager (ref-counted pool, same pattern as HAManager)
- CRUD API at /api/v1/mqtt/sources + test + status endpoints
- MQTTRule gains mqtt_source_id field for source selection
- Automation engine acquires/releases MQTT runtimes automatically
- Legacy MQTTService kept for backward compat during transition

Frontend:
- MQTT source cards in Streams > Integrations tab
- Create/edit modal with test button
- Dashboard integration cards with health-dot indicators
- Removed MQTT tab from settings modal
2026-03-31 18:02:19 +03:00