feat: asset-based image/video sources, notification sounds, UI improvements
Some checks failed
Lint & Test / test (push) Has been cancelled
Some checks failed
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
This commit is contained in:
@@ -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
|
||||
@@ -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 `<audio>`, `<img>`, or download links):
|
||||
|
||||
```typescript
|
||||
// WRONG: no auth header — 401
|
||||
const audio = new Audio(`${API_BASE}/assets/${id}/file`);
|
||||
|
||||
// CORRECT: fetch with auth, then create blob URL
|
||||
const res = await fetchWithAuth(`/assets/${id}/file`);
|
||||
const blob = await res.blob();
|
||||
const audio = new Audio(URL.createObjectURL(blob));
|
||||
```
|
||||
|
||||
The only exception is raw `fetch()` for multipart uploads where you must set `Content-Type` to let the browser handle the boundary — but still use `getHeaders()` for the auth token.
|
||||
|
||||
## Bundling & Development Workflow
|
||||
|
||||
@@ -266,11 +278,11 @@ See [`contexts/chrome-tools.md`](chrome-tools.md) for Chrome MCP tool usage, bro
|
||||
|
||||
### Uptime / duration values
|
||||
|
||||
Use `formatUptime(seconds)` from `core/ui.js`. Outputs `{s}s`, `{m}m {s}s`, or `{h}h {m}m` via i18n keys `time.seconds`, `time.minutes_seconds`, `time.hours_minutes`.
|
||||
Use `formatUptime(seconds)` from `core/ui.ts`. Outputs `{s}s`, `{m}m {s}s`, or `{h}h {m}m` via i18n keys `time.seconds`, `time.minutes_seconds`, `time.hours_minutes`.
|
||||
|
||||
### Large numbers
|
||||
|
||||
Use `formatCompact(n)` from `core/ui.js`. Outputs `1.2K`, `3.5M` etc. Set `element.title` to the exact value for hover detail.
|
||||
Use `formatCompact(n)` from `core/ui.ts`. Outputs `1.2K`, `3.5M` etc. Set `element.title` to the exact value for hover detail.
|
||||
|
||||
### Preventing layout shift
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ Two independent server modes with separate configs, ports, and data directories:
|
||||
| **Real** | `python -m wled_controller` | `config/default_config.yaml` | 8080 | `development-key-change-in-production` | `data/` |
|
||||
| **Demo** | `python -m wled_controller.demo` | `config/demo_config.yaml` | 8081 | `demo` | `data/demo/` |
|
||||
|
||||
Both can run simultaneously on different ports.
|
||||
Demo mode can also be triggered via the `WLED_DEMO` environment variable (`true`, `1`, or `yes`). This works with any launch method — Python, Docker (`-e WLED_DEMO=true`), or the installed app (`set WLED_DEMO=true` before `LedGrab.bat`).
|
||||
|
||||
Both modes can run simultaneously on different ports.
|
||||
|
||||
## Restart Procedure
|
||||
|
||||
|
||||
Reference in New Issue
Block a user