Commit Graph

749 Commits

Author SHA1 Message Date
alexei.dolgolyov e584235676 chore(activity-log): post-merge cleanup + graduate context to CLAUDE.md
- remove plans/activity-log/ (feature merged; learnings in CLAUDE.md + git history)
- server/CLAUDE.md: Activity/Audit Log architecture + extension points (recorder, fire_entity_event hook, sanitize_display, events allowlist, retention, API auth posture)
2026-06-10 18:42:15 +03:00
alexei.dolgolyov b43f821046 Merge feature/activity-log: persistent activity/audit log
Adds a persistent, queryable activity/audit log surfaced in the WebUI:

- storage: dedicated indexed activity_log table + repository (keyset pagination, prune, streaming export); migration 002

- recorder + actor ContextVar + retention engine + lifecycle wiring; activity_logged realtime event

- instrumentation across auth / device / entity-CRUD / capture / system (best-effort, no secrets logged)

- REST API: list/filter, CSV/JSON export, retention settings, clear (auth-gated)

- WebUI: Activity tab (smart filtering, live updates, localized descriptions, export), Dashboard widget, Settings retention panel; en/ru/zh

Reviewed via independent per-phase + final + security agents; full suite 2563 passed.

Includes 17dd2e0 (review-findings fixes) that the feature branch was based on.
2026-06-10 15:31:12 +03:00
alexei.dolgolyov 077c99c7d1 fix(activity-log): no spinner flash on instant filtering
- re-query keeps current rows visible instead of clearing + showing the full 'Loading' spinner
- loading affordance is delayed ~180ms: instant responses show nothing; slow ones get a subtle dim (aria-busy)
- full spinner reserved for the genuine first load; append (load-more) shows no indicator
2026-06-10 15:30:48 +03:00
alexei.dolgolyov ae74cca132 fix(activity-log): UI polish - accessible export menu, i18n placeholders, zero-result spinner fix
- export button is now a click-toggle menu (aria-haspopup/expanded, role=menu, caret, Escape + click-outside close)
- filter placeholders moved to i18n (actor/entity_type .placeholder keys, en/ru/zh)
- _fetchPage clears _loading before render so a zero-result page doesn't spin forever
- toolbar/entry use elevated card surface (--lux-bg-1); light-theme device badge contrast; mobile message grid
2026-06-10 15:24:45 +03:00
alexei.dolgolyov 77284e8e7b fix(activity-log): dashboard section reconciliation + activity column alignment
- dashboard full re-render now reconciles sections (only replaces changed ones) instead of wholesale .dashboard-dynamic innerHTML swap -> editing an entity no longer jumps the whole dashboard
- Recent Activity widget live DOM + perf strip preserved across re-renders; widget skips re-fetch when already populated (no flash)
- sweep stray non-section nodes so empty->populated doesn't leave an orphan 'no targets' banner (review-caught regression)
- Activity list rows use a CSS grid (fixed badge/actor columns) so message column aligns consistently across rows
2026-06-10 12:28:13 +03:00
alexei.dolgolyov ff1ff06cb5 fix(activity-log): post-test polish - localize descriptions, dashboard widget, ticking time
- localize entry descriptions client-side via localizeMessage (activity_log.msg.* + entity_type.* templates x3 locales); server message kept as fallback/export/search
- remove redundant Activity header banner from tab
- Recent Activity widget is now a first-class dashboard section (Customize Dashboard show/hide/reorder; pre-existing layouts preserved)
- live activity event updates the widget surgically (no full dashboard rebuild); single listener with teardown
- relative-time labels tick via shared ensureRelativeTimeTicker (single 30s interval, visibility-aware)
2026-06-10 12:03:18 +03:00
alexei.dolgolyov 3dd1ac3f0d fix(activity-log): final-review fixes - crosslink keys + sanitize parity
- _ENTITY_NAV map keys corrected to match backend entity_type (device, color_strip_source, audio_source data-id) + scene_playlist crosslink added
- sanitize_display applied uniformly to owner-authored names at remaining record sites (dependencies entity_name, device_health, automation_engine, output_targets_control, scene_presets, scene_playlists)
2026-06-09 21:23:22 +03:00
alexei.dolgolyov 6e1dd2111d feat(activity-log): phase 6 - dashboard widget + settings panel + docs
- Dashboard 'Recent Activity' widget: latest 5 entries, live prepend, 'View all' -> Activity tab
- Settings 'Activity Log' panel: retention (enabled/max_days/max_entries) GET/PUT, clear (confirm + auth-required toast), CSV/JSON export
- audit-log vs ephemeral debug Log Viewer distinction note + cross-links
- public helpers fetchRecentEntries/renderCompactEntry on activity-log.ts (reused, no dup markup)
- README Activity Log section; i18n across en/ru/zh
- review fixes: clear 401 surfaces toast; empty widget transitions on first live event
2026-06-09 21:05:40 +03:00
alexei.dolgolyov 9a0137fa4c feat(activity-log): phase 5 - Activity tab (smart filtering, live updates, export)
- new top-level Activity tab: filter toolbar (category/severity chips, presets, debounced search, actor/entity/date), keyset load-more, expandable detail
- live prepend via server:activity_logged; authed CSV/JSON blob export
- formatTimestamp/formatRelativeTime in core/ui.ts; history+severity SVG icons; Ctrl+7 shortcut
- i18n activity_log.* across en/ru/zh; getting-started tutorial step; activity-log.css (themed)
- review fixes: newest-first ordering, attribute-context XSS hardening (_escapeAttr + event delegation)
2026-06-09 20:42:44 +03:00
alexei.dolgolyov 4a0927521a feat(activity-log): phase 4 - REST API (list/export/settings/clear)
- GET /activity-log: filtered, keyset-paginated list (categories/severities/actor/entity/date/q)
- GET /activity-log/export: streaming CSV/JSON, chunked keyset (releases DB lock per batch), CSV formula-injection guard
- GET/PUT /activity-log/settings: retention config (PUT require_authenticated)
- DELETE /activity-log: clear (require_authenticated, self-audited)
- security: export DoS fix, settings-PUT auth gate, CSV \t/\r guard, metadata-as-JSON
- 122 API tests (auth posture, CSV injection, pagination integrity, filters, settings bounds, clear-audited)
2026-06-09 20:09:46 +03:00
alexei.dolgolyov 25c613c5cb feat(activity-log): phase 3 - event instrumentation (4 categories)
- entity CRUD via fire_entity_event choke point (name resolved/sanitized; deletes pass name explicitly)
- auth: failures + WS session establishment (no tokens logged); per-IP audit-record throttle
- device: online/offline (health), discovered/lost (zeroconf), ADB connect/disconnect
- capture/system: target start-stop, scenes, playlists, automations, backup/restore, update, restart, calibration, settings
- security hardening: sanitize_display strips control/NUL/ANSI/newlines from untrusted strings; malformed-IPv6 origin guard
- 129 instrumentation tests (incl. secret-leak, log-injection, throttle, best-effort) + autouse throttle-reset fixture
2026-06-09 19:20:57 +03:00
alexei.dolgolyov 726f39e2ba feat(activity-log): phase 2 - recorder, actor context, retention, lifecycle
- ActivityRecorder: thread-safe record() (inline on loop, call_soon_threadsafe off-loop), best-effort, fires activity_logged event
- current_actor ContextVar set in verify_api_key (both branches), default system
- ActivityLogRetentionEngine: prune loop (max_days+max_entries), settings persistence, rehydrates recorder.enabled on startup
- lifespan wiring: server.shutting_down recorded first on shutdown, retention stop before db.close
- events-ws.ts allowlist + parity; DI getters + module accessor; 62 new tests
2026-06-09 18:10:27 +03:00
alexei.dolgolyov 1ac4a0f66d feat(activity-log): phase 1 - storage model, migration, repository
- ActivityLogEntry dataclass + ActivityCategory/ActivitySeverity + ActivityLogFilters
- additive idempotent migration 002_add_activity_log (indexed activity_log table, seq keyset tiebreaker)
- ActivityLogRepository (record/query/count/prune/clear/iter_export), keyset pagination, parameterized SQL
- 102 unit + adversarial tests (SQL-injection, pagination, prune, codec, migration idempotency)
2026-06-09 17:40:37 +03:00
alexei.dolgolyov 1afe7d6fcc chore(activity-log): scaffold feature plan and phase subplans 2026-06-09 17:14:50 +03:00
alexei.dolgolyov 17dd2e02ba fix: resolve comprehensive review findings (security, concurrency, perf, Android, UI)
Multi-dimension review of v0.8.2. Excludes the deliberately deferred
default_config.yaml weak-default-key item (C1).

Backend:
- calibration: create_default_calibration no longer exceeds led_count for small
  odd counts (bounded trim + regression test)
- game-integration: generic webhook now requires auth_token; constant-time
  compare_digest in all adapters; per-IP failed-auth rate limit on the ingest
  route; auth_token encrypted at rest via secret_box (migration-safe)
- playlist engine: serialize _state/_task under the lifecycle lock to close a
  delete-mid-play race (+ concurrency tests)
- main: stop the calibration session on shutdown (restore prior target)
- home_assistant: validate HA host via the LAN classifier on create/update
- perf: drop slow preview-WS clients instead of blocking the send loop; cache
  composite full-strip resize linspaces; effect_stream lava reuses scratch

Frontend:
- setup/auto-calibration wizard: guard _state after awaits (cancel-safe), await
  session teardown before output start, busy-gate skip-calibration, manual
  display input keeps focus, move focus on step change
- calibration: destroy EntitySelect on modal close
- color-strips test: dirty-flag-gated render + cached ctx/ImageData
- a11y/TV: focus-visible for new wizard/auto-cal/corner controls, aria-labels on
  the spatial corner/edge picker; theme-aware syntax tokens; dead/undefined CSS
  tokens removed; .modal-error styled; i18n titles (en/ru/zh)

Android:
- ApiKeyManager: EncryptedSharedPreferences with verified, data-safe legacy
  migration that never rotates an existing key
- CaptureService: validate MediaProjection token before promoting; satisfy the
  startForeground 5s contract on the bail path
- NotificationListener: connection-scoped executor with lazy fallback
- BLE: request BLUETOOTH_SCAN/CONNECT at runtime + guard handler-thread
  SecurityExceptions
- Root: cancellation-aware su grant probe

Adds 14 tests. Gate: ruff + tsc 5.9.3 + esbuild + pytest (2185 passed) +
compileDebugKotlin all green.
2026-06-09 16:35:08 +03:00
alexei.dolgolyov 7a12f39f49 chore: release v0.8.2
Build Release / create-release (push) Successful in 3s
Build Android APK / build-android (push) Failing after 11s
Build Release / build-docker (push) Successful in 3m14s
Build Release / build-linux (push) Successful in 4m3s
Build Release / build-windows (push) Successful in 4m25s
Build Release / publish-release (push) Successful in 1s
v0.8.2
2026-06-08 20:18:28 +03:00
alexei.dolgolyov dd43f3836d fix(calibration-wizard): all-provider discovery + spatial corner picker
- Setup wizard discovery now scans every device provider, not just WLED:
  drop the hardcoded device_type=wled filter so the backend's parallel
  all-provider scan runs (Adalight, DDP, OpenRGB, BLE, DMX, etc). The list
  UI was already multi-type aware (per-type icon + badge).
- Auto-calibration corner picker: finish the unfinished step. The empty
  .autocal-corner-glyph + dead --top-left/etc modifier classes now render a
  mini-screen frame with the matching corner lit, so users map the physical
  lit LED to a button at a glance. Hover/focus lights the corner dot.
- Remove dead empty CSS rule for #wizard-rerun-btn.
2026-06-08 17:59:56 +03:00
alexei.dolgolyov d32961085d Merge feature/edge-calibration-wizard: auto edge-calibration + first-run wizard
Auto edge-calibration via on-screen chase (#4) and a guided first-run setup
wizard (#3); spatial model (#11) intentionally excluded. Also adds scene
playlists + cycling state to the /api/v1/snapshot poll.

- calibration solver + chase session (lock, idle-timeout, stop/restore) + /api/v1/calibration/* (phase 1)
- POST /api/v1/setup/scaffold (rollback, registers target with manager) + onboarding flag (phase 2)
- reusable browser-driven auto-calibration flow + calibration-modal entry (phase 3)
- guided first-run wizard with first-run trigger + tour suppression (phase 4)
- snapshot endpoint returns scene_playlists + playlist_state

Full suite 2149 passed / 2 skipped; tsc clean; build passes; ruff clean.
2026-06-08 17:00:41 +03:00
alexei.dolgolyov 6cd5e057da fix(setup): register scaffolded target with ProcessorManager + final-review hardening
Final-review blocker: the setup scaffold created the LED output target in the
store but never registered it with the ProcessorManager, so the wizard's
"Start" step 404'd on a fresh setup (target not found) — the lights never
started despite a success screen. Now the scaffold calls
target.register_with_manager(manager) right after create (mirroring the
canonical POST /output-targets route, same ValueError guard), so
start_processing finds the target. Rollback unregisters via
manager.remove_target before deleting the store entity, so a post-registration
failure leaves no half-registered target.

Also from the final review:
- solve corner_indices elements now bounded ge=0 (clear 422 instead of silent
  modulo-wrap).
- setup-wizard.ts: reuse tutorials' suppressGettingStartedTour()/TOUR_KEY
  instead of a duplicated 'tour_completed' literal; drop a duplicate manual-form
  submit listener.

Tests: + adversarial pass over the whole feature (solver/session/scaffold edge
cases) and a scaffold->register->startable regression test. Full suite
2149 passed / 2 skipped; tsc clean; build passes; ruff clean.
2026-06-08 16:55:36 +03:00
alexei.dolgolyov 81b18089e1 feat(onboarding): guided first-run setup wizard (phase 4, final)
A multi-step first-run wizard that takes a brand-new user from install to a
running, calibrated ambient light in ~2 minutes, orchestrating the existing
primitives (no node graph required).

- features/setup-wizard.ts + modals/setup-wizard.html: welcome -> find device
  (discovery list + manual add via the canonical, URL-validated POST /devices)
  -> pick screen (GET /config/displays) -> scaffold (POST /setup/scaffold) ->
  calibrate (embeds the phase-3 auto-calibration flow via mountAutoCalibration
  on the scaffolded CSS + device) -> start -> done, with a progress indicator.
- First-run trigger in app.ts (checkAndOpenWizardIfNeeded): on load, if the
  onboarding flag is unset AND no output targets exist, the wizard takes over
  and the tooltip tour is suppressed; on finish/skip it PUTs the onboarding
  flag and sets localStorage tour_completed so neither re-fires. Re-runnable.
- tutorials.ts exposes TOUR_KEY + a takeover hook so the getting-started tour
  and the wizard never double-fire.
- Calibrate step always calls unmountAutoCalibration() on exit so the device
  is restored. i18n in en/ru/zh (wizard.* keys + common.back).

Final phase of the edge-calibration + first-run-wizard feature. Big Bang final
gate green: tsc --noEmit clean, npm run build passes, full pytest suite
2064 passed / 2 skipped, ruff clean.
2026-06-08 16:27:55 +03:00
alexei.dolgolyov abc204c04e feat(snapshot): include scene playlists + cycling state in snapshot
The aggregated /api/v1/snapshot poll now emits a `scene_playlists` section
(each playlist with its `is_running` flag) plus a companion `playlist_state`
key carrying the single global cycling state (running playlist, current index/
preset, dwell) — so the HA-coordinator and other low-overhead pollers get
playlist state in the same round trip as scenes/targets, matching the other
entity sections. Gated by the `scene_playlists` include-section like the rest.

Reuses the existing list_scene_playlists handler; snapshot route tests updated.
2026-06-08 16:22:47 +03:00
alexei.dolgolyov 9550688c1e feat(calibration): browser-driven auto edge-calibration UI (phase 3)
Reusable, chase-driven calibration flow that solves + saves the linear
CalibrationConfig with a few taps — no LED counting — and works in the
browser on desktop and Android (no Tkinter dependency).

- features/auto-calibration.ts: 5-step flow (start corner -> direction ->
  tap-to-mark-corners -> solved preview -> save). Drives the phase-1 session
  endpoints (session/position/solve) and persists via PUT /color-strip-
  sources/{id}. cornerIndices[0] is anchored to strip index 0 per the solver
  contract. unmountAutoCalibration() is the single cleanup gate — the
  calibration session is always stopped (device restored) on cancel, modal
  close, after save, AND on a mid-flow error, so the strip is never left dark
  or stuck.
- Public API mountAutoCalibration({container, cssId, deviceId, onComplete,
  onCancel}) for the phase-4 wizard to embed; showAutoCalibration() standalone.
- "Auto-calibrate" entry added to the existing calibration modal; standalone
  modal template; app.ts/global.d.ts exports; .autocal-* CSS matching the
  ds-section vocabulary; 43 autocal.* i18n keys in en/ru/zh; docs/CALIBRATION.md.

Part of the edge-calibration + first-run-wizard feature (Big Bang; intermediate
phase — full build/suite + test-writer gated at the final phase).
2026-06-08 15:52:45 +03:00
alexei.dolgolyov 9dcd76d264 feat(setup): one-call setup scaffold + onboarding flag (phase 2)
Backend for the first-run wizard (phase 4).

- POST /api/v1/setup/scaffold: given an existing device_id + display_index
  (+ optional calibration), wires a working chain via the real validated
  store create paths — create-or-reuse capture template -> raw picture
  source -> picture color-strip source (calibration or default) -> LED
  output target -> returns the ids. Does NOT auto-start. Rolls back every
  entity it created (reverse order) on any partial failure, leaving no
  orphans; "created" events are deferred until the whole chain succeeds so
  a rolled-back scaffold never leaves ghost cards in the UI.
- Requires an existing device_id (no inline device creation) — the wizard
  creates the device first via the canonical, URL-validated POST /devices,
  so the scaffold can't bypass device validation. display_index is bounded.
- GET/PUT /api/v1/preferences/onboarding: persistent first-run flag
  ({onboarded, completed_at}) via db.set_setting; server stamps completed_at.
- Both routes AuthRequired. Tests: 25 (scaffold happy/reuse/rollback/
  validation + onboarding + calibration round-trip integration). docs/API.md.

Part of the edge-calibration + first-run-wizard feature (Big Bang; intermediate
phase — full build/suite gated at the final phase).
2026-06-08 15:22:04 +03:00
alexei.dolgolyov 0409cd8b66 feat(calibration): auto edge-calibration backend core (phase 1)
Backend engine for guided LED-chase calibration, driven by the upcoming
auto-calibration UI (phase 3) and first-run wizard (phase 4).

- solve_calibration(): pure function mapping start corner + direction + 4
  corner-tap indices to per-edge LED counts, consistent with EDGE_ORDER/
  EDGE_REVERSE so it round-trips through build_segments().
- CalibrationChaseMixin.set_calibration_pixel(): light a specific LED index
  (+ optional window) on a device, reusing the device_test_mode idle-client
  send path.
- CalibrationSession: single-active session with start/position/stop/cancel,
  a 60s idle-timeout watchdog, and a concurrency lock so interleaved calls
  can't corrupt the stop/restore bookkeeping — start() stops + remembers any
  running target on the device and stop/cancel/timeout always restore it
  (never leaves the device dark or stuck in chase).
- Routes /api/v1/calibration/{session,session/position,session/stop,
  session/cancel,session/state,solve} (all AuthRequired, bounds-validated);
  calibration is persisted by reusing the existing PUT /color-strip-sources/
  {id} (hot-reloads running streams) rather than a duplicate endpoint.
- Tests: 19 solver pure-logic + 19 route/bounds. docs/API.md updated.

Part of the edge-calibration + first-run-wizard feature (Big Bang; intermediate
phase — full build/suite gated at the final phase).
2026-06-08 14:59:58 +03:00
alexei.dolgolyov 6180569b10 wip(dashboard): in-progress dashboard customization changes
Snapshot of uncommitted dashboard-customization work (dashboard, customize,
layout, component styles, config defaults, and en/ru/zh locales) committed
as-is to clear the working tree before branching the edge-calibration +
first-run-wizard feature. Not independently verified to build.
2026-06-08 14:33:33 +03:00
alexei.dolgolyov f71e10ee06 feat(scenes): scene playlists with timed auto-cycling
Add ordered, timed sequences of scene presets that auto-cycle — activating
each preset and holding it for its dwell duration before advancing.

Backend:
- ScenePlaylist / PlaylistItem models + SQLite store (new scene_playlists table)
- PlaylistEngine: cycles ONE playlist at a time (starting one stops any other),
  loop/shuffle, re-reads the playlist each cycle so edits/deletes apply at the
  boundary, skips missing presets, guards against busy-loops; reuses the shared
  apply_scene_state path used by scene presets and automations
- REST API: CRUD + /start, /stop, /state with scene-preset reference validation
- Constructed in the app lifespan with a bounded stop on shutdown

Frontend:
- New "Playlists" sub-tab in the Automations tab with start/stop controls and a
  running indicator; editor modal with ordered scene rows (reorder + per-item
  duration), loop/shuffle toggles, and tags
- Live refresh via the playlist_state_changed WebSocket event
- i18n in en/ru/zh

Tests: new unit + API coverage for the store/model, engine (cycling,
single-active exclusivity, missing-preset skip, shuffle, and the
playlist_state_changed event contract), and routes. Full suite green;
ruff and tsc clean.
2026-06-08 13:48:43 +03:00
alexei.dolgolyov ca59546711 feat(capture): region-of-interest (ROI) crop for screen sampling
Sample only a sub-rectangle of the captured frame instead of the whole display,
so a taskbar, game HUD, or letterbox bars don't pollute the border colours — the
first functional gap a reviewer hits (capture was full-display only).

- New pure crop_screen_capture() returns a numpy view (no copy), fast-paths the
  full-frame case, and clamps degenerate/out-of-range ROIs to >=1px.
- ROI lives on CalibrationConfig (simple mode) as fractions 0..1 with a has_roi
  helper; applied in the picture color-strip stream just before border
  extraction, clamping border_width to the cropped size. Additive + backward
  compatible (full-frame default, omitted from serialization when unset -> no
  migration).
- Round-trips through the calibration schema automatically; frontend adds an
  X/Y/Width/Height (%) 'Capture region' group to the calibration editor with
  i18n (en/ru/zh).

10 unit tests (crop geometry, view-not-copy, clamping, ROI round-trip, legacy
default); full suite green (1946 passed).
2026-06-05 11:58:26 +03:00
alexei.dolgolyov 4a82595f26 Merge feat/roadmap-quick-wins: WLED realtime UDP, look presets, weekday/timezone scheduling 2026-06-05 11:44:38 +03:00
alexei.dolgolyov 1ada5ac334 feat(automations): weekday + timezone scheduling for time-of-day rule
Extend the time-of-day condition from a bare server-local HH:MM window to a real
schedule: pick which weekdays it is active (0=Mon..6=Sun, empty = every day) and
an optional IANA timezone (empty = server local). Closes the parity gap where
even a $5 WLED chip has weekday timers.

- Overnight windows (start > end) count toward the day they START on, so the
  after-midnight tail is matched against the previous weekday.
- Timezones are resolved via zoneinfo, cached, and fall back to server-local
  with a one-time warning on an invalid name (the ~1Hz tick never log-spams).
- Backward compatible: new fields default to all-days / server-local, so
  existing automations are unchanged (no migration).
- Frontend: weekday chips + timezone input on the rule editor, day/timezone in
  the rule summary, styles + i18n (en/ru/zh).

10 unit tests (weekday filter, overnight start-day semantics, tz fallback,
round-trip, invalid-day filtering); full suite green (1936 passed).
(Geographic sunrise/sunset triggers are a natural follow-up — the daylight
value source already has the solar math to reuse.)
2026-06-04 23:54:03 +03:00
alexei.dolgolyov e18d56c838 feat(processing): built-in 'look' presets (Cinematic/Vivid/Cozy/Soft/Cool)
Seed five curated, read-only post-processing templates so a non-expert gets
instant good-looking output before discovering the filter pipeline. Each is an
opinionated chain of existing filters (auto-crop/saturation/contrast/colour-
temperature/temporal-blur) tuned for a use case (films, games, evening ambience,
low-flicker, crisp cool-white).

Mirrors the built-in-gradient pattern: adds is_builtin to PostprocessingTemplate,
seeds missing looks on store init (idempotent, additive — no migration), and
makes built-ins read-only (update/delete raise -> 400; clone to customise).
Surfaced via the existing template picker + is_builtin in the response/type.

7 unit tests (seeding, idempotency, read-only protection, round-trip); full
suite green (1926 passed). (A runtime intensity slider is a follow-up — it needs
a filter-chain parameterisation layer.)
2026-06-04 23:43:11 +03:00
alexei.dolgolyov 7728aecb4f feat(wled): native realtime UDP output (DRGB/DRGBW/DNRGB) with auto-revert
Add WLED's native realtime UDP protocol (port 21324) as a third output mode for
LED targets, alongside DDP and HTTP. For the device LedGrab drives most, this
brings three user-visible wins DDP lacks:

- Auto-revert: every packet carries a timeout byte, so if the stream stops
  (host hiccup/sleep/crash) WLED returns to its preset instead of freezing on
  the last frame.
- Correct RGBW whites: the DRGBW variant carries an explicit white channel.
- Lighter on weak Wi-Fi: raw RGB with a 2-byte header.

New WledRealtimeClient auto-selects DRGB (<=490), DRGBW (<=367), or chunked
DNRGB (>490). WLED applies its own per-bus colour order in realtime mode, so we
send plain RGB and the user's colour-order config just works. Protocol 'udp' is
threaded through WLEDConfig/provider/processor and the schema pattern; the
target editor gains a protocol option + badge + i18n (en/ru/zh).

8 unit tests for the packet builder; full suite green (1919 passed).
2026-06-04 23:34:26 +03:00
alexei.dolgolyov e28ab5a956 Merge feat/power-budget-abl: automatic brightness limiting (ABL) / power budget 2026-06-04 23:22:18 +03:00
alexei.dolgolyov 1e395fd09e Merge fix/verified-bugs: weak default key, broken MQTT route, scene brightness sync 2026-06-04 23:22:18 +03:00
alexei.dolgolyov ffee156c17 feat(targets): automatic brightness limiting (ABL) / per-LED power budget
Cap an addressable strip's estimated current draw to a PSU budget so bright/
white scenes can't brown out an under-spec'd supply (voltage sag -> red/orange
shift, flicker, controller resets) — a classic 'it's broken' first impression.

- New core/processing/power_limit.py: pure current estimate (full white over N
  LEDs draws N * mA_per_led) + a (0,1] scale to land a frame on budget.
- Applied in WledTargetProcessor._send_to_device (single choke point, every send
  path; scales into a reusable scratch buffer, never mutates shared frames).
- Two per-target fields on LED targets: max_milliamps (0 = unlimited) and
  milliamps_per_led (default 55), threaded through model/store/manager/processor/
  schema/route with hot-update via update_target_settings. Additive with safe
  defaults (no data migration needed; legacy targets read as unlimited).
- Frontend: editor fields + i18n (en/ru/zh) + LedOutputTarget type.
- Tests: 10 unit tests for the estimator/scale; full suite green (1911 passed).
2026-06-04 22:56:50 +03:00
alexei.dolgolyov 02e2ea37f3 fix(scenes): sync brightness value-source change to live processor
apply_scene_state computed brightness_changed = "brightness" in changed, but
the change dict only ever uses the key "brightness_value_source_id", so the
branch was dead and a running target's brightness source was never live-synced
on scene activation (it only took effect after a restart). Check the correct
key.
2026-06-04 20:46:26 +03:00
alexei.dolgolyov fdc9201660 fix(api): remove broken legacy /system/mqtt/settings route
The GET/PUT /api/v1/system/mqtt/settings handlers read cfg.mqtt.*, but the
single-broker MQTTConfig block was removed in the multi-broker refactor, so any
call raised AttributeError. Brokers are now first-class MQTTSource entities
managed via the mqtt.py router, and the frontend no longer calls this endpoint.
Remove the dead handlers, the _load_mqtt_settings helper, the now-unused
get_config import, and the orphaned MQTTSettings{Request,Response} schemas.
2026-06-04 20:46:24 +03:00
alexei.dolgolyov 5686ae5468 fix(security): remove active weak default API key from shipped config
default_config.yaml shipped api_keys.dev: "development-key-change-in-production"
uncommitted/active, while the surrounding comment claimed it had been removed.
On a non-loopback bind this is a publicly-known credential granting full LAN
access. Restore the documented secure default (empty api_keys -> loopback-only
anonymous, LAN rejected) and leave a commented example instead.
2026-06-04 20:46:13 +03:00
alexei.dolgolyov 9960f15a1b docs(android): remove ANDROID-REVIEW planning/review docs
The Android feature-gap assessment and per-feature design docs have served
their purpose — notification, audio, webcam, and the foreground-app automation
condition are all implemented and merged, so no gaps remain to track. The
implementation is documented in the code, commit messages, and git history; the
review docs are now obsolete. No committed files referenced them (only the
local-only plans/ archives, left as point-in-time records).
2026-06-02 15:05:11 +03:00
alexei.dolgolyov 397a53ed1c Merge feature/android-foreground-app-automation: Android foreground-app automation condition
Foreground-app -> scene automation on the Android-TV build via a Kotlin
ForegroundAppBridge (UsageStatsManager) bridged into PlatformDetector ahead of the
Windows-only ctypes path; LauncherApps-backed app picker (/system/installed-apps) +
platform signal (/system/info); PACKAGE_USAGE_STATS special-access UX (on-device
button + web-UI banner, graceful degradation). Reuses the existing automation engine
unchanged; zero new deps. assembleDebug + 1897 pytest + ruff + tsc + build green;
independent final + security reviews pass.
2026-06-02 14:57:45 +03:00
alexei.dolgolyov 1c1bbe2551 feat(android): foreground-app automation condition
Make the existing Application automation rule (foreground app -> activate
scene) work on the Android-TV build. A Kotlin ForegroundAppBridge reads the
foreground app via UsageStatsManager and lists launchable apps via LauncherApps;
PlatformDetector bridges it in (ahead of the Windows-only ctypes guard) so the
existing AutomationEngine / ApplicationRule / storage / deactivation modes are
unchanged. New /system/installed-apps + /system/info endpoints feed an app picker
that stores package names (vs process names on desktop); on Android the editor
hides the match-type selector since the foreground app is the only obtainable
signal. PACKAGE_USAGE_STATS is granted via an on-device button + a web-UI banner
(no blanket prompt at capture start); detection degrades gracefully until granted.

Zero new Python/Gradle deps (UsageStatsManager + LauncherApps are in-platform;
matching only string-compares the package name, so no QUERY_ALL_PACKAGES).
assembleDebug + 1897 pytest + ruff + tsc + npm build all green; independent final
review (0 blockers) + security review (no critical issues).
2026-06-02 14:57:29 +03:00
alexei.dolgolyov 68040173c6 Merge feature/android-webcam-capture: Android on-device webcam capture
Camera2 + ImageReader (CameraBridge) -> push RGB frames -> AndroidCameraEngine,
reusing the MediaProjection capture-engine selection path so webcams surface as
selectable displays on Android TV. On-demand camera lifecycle, conditional camera
FGS type (granted-only), single-camera ownership lock. Per-phase + final + security
reviews pass; 14 files; new isolated tests; zero new Python/Gradle deps.
assembleDebug + 1883 pytest + ruff all green.
2026-06-02 13:46:59 +03:00
alexei.dolgolyov 4bf3fe65db feat(android): on-device webcam capture via Camera2 (AndroidCameraEngine)
Add on-device webcam capture to the experimental Android-TV build. Desktop
captures webcams via OpenCV (no Chaquopy/Android wheel); this adds a push-based
AndroidCameraEngine that plugs into the same selection path desktop uses
(capture template engine_type="android_camera" + display_index, HAS_OWN_DISPLAYS).

A Kotlin CameraBridge (Camera2) enumerates cameras and opens them on demand —
only while a capture source is active, driven Python->Kotlin via a guarded jclass
singleton (BleBridge pattern) — converts each frame YUV_420_888->RGB, and pushes
RGB bytes into a module-level queue mirroring mediaprojection_engine.py. Cameras
surface as selectable displays like the desktop OpenCV engine; the data-driven
capture-template UI is unchanged. No new Python deps; no new Gradle deps
(Camera2 is in-platform).

Engine: ENGINE_PRIORITY=0 (never auto-selected over MediaProjection=100; explicit
engine_type only). Single-camera ownership is serialized with a lock + ref-count
(same-camera streams attach, different-camera refused, last release stops),
mirroring the desktop CameraEngine guard.

Permission: CAMERA requested at capture-start, gated on FEATURE_CAMERA_ANY so
camera-less TV boxes never prompt; graceful degradation when denied. The service
is promoted with the camera FGS type (+ FOREGROUND_SERVICE_CAMERA) only when
CAMERA is already granted, so backgrounded capture keeps working without risking
a failed startForeground on camera-less boxes (camera can't ride the
MediaProjection token the way audio playback capture does).

Reviewed via multi-agent adversarial pass (13 findings -> 4 fixed: device leak on
session-failure, multi-stream collision, camera FGS type, i18n key; 9 refuted).

Tests: 18 new desktop-CI tests (no device needed); full suite 1883 passed.
Verified: assembleDebug BUILD SUCCESSFUL, ruff clean.

Docs: ANDROID-REVIEW/android-webcam-capture-plan.md (design), updated
android-missing-functionality.md + README feature table + en/ru/zh locales.
2026-06-02 13:36:23 +03:00
alexei.dolgolyov 34db5de8c3 Merge feature/android-notification-capture: Android on-device notification capture
NotificationListenerService -> push_notification() -> _AndroidBackend path so OS
notifications drive the existing color-strip LED effects on Android TV at app-name
parity. Per-phase + final + security reviews all pass; 11 new isolated tests; zero
new Python deps. Android assembleDebug pending device/CI verification.
2026-06-02 11:47:29 +03:00
alexei.dolgolyov 0be3f833df feat(android): on-device OS notification capture (NotificationListenerService)
Add an Android backend to os_notification_listener.py so notifications on the
experimental Android-TV build drive the existing NotificationColorStripSource
LED effects (flash/pulse/sweep, per-app colors + sounds) at app-name parity
with the Windows/Linux backends.

A Kotlin NotificationListenerService forwards the posting app's display label
across the Chaquopy JNI boundary into a new push-based _AndroidBackend +
module-level push_notification() receiver; the existing color-strip pipeline,
per-app colors/filters, and history endpoint are reused unchanged.

- Python: _AndroidBackend (probed first), push_notification() receiver,
  _LinuxBackend.probe() hardened with is_linux() to exclude Android (which
  also reports platform.system() == "Linux").
- Android: LedGrabNotificationListener NLS — serial single-thread executor,
  full crash isolation around Python.getInstance(), label-only forwarding
  (never notification title/body), ongoing/group-summary/self-package noise
  filtering. Manifest service exported + gated by
  BIND_NOTIFICATION_LISTENER_SERVICE (no new uses-permission).
- UX: prompt-once notification-access + manual "Grant notification access"
  button wired into the D-pad focus chain (computed from visible controls);
  en/ru/zh strings.
- Tests: 11 isolated unit tests — module-global + tmp_path history isolation,
  push routing contract, callback-exception swallowing, None app-name, and a
  desktop-regression lock on backend selection order.
- Docs: README OS-support Android column (notification + audio cells),
  ANDROID-REVIEW status flipped to Implemented.

Zero new Python deps; no build.gradle.kts / Chaquopy pip changes.
2026-06-02 11:47:13 +03:00
alexei.dolgolyov 4b2e8fc5ec docs(android): add audio-capture design + missing-functionality review
- android-audio-capture-plan.md — design behind the merged on-device audio
  capture feature (487259a).
- android-missing-functionality.md — Android missing-feature review notes.
2026-06-02 03:30:43 +03:00
alexei.dolgolyov 487259a96d Merge feature/android-audio-capture: Android on-device audio capture
Push-based AndroidAudioEngine (AudioPlaybackCapture, API 29+) reusing the
MediaProjection token, feeding the unchanged AudioAnalyzer. Build green
(assembleDebug), 1854+13 tests pass. Reviewed: READY/SECURE, 0 blockers.
2026-06-02 03:28:37 +03:00
alexei.dolgolyov fd62db1720 feat(audio): Android on-device system playback capture
Enable audio-reactive lighting on the Android-TV build. A push-based
AndroidAudioEngine captures system playback audio via AudioPlaybackCapture
(API 29+), reusing the existing MediaProjection token, and feeds PCM into
the unchanged AudioAnalyzer pipeline. No new Python deps; no Chaquopy/pip
changes (numpy already bundled).

- Python: android_audio_engine.py — module-level queue + configure/
  push_samples/shutdown mirroring mediaprojection_engine; AndroidAudioEngine
  (priority 100) registered behind a guarded import. push_samples copies and
  defensively trims/clamps each block so the analyzer can't crash on
  variable-length or non-frame-divisible PCM.
- Kotlin: AudioCapture.kt — AudioRecord + AudioPlaybackCaptureConfiguration,
  fixed chunk-size block framing, little-endian float32, mic fallback;
  reads back the actual negotiated channel/sample rate. PythonBridge gains
  configureAudio/pushAudio/shutdownAudio with a cached module handle.
- Wiring: CaptureService starts/stops AudioCapture in the MediaProjection
  path (gated on API>=29 + RECORD_AUDIO + live projection); MainActivity
  requests RECORD_AUDIO; manifest declares it. Degrades gracefully when
  denied; root path stays audio-less by design.
- Tests: 13 desktop-CI tests incl. an over-length/non-divisible regression
  guard that exercises the full read_chunk -> AudioAnalyzer.analyze path.
2026-06-02 03:28:22 +03:00
alexei.dolgolyov 669ae20824 feat(value-sources): optional normalization for magnitude sources
Add an opt-out `normalize` flag to the four magnitude value sources
(ha_entity, http, system_metrics, game_event). get_value() stays in
[0,1] for every source (the normalized scalar-bus invariant), so all
existing consumers — brightness sinks, gradient_map, template `name`
bindings, and color-strip bindable floats — are unaffected.

- normalize=False is a clamp-passthrough: skip the min/max rescale and
  clamp the raw reading into [0,1] (for sources already reporting a
  0..1 fraction). The un-normalized magnitude stays available via
  get_raw_value() / template raw[name] / automations.
- Add get_raw_value() + a raw channel to GameEventValueStream (it had
  none). game_event's flag is model/stream-level only (no CRUD schema).
- Finite-safe clamp01() util; harden the composite-layer brightness
  multiply (latent negative-wrap / >=1 skip) with it.
- Preview WebSocket: tolerate non-numeric raw, generalize raw-range.
- Frontend: settings-toggle slider per HA/system_metrics/http editor
  with min/max grey-out; toggle hidden for fixed-mapping percent
  metrics. en/ru/zh locale keys.
- Additive optional field (default True) — JSON round-trip, no migration.

Tests: store create/update round-trip, clamp-passthrough, live
normalize flip, game_event raw channel + build_stream forwarding,
and finite-safe clamp01.
2026-06-02 02:24:40 +03:00
alexei.dolgolyov 6de61b965e feat(value-sources): add sandboxed-Jinja template combinator
A new `template` value source evaluates a hardened, sandboxed Jinja
expression over the live values of other value sources — the system's
first float combinator.

Backend:
- Shared engine (utils/template_expr.py): ImmutableSandboxedEnvironment with
  filters/tests and auto-injected globals stripped; only min/max/abs/round/
  clamp exposed; rejects **, string/collection-literal repetition, attribute
  access and non-global calls; NaN/inf-safe result coercion.
- TemplateValueSource model + TemplateValueStream runtime: compile-once,
  primitives-only eval context, raw[name] exposure, eval_interval throttle,
  ref-counted input acquire/release, rename-safe hot-update.
- Validation: unbound-variable + reserved-name rejection, reference
  cycle/depth guards (depth-only at create, full cycle at update), runtime
  acquire() depth backstop, and delete referential-integrity.
- API: Create/Update/Response schemas + discriminated unions, _RESPONSE_MAP,
  and an advisory POST /value-sources/validate-template endpoint.
- Demo seed: a static source plus a template combinator example.

Frontend:
- Editor modal section: repeatable inputs list (EntitySelect rows), a
  zero-dependency Jinja syntax highlighter, a hints/reference panel, and a
  debounced live validator that gates Save (stale-response-safe).
- Graph editor: read-only template node with one edge per input.
- i18n (en/ru/zh), icon, and card rendering.

Tests: engine, stream, factory/cycle, validate endpoint, and demo seed.
2026-06-01 18:53:56 +03:00
alexei.dolgolyov 12b40e6071 docs: actualize README and API reference, embed screenshots
README: add free & open-source (MIT) framing; add a Platforms table (Windows/Linux/macOS/Docker supported, Android-TV build marked experimental, scrcpy phone-capture retained); expand the LED device list to ~20 protocols; correct storage to SQLite, the auth model, and resource names (Output Targets, Automations); add a prebuilt-download section; remove the Architecture and Acknowledgments sections; embed dashboard, channels, live-preview, and device-picker screenshots.

docs/API.md: full rewrite from the live route modules — all 253 endpoints across 34 modules, grouped by resource with REST + WebSocket tables, accurate auth model and WS handshake, worked examples, and sensitive-endpoint markers. Replaces the stale v0.1.0 stub.

server/CLAUDE.md: storage is now SQLite (BaseSqliteStore / ledgrab.db / LEDGRAB_DATA_DIR / data_migrations.py); fix the auth description (loopback anonymous, LAN rejected with 401 — not all endpoints open); router registration happens in api/__init__.py.
2026-05-29 14:35:45 +03:00