Commit Graph

57 Commits

Author SHA1 Message Date
alexei.dolgolyov a5effba553 feat: aggregated snapshot + wiring-graph APIs, MQTT device brokers
Backend
- snapshot: GET /api/v1/snapshot aggregates targets, devices, sources,
  presets and system into one payload for the HA coordinator, collapsing
  the prior ~2N+M request fan-out; per-section ?include= gating.
- graph: GET /api/v1/graph{,/schema,/dependents} backed by a pure,
  unit-tested graph_schema engine — one authoritative connectable-field
  registry so the editor no longer hard-codes topology in two places.
- devices: thread mqtt_source_id through DeviceCreate/Update/Response and
  the routes for multi-broker MQTT; shared validate_mqtt_source_exists
  (_mqtt_validation.py) reused by device + output-target routes; stop
  update_device masking intentional 4xx as 500.
- shutdown: bound uvicorn graceful-shutdown via GRACEFUL_SHUTDOWN_TIMEOUT
  (shared by __main__, android_entry, demo) so a lingering events WebSocket
  can't strand LED targets or block process exit.
- access log: structured _access_log middleware attributing each request to
  its authenticated token label (never the secret); uvicorn access_log off.

Frontend
- graph editor: generic schema-driven port/edge rendering, layout and
  connection handling; service-worker refresh.
- device modals: MQTT broker EntitySelect for device_type=mqtt in add-device
  and settings, wired into load/save/validate/dirty-check/clone.
- i18n: en/ru/zh keys.

Tests: graph routes + schema, snapshot routes, access log, mqtt_source_id
device regressions, bounded-shutdown entrypoint. 1614 passed.
2026-05-28 22:51:04 +03:00
alexei.dolgolyov fd46c51dba docs: TODO + CLAUDE.md notes + locale keys for new features
TODO.md grows the device-support follow-up roadmap. CLAUDE.md trims a
stale section. en/ru/zh locales add the strings used by the new
HTTP-endpoint editor, MiniSelect labels, automations expansion, and
value-source kinds. Ru/zh parity for the older keys is tracked
separately in REVIEW_TODO.md.
2026-05-23 00:50:31 +03:00
alexei.dolgolyov 17684afba1 docs: record review-fix pass in TODO.md
Marks the pre-merge code review as complete in TODO.md and lists each
finding alongside the commit that closed it. Branch is in shape to merge.
2026-05-16 11:06:51 +03:00
alexei.dolgolyov 390d2b472c docs: mark expand-device-support branch ready for merge
Updates TODO.md to reflect the verification pass outcome:
- pixel_reduce extraction marked done (commit cc87fba)
- pre-merge verification pass marked complete: 1358 pytest tests pass,
  ruff and black clean (against pre-commit-pinned 24.10.0),
  npx tsc --noEmit clean, bundle compiles
- Mi-Light / MiBoxer explicitly marked WONTDO with rationale
  (esp8266_milight_hub firmware -> existing MQTT target is the better
  path for modern Mi-Light deployments)
- duplicate Nanoleaf bullet removed; Twinkly noted as deferred

No code changes -- this is purely a checkpoint for the merge review.
2026-05-16 04:18:33 +03:00
alexei.dolgolyov 426484adf8 feat(devices): Nanoleaf OpenAPI target type + first pair-flow user
Adds support for Nanoleaf controllers (Light Panels / Canvas / Shapes /
Lines / Elements) via the documented HTTP REST API on port 16021.
First concrete consumer of the pair-UX scaffold from commit 2f31680 --
the abstraction is no longer speculative.

Backend:
- NanoleafClient is a single-pixel HTTP adapter: averages the strip to
  one RGB triple, converts to Nanoleaf's HSB scale (H 0-360 / S 0-100 /
  B 0-100), and PUTs to /api/v1/<token>/state with duration:0 so
  transitions are instant for ambilight. Brightness is clamped to >=1
  because Nanoleaf rejects brightness=0.
- pair_nanoleaf(host) implements the two-step handshake: POST
  /api/v1/new during the 30-second pairing window the controller opens
  after the user holds the power button for 5 s.
    200 -> {auth_token: "..."}
    403 -> raises PairingNotReady ("Hold the power button...")
    other / transport error -> RuntimeError wrapping the cause
- NanoleafDeviceProvider.pair_device returns {nanoleaf_token: ...}
  forwarded by POST /api/v1/devices/pair to the frontend for inclusion
  in the subsequent create payload.
- mDNS discovery via _nanoleafapi._tcp (and the v1 variant); failures
  yield [] rather than raising.
- Health check probes /api/v1 without a token (401/403 still proves
  the host is alive).
- NanoleafConfig has nanoleaf_token + nanoleaf_min_interval_ms
  (default 100 ms = ~10 Hz; HTTP overhead caps practical max ~20 Hz).
- Auth token encrypted at rest via _enc/_dec, matching Hue / BLE-Govee.
- 42 unit tests cover URL parsing, RGB->HSB conversion, pairing
  handshake (200 / 403 / 500 / missing-token / transport-error),
  state mutations, brightness clamp, set_power / set_brightness /
  set_color, connection lifecycle, provider validate / pair /
  discover / capabilities, and Device.to_config round-trip including
  the encrypted-token roundtrip via to_dict + from_dict.

Frontend:
- 'nanoleaf' in DEVICE_TYPE_KEYS (next to 'govee'), HEXAGON icon
  (deliberate departure from the smart-bulb lightbulb family --
  Nanoleaf is panels, not bulbs, and the brand identity is hexagonal).
- isNanoleafDevice predicate + per-type field show/hide.
- Pair flow integration: when the device type is Nanoleaf, the add-
  device modal retitles its submit button to "Pair Device" and
  intercepts the submit. handleAddDevice awaits
  runPairingFlow({deviceType: 'nanoleaf', url}), merges result.fields
  ({nanoleaf_token}) into the create body, then POSTs. On
  PairingCancelled the user stays on the modal silently.
- Settings modal exposes the rate-limit field and a read-only
  "Paired" indicator reusing the pair-modal success badge. The token
  itself is never rendered to the DOM and never sent on update --
  re-pairing requires delete + re-add.
- Per-type pairing instructions in en/ru/zh
  (device.nanoleaf.pair.instructions) that the scaffold's i18n lookup
  resolves automatically.
- Bundle: +6.4 KiB (pairing-flow.ts was tree-shaken before this
  commit; now both it and the Nanoleaf branches are baked in).

The pair-UX scaffold is now proven, not speculative. Tuya and Twinkly
can follow the same shape when their phases arrive.
2026-05-16 03:59:38 +03:00
alexei.dolgolyov 2f31680823 feat(devices): pairing-UX scaffold (Phase 2)
Lays the groundwork for device families that require a one-time
physical pairing action (Nanoleaf hold-power-button, Tuya local-key
extraction, Twinkly network-setup mode, Hue link-button). No driver
uses it yet -- Nanoleaf will be the first concrete consumer.

Phase 2 as originally written had three bullets; only this one was
genuinely missing work. The other two (generic NetworkDiscoveryService
fan-out, unified scan-network UI) were already solved at the route
level by the existing /api/v1/devices/discover handler running all
providers in parallel via asyncio.gather(return_exceptions=True).
Marked WONTDO in TODO.md with rationale.

Backend:
- LEDDeviceProvider gains an async pair_device(url) -> dict method.
  Default raises NotImplementedError so missing implementations on a
  requires_pairing provider fail loud at request time.
- New PairingNotReady exception, distinct from generic errors so the
  route handler can return 409 (user must perform the physical action,
  retry possible) instead of 500.
- POST /api/v1/devices/pair endpoint with PairDeviceRequest /
  PairDeviceResponse schemas. Status-code mapping:
    200 -> paired, fields returned for the subsequent create payload
    400 -> unknown device type, or type doesn't support pairing
    409 -> PairingNotReady (retryable from the UI)
    422 -> invalid URL / device configuration (ValueError)
    502 -> transport / network failure (other exceptions)
    500 -> provider returned a non-dict (defensive)
- 8 route tests register a stub provider and exercise every
  status-code path.

Frontend:
- New modals/pair-device.html with five state blocks (idle / pairing
  / not_ready / success / failed) toggled via data-pair-state, plus
  a 30-second SVG progress ring with monospace countdown.
- New features/pairing-flow.ts exposing
  runPairingFlow({deviceType, url, instructionsKey?}) ->
  Promise<{fields: Record<string, unknown>>. Wires the modal to the
  pair endpoint, maps response codes to UI states, AbortControllers
  in-flight fetches on cancel. Exports a PairingCancelled sentinel
  error class.
- Generic pairing.* i18n keys in en/ru/zh. Drivers will add their own
  device.<type>.pair.instructions key that overrides the default.

Design decisions (per frontend-design skill):
- Single SVG ring + centered countdown (HomeKit-style)
- Instructions stay visible during pairing, dimmed to 60% via :has()
- Success state held 450 ms before auto-dismiss
- Cancel-X in the footer; primary action lives in the state block
- prefers-reduced-motion disables pulse/fade/ring transitions

Note: the components.css diff includes a pre-existing MiniSelect block
from the user's parallel work; pairing-specific styles are the second
hunk (lines ~1628+).
2026-05-16 03:26:53 +03:00
alexei.dolgolyov 31c6c3abb2 feat(devices): Open Pixel Control (OPC) target type
Adds support for Open Pixel Control receivers (Fadecandy boards,
xLights/Falcon endpoints, OPC bridges, art-installation controllers,
hobbyist LED driver software). OPC is a tiny TCP protocol on port
7890 with a 4-byte header [channel][cmd][len_hi][len_lo] + RGB body.

Backend:
- OPCClient opens one persistent TCP connection and streams frames as
  header+body byte pairs. Channel 0 broadcasts to every output on the
  OPC server; channels 1-255 address a specific channel on multi-output
  servers (Fadecandy with multiple Open Pixel chains).
- supports_fast_send=True with a synchronous send_pixels_fast hot path.
  The fast path skips the async drain so the OS write-buffer flushes
  on its own schedule -- exactly what ambilight streaming wants.
- Brightness applies client-side before the frame is sent (OPC has no
  reply channel for hardware-side brightness).
- Health check opens a TCP connection and closes it.
- OPCConfig joins the typed config union; storage gains an opc_channel
  field; full to_dict/from_dict/to_config wiring.
- 36 unit tests cover URL parsing, header construction, send_pixels
  emitting header+body in order, brightness application, list and
  flat-array input shapes, drain behavior, connection lifecycle,
  provider validate/discover/capabilities, Device.to_config round-trip.

Frontend:
- 'opc' in DEVICE_TYPE_KEYS (next to 'ddp'), paper-plane icon -- same
  as DDP since both are open pixel-streaming protocols.
- isOpcDevice predicate + per-type field show/hide.
- Optional channel number input (default 0 = broadcast) with hint copy
  explaining the channel semantics.
- Locale strings in en/ru/zh.

No native discovery (OPC has no discovery protocol); users supply
the receiver IP manually.
2026-05-16 03:02:41 +03:00
alexei.dolgolyov 887131d4af feat(devices): Govee LAN target type
Adds support for Govee Wi-Fi smart bulbs and ambient-lighting kits via
their LAN API (opened in 2023). Discovery is multicast UDP on
239.255.255.250:4001; control commands go unicast to the device's port
4003; responses arrive on port 4002.

Each device requires "LAN Control" toggled ON in the Govee Home app
(Device -> settings -> LAN Control). Devices with LAN Control disabled
silently fail to appear in discovery and won't respond to commands; the
UI hint copy reminds users.

Backend:
- GoveeClient is a single-pixel UDP adapter: averages the strip to one
  RGB triple and pushes a 'colorwc' command with colorTemInKelvin=0 to
  select pure RGB mode (non-zero kelvin would switch the bulb to CCT
  mode and ignore the RGB values).
- Brightness folds into the RGB scaling so we burn one packet per
  frame instead of two.
- supports_fast_send=True with a synchronous send_pixels_fast hot path.
  Default rate gate 50 ms (~20 Hz); UDP fire-and-forget tolerates it.
- Multicast discovery: scan request to 239.255.255.250:4001, listen on
  port 4002, parse the inner data dict for IP + device-id + SKU +
  firmware version. Degrades to [] when port 4002 is already bound or
  network is unavailable.
- Health check sends devStatus and waits 1.5s for any reply; the error
  message points at the LAN-Control toggle since that's the #1 root
  cause of silent failures.
- GoveeConfig joins the typed config union; storage gains
  govee_min_interval_ms; full to_dict/from_dict/to_config wiring.
- 40 unit tests cover URL parsing, scan-reply parsing (rejecting
  non-scan commands and malformed JSON), payload builders (colorwc
  with colorTemInKelvin=0, brightness clamping, power as 1/0 not
  true/false), strip averaging, rate limiting, fast-send hot path,
  provider validate/discover/health, Device.to_config round-trip.

Frontend:
- 'govee' in DEVICE_TYPE_KEYS (next to 'lifx'), lightbulb icon
  (deliberate smart-bulb family grouping).
- isGoveeDevice predicate + per-type field show/hide.
- Rate-limit number input (default 50 ms).
- URL hint copy explicitly instructs users to enable LAN Control in
  the Govee Home app -- the #1 source of "why isn't my Govee
  responding?" support churn.
- Locale strings in en/ru/zh.
2026-05-16 02:47:15 +03:00
alexei.dolgolyov 8f9d490063 feat(devices): LIFX LAN target type
Adds support for LIFX smart bulbs and lightstrips that speak the LIFX
binary UDP protocol on port 56700, with broadcast LAN discovery via the
standard GetService/StateService probe.

Backend:
- LIFXClient is a single-pixel UDP adapter: averages the strip to one
  RGB triple, converts to LIFX HSBK (16-bit hue/saturation/brightness +
  kelvin), and pushes a tagged SetColor packet so all bulbs on the
  subnet act on it. Brightness folds into the HSBK brightness channel.
- Hand-rolled packet builder: 36-byte LIFX header (frame +
  frame-address + protocol-header) + variable-length payload. Source
  ID 'LGGR' identifies LedGrab in protocol logs.
- supports_fast_send=True with a synchronous send_pixels_fast hot path
  -- UDP costs nothing, so the default rate gate is 50 ms (~20 Hz) to
  match LIFX's documented <=20 cmd/sec recommendation.
- Broadcast discovery sends GetService and parses StateService replies
  back into IP + MAC + service-port triples. Broadcast failures yield
  [] rather than raising.
- Health check sends GetService and waits 1.5s for any reply on a
  one-shot UDP socket.
- LIFXConfig joins the typed config union; Device storage gains a
  lifx_min_interval_ms field; full to_dict/from_dict/to_config wiring.
- 47 unit tests cover URL parsing, RGB->HSBK conversion (red/green/
  blue/white/black/clamping), packet construction (size, msg type,
  tagged flag, target MAC, sequence byte), SetColor and SetPower
  payload layouts, StateService reply parsing (including rejection
  of wrong msg types and runt payloads), strip averaging, rate
  limiting, fast-send hot path, provider validate/discover/health,
  and Device.to_config round-trip.

Frontend:
- 'lifx' in DEVICE_TYPE_KEYS (next to 'wiz'), lightbulb icon
  (deliberate smart-bulb family grouping with Hue + Yeelight + WiZ).
- isLifxDevice predicate + per-type field show/hide in create and
  settings modals.
- Rate-limit number input (default 50 ms) in both modals with hint
  text referencing LIFX's documented <=20 cmd/sec ceiling.
- Locale strings in en/ru/zh.

LIFX bulbs are reachable from the existing "Scan network" button -- no
new discovery UI affordance was needed. No brightness_control capability
exposed; LIFX brightness is folded into the HSBK on the wire.
2026-05-16 02:30:30 +03:00
alexei.dolgolyov ede627b4ac feat(devices): WiZ Connected LAN target type
Adds support for WiZ Connected (Philips' budget-tier) smart bulbs that
accept JSON commands as UDP datagrams on port 38899 with broadcast LAN
discovery on 255.255.255.255:38899.

Backend:
- WiZClient is a single-pixel UDP adapter: averages the incoming strip
  to one RGB triple and pushes it via setPilot with r/g/b params.
  Brightness folds into the RGB scaling so we burn one packet per frame
  instead of two.
- UDP fire-and-forget tolerates high update rates with no ack overhead,
  so the default rate gate is 50 ms (~20 Hz) -- 10x faster than Yeelight.
- supports_fast_send=True with a synchronous send_pixels_fast hot path.
- Broadcast discovery sends the standard registration envelope; bulb
  replies are parsed for IP+MAC and surfaced as DiscoveredDevice
  entries. Broadcast failures (no network, firewall) yield [] rather
  than raising.
- Health check sends getPilot and waits 1.5s for any reply on a
  one-shot UDP socket.
- WiZConfig joins the typed config union; Device storage gains a
  wiz_min_interval_ms field; full to_dict/from_dict/to_config wiring.
- 36 unit tests cover URL parsing, MAC extraction, strip averaging,
  rate limiting, fast-send hot path, provider validate/discover/health,
  and Device.to_config round-trip.

Frontend:
- 'wiz' in DEVICE_TYPE_KEYS (next to 'yeelight'), lightbulb icon
  (deliberate smart-bulb family grouping with Hue + Yeelight).
- isWizDevice predicate + per-type field show/hide in create and
  settings modals.
- Rate-limit number input (default 50 ms) in both modals with hint
  text noting the UDP fire-and-forget characteristic.
- Locale strings in en/ru/zh.

WiZ bulbs are reachable from the existing "Scan network" button -- no
new discovery UI affordance was needed.
2026-05-16 02:12:01 +03:00
alexei.dolgolyov 4b65005823 feat(devices): Yeelight LAN target type
Adds support for Xiaomi/Yeelight smart bulbs and lightstrips that speak
the bulb-vendor's JSON-RPC protocol over TCP port 55443 with SSDP-style
LAN discovery on 239.255.255.250:1982.

Backend:
- YeelightClient is a single-pixel adapter: it averages the incoming
  strip down to one RGB triple, packs it into the 24-bit color int the
  bulb expects, and pushes it via set_rgb with sudden+0ms effect.
- Brightness folds into the RGB scaling on the wire so we burn one
  command per frame instead of two.
- A configurable client-side rate gate (yeelight_min_interval_ms, default
  500) keeps us under the bulb's ~1 cmd/sec cap. Frames that arrive
  inside the gate no-op without TX. Music mode (~60 Hz via reverse-TCP)
  is deferred -- the MVP caps at ~2 Hz and that's fine for a strip-to-
  single-pixel averaging device.
- SSDP discovery scans 239.255.255.250:1982 with the bulb-specific
  ST: wifi_bulb header; replies are parsed into DiscoveredDevice
  entries. Multicast failures (no network, firewall) yield [] rather
  than raising -- discovery is best-effort.
- Health check opens a TCP socket to the bulb and closes it.
- YeelightConfig joins the typed config union; Device storage gains a
  yeelight_min_interval_ms field; full to_dict/from_dict/to_config wiring.
- 34 unit tests cover URL parsing, RGB packing, strip averaging, rate
  limiting, SSDP response parsing, provider validate/discover/health,
  and Device.to_config round-trip.

Frontend:
- 'yeelight' in DEVICE_TYPE_KEYS (next to 'hue'), lightbulb icon
  (intentional family-grouping signal with Hue).
- isYeelightDevice predicate + per-type field show/hide in create and
  settings modals.
- Rate-limit number input (default 500 ms) in both modals with hint
  text explaining the trade-off.
- Locale strings in en/ru/zh.
- Drive-by: types.ts DeviceType union backfilled with 'ddp' and 'ble'
  for type-safety consistency.

Yeelight bulbs are now reachable from the existing "Scan network"
button -- no new discovery UI affordance was needed.
2026-05-16 01:44:13 +03:00
alexei.dolgolyov 8f1140abad feat(devices): standalone DDP target type
Promotes the existing DDP packet layer (previously WLED-internal) to a
first-class device type so any DDP-speaking receiver (Pixelblaze,
ESPixelStick, xLights/Falcon endpoints, generic firmware) can be driven
directly without WLED in the path.

Backend:
- New DDPLEDClient wraps the DDPClient transport as a proper LEDClient
  with supports_fast_send=True (synchronous UDP push on the hot loop).
- New DDPDeviceProvider — no native discovery, manual LED count,
  capabilities = {manual_led_count, health_check}.
- DDPConfig joins the typed config union; Device storage gains
  ddp_port / ddp_destination_id / ddp_color_order fields with safe
  defaults (0/1/1 -> port 4048, destination 1=display, RGB byte order).
- URL scheme: ddp://host[:port] or bare host[:port] (default 4048).
- Health check resolves the host via async DNS; UDP has no reply
  channel so reachability is best-effort by design.
- 29 new tests in test_ddp_led_client.py cover URL parsing, packet
  hot path (brightness, list/numpy input shapes, fast vs async send),
  provider validate/discover/capabilities, config round-trip via
  Device.to_config() and to_dict/from_dict.

Frontend:
- 'ddp' in DEVICE_TYPE_KEYS (next to 'dmx'), paper-plane icon.
- isDdpDevice predicate + per-type field show/hide in the create &
  settings modals.
- Color-order picker uses IconSelect (project rule bans plain select).
- Locale strings added in en/ru/zh.

Note: this commit also carries two pre-existing in-flight hunks that
were intermixed in the same files and could not be split out
non-interactively:
- api/routes/devices.py: URL-scheme inference for bare WLED hosts,
  safer error messages, exception-isolated parallel discovery.
- storage/device_store.py: secret_box helpers + at-rest encryption of
  Hue / BLE-Govee / MQTT credentials.
Both are independent of DDP and intentional per the user.
2026-05-16 01:26:45 +03:00
alexei.dolgolyov 530316c2c3 feat(mqtt): multi-broker MQTT + Zigbee2MQTT light target
- New Z2MLightOutputTarget storage, processor, editor and routes for
  Zigbee2MQTT light entities (shares the HA-Light editor UI via the new
  light-target-editor module)
- Replace global MQTTService/MQTTConfig with per-source MQTTManager +
  MQTTRuntime; thread mqtt_source_id through Z2M targets, DIY MQTT
  devices, and the automation engine
- Migrate legacy single-broker YAML/env config to a "Default Broker"
  MQTTSource on startup (core/mqtt/legacy_migration.py) and drop the
  obsolete core/mqtt/mqtt_service.py
- Refresh /api/v1/system integration status to surface every MQTT source
- Extract shared light-target editor and refactor OutputTargetStore +
  output_targets routes around typed factories / auto-registry
- Modal CSS polish, locale strings, and storage/bindable test coverage
2026-05-12 18:06:09 +03:00
alexei.dolgolyov e65dcb41f4 chore: clean up cfg abbreviation and stale TODO link
Rename `cfg` parameter/local in resolve_mqtt_password to `config`
for PEP 8 compliance. Drop the broken reference to the long-removed
docs/plans/device-typed-configs.md from TODO.md.
2026-05-10 23:19:15 +03:00
alexei.dolgolyov 0f5850ef80 feat(ui): customisable card icon for all entity types
Extends the icon-plate work from devices and output targets to every
remaining card type — 18 new entities, 20 in total. Users can now pick
a curated icon (with optional colour override) for any card on any tab,
and the picker reuses the same modal, recent-strip, search, and
category tabs introduced for the device picker.

Foundation:
- icon-picker.ts — replace the hardcoded 2-entry adapter record with a
  Map<EntityType, EntityTypeAdapter> and expose
  registerIconEntityType() + makeSimpleIconAdapter() so each feature
  module owns its own adapter (~6 lines per type).
- bodyExtras hook on adapters, keyed off id, lets discriminated routes
  (output-targets target_type, picture-sources stream_type, audio /
  value / color-strip-sources source_type) accept icon-only PUTs.
- core/card-icon.ts — new makeCardIconFields(type, id, entity) helper
  spreads iconHtml / iconColor / iconAttrs into a mod-card head in one
  line.
- _onDocumentClick now accepts any registered type instead of a
  hardcoded device/target check.

Backend (purely additive — no migrations needed thanks to JSON-blob
storage):
- 18 dataclasses gained icon: str = "" + icon_color: str = "" with
  emit-when-truthy serialisation and "" defaults on load.
- All matching Create / Update / Response Pydantic schemas gained the
  fields with the standard Optional[str] + max_length=64/32 +
  description set.
- All routes' response builders use
  getattr(entity, "icon", "") or "" so existing rows render unchanged.
- ValueSource and CSS handle icon/icon_color on the base class so all
  source-type subclasses inherit them automatically.

Frontend wiring (12 modules):
- streams.ts — picture sources, capture templates, PP templates,
  CSPT, audio sources, audio templates, gradients (built-in
  gradients keep no plate).
- automations, scene-presets, sync-clocks, weather-sources,
  value-sources, mqtt-sources, home-assistant-sources,
  game-integration, audio-processing-templates, assets,
  color-strips/cards.
- pattern-templates skipped — uses the legacy wrapCard({content,
  actions}) string API, separate migration.

Dashboard cards now also display the chosen icon:
- Targets already had it (with device inheritance for LED targets).
- Sync clocks, automations, and scene presets gained the same plate
  via a shared _dashboardIconPlate helper that mirrors the mod-card
  layout (mod-head--with-icon class flips on when present).

i18n: 20 new device.icon.entity.<type> labels in en/ru/zh.

Verification:
- ruff check src/ tests/ — clean.
- npx tsc --noEmit — clean.
- npm run build — 2.6 MB bundle.
- pytest tests/ --no-cov — 949 passed (no regressions).

Pending: manual smoke test on each card type — open picker, save, and
confirm the channel-color preview matches the live card.
2026-05-09 16:19:20 +03:00
alexei.dolgolyov 8aa3a323d6 feat(notifications): device event notifications (snack + Web Notifications)
Surface device connection state changes (configured target online/offline)
and discovery events (new WLED on LAN, new serial port, devices that
disappear) through a configurable per-event channel matrix:
none / snack / OS / both.

- Backend: long-running mDNS browser + 10 s serial poller in
  core/devices/discovery_watcher.py, gated by user pref. Reuses the
  existing device_health_changed event for online/offline transitions.
  New GET/PUT /api/v1/preferences/notifications endpoint with Pydantic v2
  schema (channel matrix + background-discovery flag + grace/debounce).
  13 new tests, full suite still 899 passing.
- Frontend: features/notifications-watcher.ts with startup-grace +
  flap-debounce + bulk-coalesce pipeline. Web Notifications API for the
  OS channel (no platform-specific code, works in PWA shell).
  New "Notifications" tab in Settings with 4 IconSelect rows + bg toggle
  + permission row + test button. en/ru/zh translations.

Defaults: device_offline=both (urgent), online/discovered=snack, lost=none,
background discovery on. Already-configured devices are filtered from
discovery events to avoid double-notifications.
2026-04-25 17:49:20 +03:00
alexei.dolgolyov 80f01d4813 chore: harden test isolation, gitignore stale src/data, mark shutdown action done
- ``tests/test_preferences_api.py`` no longer captures the auth API
  key at module-import time. The new ``client`` fixture resolves it
  inside its body and bakes the Bearer header into ``TestClient.headers``,
  so the e2e conftest swapping the global config singleton during
  collection cannot leave the test holding a stale 401-bound header.
  Same proven pattern as ``test_audio_processing_templates_api.py``.
- ``.gitignore`` now anchors ``/server/src/data/`` defensively. If the
  server is launched from ``server/src/`` (uncommon but possible during
  ad-hoc debugging), its relative ``data/`` resolves there. Templates
  now live in SQLite (``capture_templates`` / ``pattern_templates`` /
  ``postprocessing_templates`` tables); any stale ``*.json`` that
  lands in that directory is a runtime export and must not be
  committed.
- Three such stale exports were untracked at the start of the
  pre-merge audit and have been deleted from the working tree.
- ``TODO.md`` flips the shutdown-action checklist to done and notes
  that real-hardware verification (WLED + serial after Ctrl+C) is
  still pending.
2026-04-25 15:11:39 +03:00
alexei.dolgolyov b43e1cf375 feat(ui): Lumenworks treatment for Inputs / Integrations / Graph tabs
Brings the remaining tabs in line with the Channels-tab visual language:

- .template-card now mirrors .card and .dashboard-target — channel stripe
  on the left edge with glow, silkscreened corner bracket top-right,
  hairline border on --lux-bg-1, hover lift + stripe widen-and-glow.
  Covers streams, capture / pp / cspt / pattern / audio templates and
  every Integrations card (HA / MQTT / weather / value / sync clocks /
  game integrations).

- Channel mapping extended in cards.css. Direct attribute hooks for the
  per-domain ids; section-scoped hooks via [data-card-section="…"] for
  the cards that share a generic data-id (HA / MQTT / weather / value
  → cyan, game-integrations → amber, sync-clocks → violet,
  HA-light-targets → signal). No JS changes — uses the section markup
  CardSection.render already emits.

- Graph editor nodes pick up the studio-console palette: --lux-bg-1
  fill with hairline stroke, hover bold-line, selected/running stroke
  --ch-signal with drop-shadow glow. Title font moved off Big Shoulders
  Display (which read as "stretched" at 12 px) onto --font-body
  (Manrope); subtitle keeps the mono-uppercase caption treatment with a
  conservative letter-spacing. Running gradient now rides the channel
  palette (signal → cyan → signal) rather than the legacy primary /
  success colours. Port labels and grid dots adopt --lux-line tokens.

- Graph node titles get real text-overflow:ellipsis behaviour. SVG
  <text> can't do that natively, so renderNodes runs a post-mount fit
  pass that binary-searches the longest character prefix that fits
  inside the clip rect (with 2 px slack), suffixed with "…". Trailing
  whitespace is stripped before the ellipsis so we never get "Foo …".
  Full text is stashed on data-full-text so the fit can be re-run on
  re-renders.

Also bundles two perf-charts fixes from the same session:

- Hover regression — listener was bound to .perf-charts-grid, which
  rerenderPerfGrid() replaces. Moved to document.body with a guard, and
  the cursor → sample math now uses the same sliceN as the spark
  rendering so the tooltip stays accurate when the user changes the
  window setting.

- Color picker on every perf cell. Patches / Total FPS / Devices now
  expose the same color picker as the spark cells; defaults added to
  METRIC_CSS_VARS. Each card gets an inline --perf-accent on render so
  saved colours apply immediately, including across rerenderPerfGrid.
2026-04-25 02:27:38 +03:00
alexei.dolgolyov 56853b7123 feat(dashboard): per-account customizable dashboard with slide-in panel
Open-registry section/perf-cell schema persisted server-side under
db.get_setting('dashboard_layout'); localStorage cache for instant
first-paint, server sync after auth. 5 built-in presets
(Studio/Operator/Showrunner/Diagnostics/TV); JSON export/import.

Slide-in Customize panel toggles section + perf-cell visibility,
reorders via hand-rolled HTML5 drag (with up/down buttons for
keyboard/TV-remote use), changes density per section, and exposes
global Width / Animations / Perf-mode / Window with per-cell Inherit
overrides.

Window setting now drives the actual sparkline slice (30s/1m/2m/5m at
configurable poll interval) instead of always rendering 120 fixed
samples. Perf-grid edits re-render in place — sparklines repaint from
persistent module-level history, value labels replay from cached
last-fetch payload, so there is no flicker frame and no zero-data
window between layout change and next poll. initPerfCharts now fires
an immediate fetch on init so reload no longer shows "—" until the
first interval tick.

Reset confirmation uses the project's themed showConfirm modal
instead of the browser dialog. Reserved registry keys (audio-meters,
alerts, led-preview, source-thumbs, pinned, flow) are forward-
compatible so v1.1 cards slot in without a schema bump.

Backend exposes GET/PUT/DELETE /api/v1/preferences/dashboard-layout
treating the body as opaque JSON with a numeric version gate; covered
by 6 round-trip / validation / unknown-field tests.
2026-04-25 01:43:14 +03:00
alexei.dolgolyov 539e43195f feat(ui): Lumenworks studio-console WebUI redesign
Full-app UI/UX refresh committing to a tech-instrument / studio-console
aesthetic inspired by hardware synths, Eurorack panels, and DAW layouts.

Design tokens and fonts:
- Embed Manrope (body), JetBrains Mono (labels/metrics), Big Shoulders
  Display (numeric readouts) as local .woff2 variable fonts with
  latin + latin-ext + cyrillic + cyrillic-ext subsets via unicode-range.
- New Lumenworks token layer in base.css: --lux-bg-0..3, --lux-line(-bold),
  --lux-ink(-dim/-mute/-faint), --ch-signal/-cyan/-magenta/-amber/-coral/
  -violet channel palette, --lux-signal-glow, --lux-shadow-rack, all
  theme-aware for dark + light. Existing tokens untouched for compat.

Shell (header + sidebar):
- Header rebuilt as a 3-column CSS-grid transport bar (brand | center |
  toolbar) with a glowing LED brand mark rendered via pseudo-elements on
  .header-title. Gradient channel-color rule under the bottom border.
- New sidebar.css introduces a vertical channel-strip nav. Active tab
  gets a glowing left stripe + radial tint + LED pip. .sidebar-foot
  contains a live CPU/FPS meter plate.
- Sidebar collapses to a 56 px icon rail at <=1100 px and hides via
  display:contents at <=600 px so mobile.css's fixed bottom tab-bar
  flows through unchanged.

Cards and dashboard:
- .card gets channel stripe (data-card-type + .ch-* utilities auto-map
  from data-target-id / data-stream-id / data-automation-id etc.), corner
  bracket, gradient background, subtle rack shadow.
- .card-running replaces the old @property --border-angle conic-gradient
  rotating border with a lightweight signalFlow linear-gradient strip on
  the bottom edge (cheaper paint, no GPU layer compositing per card).
- Skeleton loaders rewritten: left hairline + corner bracket + gradient
  shimmer instead of the old text-color opacity pulse.
- .dashboard-target rows pick up the same channel-stripe + signalFlow
  treatment. Section headers use mono micro-caps with a channel-green
  underline accent consistent across the app.
- .perf-chart-card: channel stripe replaces old border-top; per-metric
  accents moved to the channel palette (CPU=coral, RAM=violet, GPU=green,
  temp=amber). Metric values use tabular-nums + a soft glow.

Live bindings (no new endpoints):
- _updateSidebarMeter: binds the sidebar Load + FPS bars to the existing
  /system/performance poll.
- _updateTransportStatus: toggles the transport chip between "Ready" and
  "Armed - N live" whenever the dashboard's running-target set is
  recomputed.

Tree-nav + sub-tabs:
- tree-nav.css trigger pill gets a channel-stripe left edge that glows
  when open; panel has a gradient channel-accent rule across the top;
  group headers use silkscreened micro-caps; active leaf has a pulsing
  LED pip + channel tint.
- .stream-tab-btn / .subtab-section-header adopt the same mono-caps +
  channel-underline language for consistency.
- Graph editor toolbar gets gradient + hairline + rack shadow + backdrop
  blur. Canvas and nodes untouched.

Modals (40+ modals share modal.css):
- Radial-dim + 6 px blur backdrop. Content gets a gradient background,
  hairline border, deep rack shadow, top channel-accent rule driven by
  --modal-ch, bottom-right corner bracket (hidden on mobile fullscreen).
- Per-modal-ID channel lanes: target editors = green, source/input
  editors = cyan, audio = magenta, automation/scene/game = violet,
  settings/auth = amber, confirm = coral.
- Modal headers: vertical channel stripe left of the title + hairline
  divider. Modal footers: hairline top border + subtle gradient wash.

Forms:
- Inputs use hairline borders; number inputs switch to mono + tabular-nums
  for column alignment. Focus state: channel-green ring + soft glow.
- Buttons use mono-uppercase type with signal-glow on primary and coral-
  glow on danger.

Mobile (<=600 px):
- Fixed bottom .tab-bar gets the full Lumenworks treatment: gradient fill,
  top channel-accent rule matching the transport bar, backdrop blur.
  Active tab has an LED pip above the icon + channel tint + icon recolor.
- Fullscreen modals: corner bracket hidden, header stripe slimmed.

Microcopy (en / ru / zh):
- "Targets" -> "Channels" / "Каналы" / "通道"
- "Sources" -> "Inputs"    / "Входы"   / "输入"
- Internal tab keys (dashboard/automations/targets/streams/integrations/
  graph) kept stable so no JS or localStorage migration is needed.
- Added: sidebar.workspaces, sidebar.load, sidebar.fps,
  transport.status.ready, transport.status.armed.

Compatibility:
- All existing class hooks preserved (.tab-bar, .tab-btn, .card,
  .card-running, .tree-dd-*, .cs-*, .perf-chart-card, .modal-content,
  .dashboard-target, etc.). No JS or API changes required for the new
  look to take effect.
- Tour selectors survive (header .header-title, #tab-btn-*, onclick
  markers on theme/settings/search, #cp-wrap-accent, etc.).
- Mobile <=600 px bottom tab-bar keeps working via display:contents
  fall-through in the new sidebar.

Build: tsc --noEmit clean; npm run build clean. CSS bundle grew from
~177 KB to ~201 KB for the full new visual system. Fonts loaded lazily
per unicode-range subset (~98 KB critical path for English).

Phased plan + deferred follow-ups (dashboard hero strip, legacy-token
cleanup) recorded at the top of TODO.md.

Reference mockup: server/docs/ui-redesign-mockup.html.
2026-04-24 15:46:47 +03:00
alexei.dolgolyov b3775b2f98 feat(android): boot-time autostart, capture watchdog, versionCode from git
Boot-time startup so LedGrab has display capture and control without user
interaction on rooted TV boxes. Also folds in a batch of review findings
from the Android package audit.

Autostart
- BootReceiver fires on BOOT_COMPLETED / LOCKED_BOOT_COMPLETED /
  MY_PACKAGE_REPLACED, gated by AutostartPrefs and Root.looksRooted().
  Dispatches CaptureService.createRootIntent via
  ContextCompat.startForegroundService. Unrooted devices are a no-op
  because MediaProjection consent cannot be bypassed silently.
- AutostartPrefs: thin SharedPreferences wrapper, defaults to enabled.
  Exposed as a CheckBox on the stopped panel; greyed out when not rooted.
- Manifest: RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
  WAKE_LOCK permissions + the new BootReceiver.
- MainActivity prompts for battery-optimization exemption on first opt-in
  so Doze/App Standby doesn't kill the FG service on phones.

Service stability
- onStartCommand now flips isRunning only after startForeground succeeds
  (was stuck=true forever if the FG transition threw) and resets on
  exception. Returns START_REDELIVER_INTENT for root mode so the OS can
  restart the service with the original intent (no consent token to
  invalidate); MediaProjection mode keeps START_NOT_STICKY.
- Watchdog coroutine monitors RootScreenrecord.framesDelivered. Respawns
  the pipeline on stall (reusing the existing Python bridge — no server
  restart), caps at 3 consecutive restarts before giving up.
- RootScreenrecord.framesDelivered is now an AtomicInteger, exposed as a
  public property for the watchdog.
- ScreenCapture takes an onProjectionStopped lambda; when the user taps
  the system Cast/Screen-capture stop banner, the whole service is torn
  down instead of leaving a stale FG notification.
- MainActivity's two startForegroundService calls switch to
  ContextCompat.startForegroundService, clearing pre-existing NewApi lint
  errors (minSdk=24 < API 26 native method).

Build
- versionCode derived from git rev-list --count HEAD (or the
  ANDROID_VERSION_CODE env var for CI). Was pinned to 1 — sideload
  upgrades were silently refusing to install.
- New i18n strings (autostart_label, autostart_unavailable, version_prefix)
  in en/ru/zh; version_text now uses the resource instead of string
  concat.

TODO.md: new "Android Autostart on Boot" section tracking done/pending
items; real-hardware verification on a Magisk'd TV box is the remaining
checkbox.
2026-04-21 18:53:27 +03:00
alexei.dolgolyov d3a6416a1d refactor(devices): per-provider typed configs (phases 1-4)
Phase 1 — DeviceConfig hierarchy (device_config.py):
- 17 @dataclass(frozen=True) subclasses (WLEDConfig, AdalightConfig, …) sharing
  BaseDeviceConfig; DeviceConfig = Union[all 17]
- Device.to_config() in device_store.py: single flat→typed dispatch point

Phase 2+3 — Typed provider signatures + call-site migration:
- ProviderDeps(device_store) frozen dataclass in led_client.py
- LEDDeviceProvider.create_client(config, *, deps) abstract signature
- create_led_client(config, *, deps) factory dispatches via config.device_type
- All 17 providers narrowed to their specific config type; drop kwargs.get()
- GroupLEDClient.connect() uses device.to_config() + create_led_client()
- wled_target_processor: replaced 21-field DeviceInfo unpacking with to_config()
  + dataclasses.replace(config, use_ddp=…) for DDP override
- device_test_mode: build typed config via to_config() + ProviderDeps
- Deleted DeviceInfo dataclass, _get_device_info(), _DEVICE_FIELD_DEFAULTS
- TargetContext: replaced get_device_info callback with is_test_mode_active

Phase 4 — Test migration:
- 47-case test suite in tests/core/devices/test_device_config.py (100% coverage)
- test_group_device.py TestGroupLEDClient migrated to GroupConfig + ProviderDeps
- Removed legacy keyword-arg init path from GroupLEDClient
2026-04-18 01:24:27 +03:00
alexei.dolgolyov 928d626620 refactor(devices): route ESP-NOW client through SerialTransport
Build Android APK / build-android (push) Failing after 1m39s
Lint & Test / test (push) Successful in 4m54s
Drops the direct pyserial imports from espnow_client/espnow_provider
in favor of open_transport/list_serial_ports/port_exists. The gateway
protocol is write-only, so no read() extension was needed. ESP-NOW
gateways are now reachable via usb:VID:PID URLs on Android.
2026-04-14 19:15:08 +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 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 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 492bdb95e3 feat: game integration system
Receive real-time events from games (CS2, Dota 2, LoL, etc.) and drive
LED effects through the existing color strip and value source pipelines.

Core:
- GameEventBus (thread-safe pub/sub) with standardized 23-type event vocabulary
- GameAdapter ABC + AdapterRegistry + MappingAdapter (YAML-driven)
- Built-in adapters: CS2 GSI, Dota 2 GSI, LoL Live Client, Generic Webhook
- Community YAML adapters: Minecraft, Valorant, Rocket League
- GameEventColorStripStream with 5 effects (flash/pulse/sweep/color_shift/breathing)
- GameEventValueSource with EMA smoothing and timeout
- 4 built-in effect presets (FPS Combat, MOBA Health, Racing, Generic Alert)
- Auto-setup for Valve GSI games (Steam path detection, cfg file writing)
- Demo capture engine exposed to non-demo mode

Frontend:
- Game tab in Streams tree navigation with integration cards
- Game integration editor modal with adapter picker, config fields, event mappings
- game_event source type in CSS and ValueSource editors
- Setup instructions overlay (markdown rendered)
- Live event monitor and connection test

API:
- Full CRUD for game integrations
- Event ingestion endpoint (adapter-level auth)
- Adapter metadata, presets, auto-setup, status/diagnostics endpoints
2026-03-31 13:17:52 +03:00
alexei.dolgolyov 384362ccf1 feat: new value source types (HA entity, gradient map, strip extract) + UI fixes
Lint & Test / test (push) Successful in 1m27s
New value source types:
- ha_entity: reads numeric values from HA entity state/attribute, normalizes
  via min/max range, applies EMA smoothing. EntitySelect for HA connection
  and entity selection with live entity list fetching.
- gradient_map: maps a float value source (0-1) through a gradient entity.
  EntitySelect for both input source and gradient with inline previews.
- css_extract: extracts single color by averaging LED range from a color
  strip source. EntitySelect for source selection.

Value source type picker:
- Filter tabs (All / Numeric / Color) above the icon grid
- showTypePicker extended with filterTabs + onFilterChange support

Palette selectors converted to EntitySelect:
- Effect palette, gradient preset, and audio palette selectors now use
  command-palette style EntitySelect with gradient strip previews

Tab indicator fixes:
- Icon now updates on tab switch (was passing no args to updateTabIndicator)
- Visible with any background effect active, not just Noise Field
- Noise Field is the default background effect for new users

Dashboard section collapse fix:
- Split header into clickable toggle (chevron+label) and non-clickable
  actions area — buttons no longer trigger collapse/expand

Discriminated union fix (422 errors):
- source_type/target_type now always included in update payloads for:
  CSS editor, LED target, HA light target, simple calibration,
  advanced calibration
2026-03-29 20:38:22 +03:00
alexei.dolgolyov 8a17bb5caa feat: BindableFloat — universal value source binding for all scalar properties
Lint & Test / test (push) Successful in 1m20s
Introduce BindableFloat abstraction that allows any numeric property to be
either a static value or dynamically driven by a ValueSource. Backward-compatible
serialization: plain float when unbound, {value, source_id} dict when bound.

Backend:
- storage/bindable.py — BindableFloat dataclass + bfloat() helper
- 25+ scalar properties converted across all entity types
- Runtime VS acquisition in ColorStripStreamManager for CSS bindings
- All stream hot loops use self.resolve() for live values
- KeyColorsColorStripStream now inherits ColorStripStream

Frontend:
- BindableScalarWidget (slider + VS picker toggle) for all editors
- TypeScript BindableFloat type + helpers
- Graph editor edges for all bindable properties
- Audio source channel IconSelect grid

Fixes: daylight longitude, candlelight wind_strength/candle_type from_dict
2026-03-29 00:33:24 +03:00
alexei.dolgolyov e2e1107df7 feat: asset-based image/video sources, notification sounds, UI improvements
Lint & Test / test (push) Has been cancelled
- Replace URL-based image_source/url fields with image_asset_id/video_asset_id
  on StaticImagePictureSource and VideoCaptureSource (clean break, no migration)
- Resolve asset IDs to file paths at runtime via AssetStore.get_file_path()
- Add EntitySelect asset pickers for image/video in stream editor modal
- Add notification sound configuration (global sound + per-app overrides)
- Unify per-app color and sound overrides into single "Per-App Overrides" section
- Persist notification history between server restarts
- Add asset management system (upload, edit, delete, soft-delete)
- Replace emoji buttons with SVG icons throughout UI
- Various backend improvements: SQLite stores, auth, backup, MQTT, webhooks
2026-03-26 20:40:25 +03:00
alexei.dolgolyov 7939322a7f feat: reduce build size — replace Pillow with cv2, refactor build scripts
Build Release / create-release (push) Successful in 1s
Build Release / build-docker (push) Successful in 42s
Lint & Test / test (push) Successful in 2m50s
Build Release / build-windows (push) Successful in 3m27s
Build Release / build-linux (push) Successful in 1m59s
- Create utils/image_codec.py with cv2-based image helpers
- Replace PIL usage across all routes, filters, and engines with cv2
- Move Pillow from core deps to [tray] optional in pyproject.toml
- Extract shared build logic into build-common.sh (detect_version, cleanup, etc.)
- Strip unused NumPy/PIL/zeroconf/debug files in build scripts
2026-03-25 14:18:16 +03:00
alexei.dolgolyov 382a42755d feat: add auto-update system with release checking, notification UI, and install-type-aware apply
- Abstract ReleaseProvider interface (Gitea impl, swappable for GitHub/GitLab)
- Background UpdateService with periodic checks, debounce, dismissed version persistence
- Install type detection (installer/portable/docker/dev) with platform-aware asset matching
- Download with progress events, silent NSIS reinstall, portable ZIP/tarball swap scripts
- Version badge pulse animation, dismissible banner with icon buttons, Settings > Updates tab
- Single source of truth: pyproject.toml version via importlib.metadata, CI stamps tag with sed
- API: GET/POST status, check, dismiss, apply, GET/PUT settings
- i18n: en, ru, zh (27+ keys each)
2026-03-25 13:16:18 +03:00
alexei.dolgolyov 9dfd2365f4 feat: migrate storage from JSON files to SQLite
Lint & Test / test (push) Failing after 28s
Replace 22 individual JSON store files with a single SQLite database
(data/ledgrab.db). All entity stores now use BaseSqliteStore backed by
SQLite with WAL mode, write-through caching, and thread-safe access.

- Add Database class with SQLite backup/restore API
- Add BaseSqliteStore as drop-in replacement for BaseJsonStore
- Convert all 16 entity stores to SQLite
- Move global settings (MQTT, external URL, auto-backup) to SQLite
  settings table
- Replace JSON backup/restore with SQLite snapshot backups (.db files)
- Remove partial export/import feature (backend + frontend)
- Update demo seed to write directly to SQLite
- Add "Backup Now" button to settings UI
- Remove StorageConfig file path fields (single database_file remains)
2026-03-25 00:03:19 +03:00
alexei.dolgolyov ef33935188 feat: add weather source entity and weather-reactive CSS source type
Lint & Test / test (push) Failing after 34s
New standalone WeatherSource entity with pluggable provider architecture
(Open-Meteo v1, free, no API key). Full CRUD, test endpoint, browser
geolocation, IconSelect provider picker, CardSection with test/clone/edit.

WeatherColorStripStream maps WMO weather codes to ambient color palettes
with temperature hue shifting and thunderstorm flash effects. Ref-counted
WeatherManager polls API and caches data per source.

CSS editor integration: weather type with EntitySelect source picker,
speed and temperature influence sliders. Backup/restore support.

i18n for en/ru/zh.
2026-03-24 18:52:46 +03:00
alexei.dolgolyov 2240471b67 Add demo mode: virtual hardware sandbox for testing without real devices
Demo mode provides a complete sandbox environment with:
- Virtual capture engine (radial rainbow test pattern on 3 displays)
- Virtual audio engine (synthetic music-like audio on 2 devices)
- Virtual LED device provider (strip/60, matrix/256, ring/24 LEDs)
- Isolated data directory (data/demo/) with auto-seeded sample entities
- Dedicated config (config/demo_config.yaml) with pre-configured API key
- Frontend indicator (DEMO badge + dismissible banner)
- Engine filtering (only demo engines visible in demo mode)
- Separate entry point: python -m wled_controller.demo (port 8081)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:17:14 +03:00
alexei.dolgolyov 304fa24389 Comprehensive WebUI review: 41 UX/feature/CSS improvements
Safety & Correctness:
- Add confirmation dialogs to Stop All, turnOffDevice
- i18n confirm dialog (title, yes, no buttons)
- Fix duplicate tutorial-overlay ID
- Define missing CSS variables (--radius, --text-primary, --hover-bg, --input-bg)
- Fix toast z-index conflict with confirm dialog (2500 → 3000)

UX Consistency:
- Add backdrop-close to test modals
- Add device clone feature (only entity without it)
- Add sync clocks to command palette
- Replace 20+ hardcoded accent colors with CSS vars/color-mix()
- Remove dead .badge duplicate from components.css
- Make calibration elements keyboard-accessible (div → button)
- Add aria-labels to color picker swatches
- Fix pattern canvas mobile horizontal scroll
- Fix graph editor mobile bottom clipping

Polish:
- Add empty-state messages to all CardSection instances
- Convert 21 px font-sizes to rem
- Add scroll-behavior: smooth with reduced-motion override
- Add @media print styles
- Add :focus-visible to 4 missing interactive elements
- Fix settings modal close label (Cancel → Close)
- Fix api-key submit button i18n

New Features:
- Command palette actions: start/stop targets, activate scenes, enable/disable
- Bulk start/stop API endpoints (POST /output-targets/bulk/start|stop)
- OS notification history viewer modal
- Scene "used by" automation reference count on cards
- Clock elapsed time display on Streams tab cards
- Device "last seen" relative timestamp on cards
- Audio device refresh button in edit modal
- Composite layer drag-to-reorder
- MQTT settings panel (broker config with JSON persistence)
- WebSocket log viewer with level filtering and ring buffer
- Runtime log-level adjustment (GET/PUT endpoints + settings UI)
- Animated value source waveform canvas preview
- Gradient custom preset save/delete (localStorage)
- API key read-only display in settings
- Backup metadata (file size, auto/manual badges)
- Server restart button with confirm + overlay
- Partial config export/import per entity type
- Progressive disclosure in target editor (Advanced section)

CSS Architecture:
- Define radius scale tokens (--radius-sm/md/lg/pill)
- Scope .cs-filter selectors to remove 7 !important overrides
- Consolidate duplicate toggle switch (filter-list → settings-toggle)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:46:38 +03:00
alexei.dolgolyov 294d704eb0 Add CSPT entity, processed CSS source type, reverse filter, and UI improvements
- Add Color Strip Processing Template (CSPT) entity: reusable filter chains
  for 1D LED strip postprocessing (backend, storage, API, frontend CRUD)
- Add "processed" color strip source type that wraps another CSS source and
  applies a CSPT filter chain (dataclass, stream, schema, modal, cards)
- Add Reverse filter for strip LED order reversal
- Add CSPT and processed CSS nodes/edges to visual graph editor
- Add CSPT test preview WS endpoint with input source selection
- Add device settings CSPT template selector (add + edit modals with hints)
- Use icon grids for palette quantization preset selector in filter lists
- Use EntitySelect for template references and test modal source selectors
- Fix filters.css_filter_template.desc missing localization
- Fix icon grid cell height inequality (grid-auto-rows: 1fr)
- Rename "Processed" subtab to "Processing Templates"
- Localize all new strings (en/ru/zh)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 02:16:59 +03:00
alexei.dolgolyov f6f428515a Update TODO.md and add task tracking rule to CLAUDE.md
Remove completed/deferred items from TODO.md and add instruction
to use TODO.md as the primary task tracker instead of TodoWrite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:40:16 +03:00
alexei.dolgolyov 304c4703b9 Add manual backup trigger endpoint
POST /api/v1/system/auto-backup/trigger creates a backup on demand
and returns the created file info (filename, size, timestamp).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 14:27:44 +03:00
alexei.dolgolyov b0a769b781 Move tags input under name field in all entity editor modals
Remove the separate tags form-group (label, hint toggle, hint text)
from all 14 editor modals and place the tags container directly
below the name input for a cleaner, more compact layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:15:46 +03:00
alexei.dolgolyov ee40d99067 Add Daylight Cycle value source type
New value source that outputs brightness (0-1) based on the daylight
color LUT, computing BT.601 luminance from the simulated sky color.
Supports real-time wall-clock mode or configurable simulation speed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:27:36 +03:00
alexei.dolgolyov 1ce25caa35 Clean up TODO.md: remove completed items, add new P1 tasks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:07:44 +03:00
alexei.dolgolyov 954e37c2ca Add auto-restart for crashed processing loops, remove sync clock badge
- Auto-restart: ProcessorManager detects fatal task crashes via done
  callback and restarts with exponential backoff (2s-30s, max 5 attempts
  in 5 min window). Manual stop disables auto-restart. Restart state
  exposed in target state API and via WebSocket events.
- Remove "Running"/"Paused" badge label from sync clock dashboard cards
  (pause/play button already conveys state).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:53:04 +03:00
alexei.dolgolyov 30fa107ef7 Add tags to all entity types with chip-based input and autocomplete
- Add `tags: List[str]` field to all 13 entity types (devices, output targets,
  CSS sources, picture sources, audio sources, value sources, sync clocks,
  automations, scene presets, capture/audio/PP/pattern templates)
- Update all stores, schemas, and route handlers for tag CRUD
- Add GET /api/v1/tags endpoint aggregating unique tags across all stores
- Create TagInput component with chip display, autocomplete dropdown,
  keyboard navigation, and API-backed suggestions
- Display tag chips on all entity cards (searchable via existing text filter)
- Add tag input to all 14 editor modals with dirty check support
- Add CSS styles and i18n keys (en/ru/zh) for tag UI
- Also includes code review fixes: thread safety, perf, store dedup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:20:19 +03:00
alexei.dolgolyov 353a1c2d85 Rename picture-targets to output-targets across entire codebase
Rename all Python modules, classes, API endpoints, config keys, frontend
fetch URLs, and Home Assistant integration URLs from picture-targets to
output-targets. Store loads both new and legacy JSON keys for backward
compatibility with existing data files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:55:36 +03:00