diff --git a/BRAINSTORM.md b/BRAINSTORM.md deleted file mode 100644 index e66a5c3..0000000 --- a/BRAINSTORM.md +++ /dev/null @@ -1,118 +0,0 @@ -# Feature Brainstorm — LED Grab - -## New Automation Conditions (Profiles) - -Right now profiles only trigger on **app detection**. High-value additions: - -- **Time-of-day / Schedule** — "warm tones after sunset, off at midnight." Schedule-based value sources pattern already exists -- **Display state** — detect monitor on/off/sleep, auto-stop targets when display is off -- **System idle** — dim or switch to ambient effect after N minutes of no input -- **Sunrise/sunset** — fetch local solar times, drive circadian color temperature shifts -- **Webhook/MQTT trigger** — let external systems activate profiles without HA integration - -## New Output Targets - -Currently: WLED, Adalight, AmbileD, DDP. Potential: - -- **MQTT publish** — generic IoT output, any MQTT subscriber becomes a target -- **Art-Net / sACN (E1.31)** — stage/theatrical lighting protocols, DMX controllers -- **OpenRGB** — control PC peripherals (keyboard, mouse, RAM, fans) as ambient targets -- **HTTP webhook** — POST color data to arbitrary endpoints -- **Recording target** — save color streams to file for playback later - -## New Color Strip Sources - -- **Spotify / media player** — album art color extraction or tempo-synced effects -- **Weather** — pull conditions from API, map to palettes (blue=rain, orange=sun, white=snow) -- **Camera / webcam** — border-sampling from camera feed for video calls or room-reactive lighting -- **Script source** — user-written JS/Python snippets producing color arrays per frame -- **Notification reactive** — flash/pulse on OS notifications (optional app filter) - -## Processing Pipeline Extensions - -- **Palette quantization** — force output to match a user-defined palette -- **Zone grouping** — merge adjacent LEDs into logical groups sharing one averaged color -- **Color temperature filter** — warm/cool shift separate from hue shift (circadian/mood) -- **Noise gate** — suppress small color changes below threshold, preventing shimmer on static content - -## Multi-Instance & Sync - -- **Multi-room sync** — multiple instances with shared clock for synchronized effects -- **Multi-display unification** — treat 2-3 monitors as single virtual display for seamless ambilight -- **Leader/follower mode** — one target's output drives others with optional delay (cascade) - -## UX & Dashboard - -- **PWA / mobile layout** — mobile-first layout + "Add to Home Screen" manifest -- **Scene presets** — bundled source + filters + brightness as one-click presets ("Movie night", "Gaming") -- **Live preview on dashboard** — miniature screen with LED colors rendered around its border -- **Undo/redo for calibration** — reduce frustration in the fiddly calibration editor -- **Drag-and-drop filter ordering** — reorder postprocessing filter chains visually - -## API & Integration - -- **WebSocket event bus** — broadcast all state changes over a single WS channel -- **OBS integration** — detect active scene, switch profiles; or use OBS virtual camera as source -- **Plugin system** — formalize extension points into documented plugin API with hot-reload - -## Creative / Fun - -- **Effect sequencer** — timeline-based choreography of effects, colors, and transitions -- **Music BPM sync** — lock effect speed to detected BPM (beat detection already exists) -- **Color extraction from image** — upload photo, extract palette, use as gradient/cycle source -- **Transition effects** — crossfade, wipe, or dissolve between sources/profiles instead of instant cut - ---- - -## Deep Dive: Notification Reactive Source - -**Type:** New `ColorStripSource` (`source_type: "notification"`) — normally outputs transparent RGBA, flashes on notification events. Designed to be used as a layer in a **composite source** so it overlays on top of a persistent base (gradient, effect, screen capture, etc.). - -### Trigger modes (both active simultaneously) - -1. **OS listener (Windows)** — `pywinrt` + `Windows.UI.Notifications.Management.UserNotificationListener`. Runs in background thread, pushes events to source via queue. Windows-only for now; macOS (`pyobjc` + `NSUserNotificationCenter`) and Linux (`dbus` + `org.freedesktop.Notifications`) deferred to future. -2. **Webhook** — `POST /api/v1/notifications/{source_id}/fire` with optional body `{ "app": "MyApp", "color": "#FF0000" }`. Always available, cross-platform by nature. - -### Source config - -```yaml -os_listener: true # enable Windows notification listener -app_filter: - mode: whitelist|blacklist # which apps to react to - apps: [Discord, Slack, Telegram] -app_colors: # user-configured app → color mapping - Discord: "#5865F2" - Slack: "#4A154B" - Telegram: "#26A5E4" -default_color: "#FFFFFF" # fallback when app has no mapping -effect: flash|pulse|sweep # visual effect type -duration_ms: 1500 # effect duration -``` - -### Effect rendering - -Source outputs RGBA color array per frame: -- **Idle**: all pixels `(0,0,0,0)` — composite passes through base layer -- **Flash**: instant full-color, linear fade to transparent over `duration_ms` -- **Pulse**: sine fade in/out over `duration_ms` -- **Sweep**: color travels across the strip like a wave - -Each notification starts its own mini-timeline from trigger timestamp (not sync clock). - -### Overlap handling - -New notification while previous effect is active → restart timer with new color. No queuing. - -### App color resolution - -1. Webhook body `color` field (explicit override) → highest priority -2. `app_colors` mapping by app name -3. `default_color` fallback - ---- - -## Top Picks (impact vs effort) - -1. **Time-of-day + idle profile conditions** — builds on existing profile/condition architecture -2. **MQTT output target** — opens the door to an enormous IoT ecosystem -3. **Scene presets** — purely frontend, bundles existing features into one-click UX diff --git a/README.md b/README.md index d12f663..8e19515 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,26 @@ Open **http://localhost:8080** to access the dashboard. See [INSTALLATION.md](INSTALLATION.md) for the full installation guide, including configuration, Docker manual builds, and Home Assistant setup. +## Demo Mode + +Demo mode runs the server with virtual devices, sample data, and isolated storage — useful for exploring the UI without real hardware. + +Set the `WLED_DEMO` environment variable to `true`, `1`, or `yes`: + +```bash +# Docker +docker compose run -e WLED_DEMO=true server + +# Python +WLED_DEMO=true uvicorn wled_controller.main:app --host 0.0.0.0 --port 8081 + +# Windows (installed app) +set WLED_DEMO=true +LedGrab.bat +``` + +Demo mode uses port **8081**, config file `config/demo_config.yaml`, and stores data in `data/demo/` (separate from production data). It can run alongside the main server. + ## Architecture ```text diff --git a/TODO.md b/TODO.md deleted file mode 100644 index f75a2f9..0000000 --- a/TODO.md +++ /dev/null @@ -1,37 +0,0 @@ -# Build Size Reduction - -## Phase 1: Quick Wins (build scripts) - -- [x] Strip unused NumPy submodules (polynomial, linalg, ma, lib, distutils) -- [x] Strip debug symbols from .pyd/.dll/.so files -- [x] Remove zeroconf service database -- [x] Remove .py source from site-packages after compiling to .pyc -- [x] Strip unused PIL image plugins (keep JPEG/PNG/ICO/BMP for tray) - -## Phase 2: Replace Pillow with cv2 - -- [x] Create `utils/image_codec.py` with cv2-based image helpers -- [x] Replace PIL in `_preview_helpers.py` -- [x] Replace PIL in `picture_sources.py` -- [x] Replace PIL in `color_strip_sources.py` -- [x] Replace PIL in `templates.py` -- [x] Replace PIL in `postprocessing.py` -- [x] Replace PIL in `output_targets_keycolors.py` -- [x] Replace PIL in `kc_target_processor.py` -- [x] Replace PIL in `pixelate.py` filter -- [x] Replace PIL in `downscaler.py` filter -- [x] Replace PIL in `scrcpy_engine.py` -- [x] Replace PIL in `live_stream_manager.py` -- [x] Move Pillow from core deps to [tray] optional in pyproject.toml -- [x] Make PIL import conditional in `tray.py` -- [x] Move opencv-python-headless to core dependencies - -## Phase 4: OpenCV stripping (build scripts) - -- [x] Strip ffmpeg DLL, Haar cascades, dev files (already existed) -- [x] Strip typing stubs (already existed) - -## Verification - -- [x] Lint: `ruff check src/ tests/ --fix` -- [x] Tests: 341 passed diff --git a/contexts/auto-update-plan.md b/contexts/auto-update-plan.md deleted file mode 100644 index 60e7033..0000000 --- a/contexts/auto-update-plan.md +++ /dev/null @@ -1,143 +0,0 @@ -# Auto-Update Plan — Phase 1: Check & Notify - -> Created: 2026-03-25. Status: **planned, not started.** - -## Backend Architecture - -### Release Provider Abstraction - -``` -core/update/ - release_provider.py — ABC: get_releases(), get_releases_page_url() - gitea_provider.py — Gitea REST API implementation - version_check.py — normalize_version(), is_newer() using packaging.version - update_service.py — Background asyncio task + state machine -``` - -**`ReleaseProvider` interface** — two methods: -- `get_releases(limit) → list[ReleaseInfo]` — fetch releases (newest first) -- `get_releases_page_url() → str` — link for "view on web" - -**`GiteaReleaseProvider`** calls `GET {base_url}/api/v1/repos/{repo}/releases`. Swapping to GitHub later means implementing the same interface against `api.github.com`. - -**Data models:** - -```python -@dataclass(frozen=True) -class AssetInfo: - name: str # "LedGrab-v0.3.0-win-x64.zip" - size: int # bytes - download_url: str - -@dataclass(frozen=True) -class ReleaseInfo: - tag: str # "v0.3.0" - version: str # "0.3.0" - name: str # "LedGrab v0.3.0" - body: str # release notes markdown - prerelease: bool - published_at: str # ISO 8601 - assets: tuple[AssetInfo, ...] -``` - -### Version Comparison - -`version_check.py` — normalize Gitea tags to PEP 440: -- `v0.3.0-alpha.1` → `0.3.0a1` -- `v0.3.0-beta.2` → `0.3.0b2` -- `v0.3.0-rc.3` → `0.3.0rc3` - -Uses `packaging.version.Version` for comparison. - -### Update Service - -Follows the **AutoBackupEngine pattern**: -- Settings in `Database.get_setting("auto_update")` -- asyncio.Task for periodic checks -- 30s startup delay (avoid slowing boot) -- 60s debounce on manual checks - -**State machine (Phase 1):** `IDLE → CHECKING → UPDATE_AVAILABLE` - -No download/apply in Phase 1 — just detection and notification. - -**Settings:** `enabled` (bool), `check_interval_hours` (float), `channel` ("stable" | "pre-release") - -**Persisted state:** `dismissed_version`, `last_check` (survives restarts) - -### API Endpoints - -| Method | Path | Purpose | -|--------|------|---------| -| `GET` | `/api/v1/system/update/status` | Current state + available version | -| `POST` | `/api/v1/system/update/check` | Trigger immediate check | -| `POST` | `/api/v1/system/update/dismiss` | Dismiss notification for current version | -| `GET` | `/api/v1/system/update/settings` | Get settings | -| `PUT` | `/api/v1/system/update/settings` | Update settings | - -### Wiring - -- New `get_update_service()` in `dependencies.py` -- `UpdateService` created in `main.py` lifespan, `start()`/`stop()` alongside other engines -- Router registered in `api/__init__.py` -- WebSocket event: `update_available` fired via `processor_manager.fire_event()` - -## Frontend - -### Version badge highlight - -The existing `#server-version` pill in the header gets a CSS class `has-update` when a newer version exists — changes the background to `var(--warning-color)` with a subtle pulse, making it clickable to open the update panel in settings. - -### Notification popup - -On `server:update_available` WebSocket event (and on page load if status says `has_update` and not dismissed): -- A **persistent dismissible banner** slides in below the header (not the ephemeral 3s toast) -- Shows: "Version {x.y.z} is available" + [View Release Notes] + [Dismiss] -- Dismiss calls `POST /dismiss` and hides the bar for that version -- Stored in `localStorage` so it doesn't re-show after page refresh for dismissed versions - -### Settings tab: "Updates" - -New 5th tab in the settings modal: -- Current version display -- "Check for updates" button + spinner -- Channel selector (stable / pre-release) via IconSelect -- Auto-check toggle + interval selector -- When update available: release name, notes preview, link to releases page - -### i18n keys - -New `update.*` keys in `en.json`, `ru.json`, `zh.json` for all labels. - -## Files to Create - -| File | Purpose | -|------|---------| -| `core/update/__init__.py` | Package init | -| `core/update/release_provider.py` | Abstract provider interface + data models | -| `core/update/gitea_provider.py` | Gitea API implementation | -| `core/update/version_check.py` | Semver normalization + comparison | -| `core/update/update_service.py` | Background service + state machine | -| `api/routes/update.py` | REST endpoints | -| `api/schemas/update.py` | Pydantic request/response models | - -## Files to Modify - -| File | Change | -|------|--------| -| `api/__init__.py` | Register update router | -| `api/dependencies.py` | Add `get_update_service()` | -| `main.py` | Create & start/stop UpdateService in lifespan | -| `templates/modals/settings.html` | Add Updates tab | -| `static/js/features/settings.ts` | Update check/settings UI logic | -| `static/js/core/api.ts` | Version badge highlight on health check | -| `static/css/layout.css` | `.has-update` styles for version badge | -| `static/locales/en.json` | i18n keys | -| `static/locales/ru.json` | i18n keys | -| `static/locales/zh.json` | i18n keys | - -## Future Phases (not in scope) - -- **Phase 2**: Download & stage artifacts -- **Phase 3**: Apply update & restart (external updater script, NSIS silent mode) -- **Phase 4**: Checksums, "What's new" dialog, update history diff --git a/contexts/frontend.md b/contexts/frontend.md index 13ca78d..caeb6d2 100644 --- a/contexts/frontend.md +++ b/contexts/frontend.md @@ -175,7 +175,7 @@ When adding **new tabs, sections, or major UI elements**, update the correspondi When you need a new icon: 1. Find the Lucide icon at https://lucide.dev -2. Copy the inner SVG elements (paths, circles, rects) into `icon-paths.js` as a new export +2. Copy the inner SVG elements (paths, circles, rects) into `icon-paths.ts` as a new export 3. Add a corresponding `ICON_*` constant in `icons.ts` using `_svg(P.myIcon)` 4. Import and use the constant in your feature module @@ -213,7 +213,19 @@ Static HTML using `data-i18n` attributes is handled automatically by the i18n sy - `fetchWithAuth('/devices/dev_123', { method: 'DELETE' })` → `DELETE /api/v1/devices/dev_123` - Passing `/api/v1/gradients` results in **double prefix**: `/api/v1/api/v1/gradients` (404) -For raw `fetch()` without auth (rare), use the full path manually. +**NEVER use raw `fetch()` or `new Audio(url)` / `new Image()` for authenticated endpoints.** These bypass the auth token and will fail with 401. Always use `fetchWithAuth()` and convert to blob URLs when needed (e.g. for `