refactor: comprehensive code quality, security, and release readiness improvements
Some checks failed
Lint & Test / test (push) Failing after 48s
Some checks failed
Lint & Test / test (push) Failing after 48s
Security: tighten CORS defaults, add webhook rate limiting, fix XSS in automations, guard WebSocket JSON.parse, validate ADB address input, seal debug exception leak, URL-encode WS tokens, CSS.escape in selectors. Code quality: add Pydantic models for brightness/power endpoints, fix thread safety and name uniqueness in DeviceStore, immutable update pattern, split 6 oversized files into 16 focused modules, enable TypeScript strictNullChecks (741→102 errors), type state variables, add dom-utils helper, migrate 3 modules from inline onclick to event delegation, ProcessorDependencies dataclass. Performance: async store saves, health endpoint log level, command palette debounce, optimized entity-events comparison, fix service worker precache list. Testing: expand from 45 to 293 passing tests — add store tests (141), route tests (25), core logic tests (42), E2E flow tests (33), organize into tests/api/, tests/storage/, tests/core/, tests/e2e/. DevOps: CI test pipeline, pre-commit config, Dockerfile multi-stage build with non-root user and health check, docker-compose improvements, version bump to 0.2.0. Docs: rewrite CLAUDE.md (202→56 lines), server/CLAUDE.md (212→76), create contexts/server-operations.md, fix .js→.ts references, fix env var prefix in README, rewrite INSTALLATION.md, add CONTRIBUTING.md and .env.example.
This commit is contained in:
@@ -72,17 +72,17 @@ For `EntitySelect` with `allowNone: true`, pass the same i18n string as `noneLab
|
||||
|
||||
Plain `<select>` dropdowns should be enhanced with visual selectors depending on the data type:
|
||||
|
||||
- **Predefined options** (source types, effect types, palettes, waveforms, viz modes) → use `IconSelect` from `js/core/icon-select.js`. This replaces the `<select>` with a visual grid of icon+label+description cells. See `_ensureCSSTypeIconSelect()`, `_ensureEffectTypeIconSelect()`, `_ensureInterpolationIconSelect()` in `color-strips.js` for examples.
|
||||
- **Predefined options** (source types, effect types, palettes, waveforms, viz modes) → use `IconSelect` from `js/core/icon-select.ts`. This replaces the `<select>` with a visual grid of icon+label+description cells. See `_ensureCSSTypeIconSelect()`, `_ensureEffectTypeIconSelect()`, `_ensureInterpolationIconSelect()` in `color-strips.ts` for examples.
|
||||
|
||||
- **Entity references** (picture sources, audio sources, devices, templates, clocks) → use `EntitySelect` from `js/core/entity-palette.js`. This replaces the `<select>` with a searchable command-palette-style picker. See `_cssPictureSourceEntitySelect` in `color-strips.js` or `_lineSourceEntitySelect` in `advanced-calibration.js` for examples.
|
||||
- **Entity references** (picture sources, audio sources, devices, templates, clocks) → use `EntitySelect` from `js/core/entity-palette.ts`. This replaces the `<select>` with a searchable command-palette-style picker. See `_cssPictureSourceEntitySelect` in `color-strips.ts` or `_lineSourceEntitySelect` in `advanced-calibration.ts` for examples.
|
||||
|
||||
Both widgets hide the native `<select>` but keep it in the DOM with its value in sync. After programmatically changing the `<select>` value, call `.refresh()` (EntitySelect) or `.setValue(val)` (IconSelect) to update the trigger display. Call `.destroy()` when the modal closes.
|
||||
|
||||
**IMPORTANT:** For `IconSelect` item icons, use SVG icons from `js/core/icon-paths.js` (via `_icon(P.iconName)`) or styled `<span>` elements (e.g., `<span style="font-weight:bold">A</span>`). **Never use emoji** — they render inconsistently across platforms and themes.
|
||||
**IMPORTANT:** For `IconSelect` item icons, use SVG icons from `js/core/icon-paths.ts` (via `_icon(P.iconName)`) or styled `<span>` elements (e.g., `<span style="font-weight:bold">A</span>`). **Never use emoji** — they render inconsistently across platforms and themes.
|
||||
|
||||
### Modal dirty check (discard unsaved changes)
|
||||
|
||||
Every editor modal **must** have a dirty check so closing with unsaved changes shows a "Discard unsaved changes?" confirmation. Use the `Modal` base class pattern from `js/core/modal.js`:
|
||||
Every editor modal **must** have a dirty check so closing with unsaved changes shows a "Discard unsaved changes?" confirmation. Use the `Modal` base class pattern from `js/core/modal.ts`:
|
||||
|
||||
1. **Subclass Modal** with `snapshotValues()` returning an object of all tracked field values:
|
||||
|
||||
@@ -144,25 +144,25 @@ Do **not** use a `range-with-value` wrapper div.
|
||||
|
||||
### Tutorials
|
||||
|
||||
The app has an interactive tutorial system (`static/js/features/tutorials.js`) with a generic engine, spotlight overlay, tooltip positioning, and keyboard navigation. Tutorials exist for:
|
||||
The app has an interactive tutorial system (`static/js/features/tutorials.ts`) with a generic engine, spotlight overlay, tooltip positioning, and keyboard navigation. Tutorials exist for:
|
||||
- **Getting started** (header-level walkthrough of all tabs and controls)
|
||||
- **Per-tab tutorials** (Dashboard, Targets, Sources, Profiles) triggered by `?` buttons
|
||||
- **Device card tutorial** and **Calibration tutorial** (context-specific)
|
||||
|
||||
When adding **new tabs, sections, or major UI elements**, update the corresponding tutorial step array in `tutorials.js` and add `tour.*` i18n keys to all 3 locale files (`en.json`, `ru.json`, `zh.json`).
|
||||
When adding **new tabs, sections, or major UI elements**, update the corresponding tutorial step array in `tutorials.ts` and add `tour.*` i18n keys to all 3 locale files (`en.json`, `ru.json`, `zh.json`).
|
||||
|
||||
## Icons
|
||||
|
||||
**Always use SVG icons from the icon system, never text/emoji/Unicode symbols for buttons and UI controls.**
|
||||
|
||||
- Icon SVG paths are defined in `static/js/core/icon-paths.js` (Lucide icons, 24×24 viewBox)
|
||||
- Icon constants are exported from `static/js/core/icons.js` (e.g. `ICON_START`, `ICON_TRASH`, `ICON_EDIT`)
|
||||
- Use `_svg(path)` wrapper from `icons.js` to create new icon constants from paths
|
||||
- Icon SVG paths are defined in `static/js/core/icon-paths.ts` (Lucide icons, 24×24 viewBox)
|
||||
- Icon constants are exported from `static/js/core/icons.ts` (e.g. `ICON_START`, `ICON_TRASH`, `ICON_EDIT`)
|
||||
- Use `_svg(path)` wrapper from `icons.ts` to create new icon constants from paths
|
||||
|
||||
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
|
||||
3. Add a corresponding `ICON_*` constant in `icons.js` using `_svg(P.myIcon)`
|
||||
3. Add a corresponding `ICON_*` constant in `icons.ts` using `_svg(P.myIcon)`
|
||||
4. Import and use the constant in your feature module
|
||||
|
||||
Common icons: `ICON_START` (play), `ICON_STOP` (power), `ICON_EDIT` (pencil), `ICON_CLONE` (copy), `ICON_TRASH` (trash), `ICON_SETTINGS` (gear), `ICON_TEST` (flask), `ICON_OK` (circle-check), `ICON_WARNING` (triangle-alert), `ICON_HELP` (circle-help), `ICON_LIST_CHECKS` (list-checks), `ICON_CIRCLE_OFF` (circle-off).
|
||||
@@ -171,9 +171,9 @@ For icon-only buttons, use `btn btn-icon` CSS classes. The `.icon` class inside
|
||||
|
||||
## Localization (i18n)
|
||||
|
||||
**Every user-facing string must be localized.** Never use hardcoded English strings in `showToast()`, `error.textContent`, modal messages, or any other UI-visible text. Always use `t('key')` from `../core/i18n.js` and add the corresponding key to **all three** locale files (`en.json`, `ru.json`, `zh.json`).
|
||||
**Every user-facing string must be localized.** Never use hardcoded English strings in `showToast()`, `error.textContent`, modal messages, or any other UI-visible text. Always use `t('key')` from `../core/i18n.ts` and add the corresponding key to **all three** locale files (`en.json`, `ru.json`, `zh.json`).
|
||||
|
||||
- In JS modules: `import { t } from '../core/i18n.js';` then `showToast(t('my.key'), 'error')`
|
||||
- In JS modules: `import { t } from '../core/i18n.ts';` then `showToast(t('my.key'), 'error')`
|
||||
- In inline `<script>` blocks (where `t()` may not be available yet): use `window.t ? t('key') : 'fallback'`
|
||||
- In HTML templates: use `data-i18n="key"` for text content, `data-i18n-title="key"` for title attributes, `data-i18n-aria-label="key"` for aria-labels
|
||||
- Keys follow dotted namespace convention: `feature.context.description` (e.g. `device.error.brightness`, `calibration.saved`)
|
||||
@@ -196,7 +196,7 @@ The frontend uses **esbuild** to bundle all JS modules and CSS files into single
|
||||
|
||||
### Files
|
||||
|
||||
- **Entry points:** `static/js/app.js` (JS), `static/css/all.css` (CSS imports all individual sheets)
|
||||
- **Entry points:** `static/js/app.ts` (JS), `static/css/all.css` (CSS imports all individual sheets)
|
||||
- **Output:** `static/dist/app.bundle.js` and `static/dist/app.bundle.css` (minified + source maps)
|
||||
- **Config:** `server/esbuild.mjs`
|
||||
- **HTML:** `templates/index.html` references the bundles, not individual source files
|
||||
@@ -219,8 +219,8 @@ The frontend uses **esbuild** to bundle all JS modules and CSS files into single
|
||||
|
||||
All JS/CSS dependencies are bundled — **no CDN or external requests** at runtime:
|
||||
|
||||
- **Chart.js** — imported in `perf-charts.js`, exposed as `window.Chart` for `targets.js` and `dashboard.js`
|
||||
- **ELK.js** — imported in `graph-layout.js` for graph auto-layout
|
||||
- **Chart.js** — imported in `perf-charts.ts`, exposed as `window.Chart` for `targets.ts` and `dashboard.ts`
|
||||
- **ELK.js** — imported in `graph-layout.ts` for graph auto-layout
|
||||
- **Fonts** — DM Sans (400-700) and Orbitron (700) woff2 files in `static/fonts/`, declared in `css/fonts.css`
|
||||
|
||||
When adding a new JS dependency: `npm install <pkg>` in `server/`, then `import` it in the relevant source file. esbuild bundles it automatically.
|
||||
@@ -260,7 +260,7 @@ Reference: `.dashboard-metric-value` in `dashboard.css` uses `font-family: var(-
|
||||
|
||||
### FPS sparkline charts
|
||||
|
||||
Use `createFpsSparkline(canvasId, actualHistory, currentHistory, fpsTarget)` from `core/chart-utils.js`. Wrap the canvas in `.target-fps-sparkline` (36px height, `position: relative`, `overflow: hidden`). Show the value in `.target-fps-label` with `.metric-value` and `.target-fps-avg`.
|
||||
Use `createFpsSparkline(canvasId, actualHistory, currentHistory, fpsTarget)` from `core/chart-utils.ts`. Wrap the canvas in `.target-fps-sparkline` (36px height, `position: relative`, `overflow: hidden`). Show the value in `.target-fps-label` with `.metric-value` and `.target-fps-avg`.
|
||||
|
||||
## Visual Graph Editor
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Visual Graph Editor
|
||||
|
||||
**Read this file when working on the graph editor** (`static/js/features/graph-editor.js` and related modules).
|
||||
**Read this file when working on the graph editor** (`static/js/features/graph-editor.ts` and related modules).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -10,12 +10,12 @@ The graph editor renders all entities (devices, templates, sources, clocks, targ
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `js/features/graph-editor.js` | Main orchestrator — toolbar, keyboard, search, filter, add-entity menu, port/node drag, minimap |
|
||||
| `js/core/graph-layout.js` | ELK.js layout, `buildGraph()`, `computePorts()`, entity color/label maps |
|
||||
| `js/core/graph-nodes.js` | SVG node rendering, overlay buttons, per-node color overrides |
|
||||
| `js/core/graph-edges.js` | SVG edge rendering (bezier curves, arrowheads, flow dots) |
|
||||
| `js/core/graph-canvas.js` | Pan/zoom controller with `zoomToPoint()` rAF animation |
|
||||
| `js/core/graph-connections.js` | CONNECTION_MAP — which fields link entity types, drag-connect/detach logic |
|
||||
| `js/features/graph-editor.ts` | Main orchestrator — toolbar, keyboard, search, filter, add-entity menu, port/node drag, minimap |
|
||||
| `js/core/graph-layout.ts` | ELK.js layout, `buildGraph()`, `computePorts()`, entity color/label maps |
|
||||
| `js/core/graph-nodes.ts` | SVG node rendering, overlay buttons, per-node color overrides |
|
||||
| `js/core/graph-edges.ts` | SVG edge rendering (bezier curves, arrowheads, flow dots) |
|
||||
| `js/core/graph-canvas.ts` | Pan/zoom controller with `zoomToPoint()` rAF animation |
|
||||
| `js/core/graph-connections.ts` | CONNECTION_MAP — which fields link entity types, drag-connect/detach logic |
|
||||
| `css/graph-editor.css` | All graph-specific styles |
|
||||
|
||||
### Data flow
|
||||
@@ -41,27 +41,27 @@ Nodes have input ports (left) and output ports (right), colored by edge type. Po
|
||||
|
||||
### Adding a new entity type
|
||||
|
||||
1. **`graph-layout.js`** — `ENTITY_COLORS`, `ENTITY_LABELS`, `buildGraph()` (add node loop + edge loops)
|
||||
2. **`graph-layout.js`** — `edgeType()` function if the new type needs a distinct edge color
|
||||
3. **`graph-nodes.js`** — `KIND_ICONS` (default icon), `SUBTYPE_ICONS` (subtype-specific icons)
|
||||
4. **`graph-nodes.js`** — `START_STOP_KINDS` or `TEST_KINDS` sets if the entity supports start/stop or test
|
||||
5. **`graph-connections.js`** — `CONNECTION_MAP` for drag-connect edge creation
|
||||
6. **`graph-editor.js`** — `ADD_ENTITY_MAP` (add-entity menu entry with window function)
|
||||
7. **`graph-editor.js`** — `ALL_CACHES` array (for new-entity-focus watcher)
|
||||
8. **`graph-editor.js`** — `_fetchAllEntities()` (add cache fetch + pass to `computeLayout`)
|
||||
9. **`core/state.js`** — Add/export the new DataCache
|
||||
10. **`app.js`** — Import and window-export the add/edit/clone functions
|
||||
1. **`graph-layout.ts`** — `ENTITY_COLORS`, `ENTITY_LABELS`, `buildGraph()` (add node loop + edge loops)
|
||||
2. **`graph-layout.ts`** — `edgeType()` function if the new type needs a distinct edge color
|
||||
3. **`graph-nodes.ts`** — `KIND_ICONS` (default icon), `SUBTYPE_ICONS` (subtype-specific icons)
|
||||
4. **`graph-nodes.ts`** — `START_STOP_KINDS` or `TEST_KINDS` sets if the entity supports start/stop or test
|
||||
5. **`graph-connections.ts`** — `CONNECTION_MAP` for drag-connect edge creation
|
||||
6. **`graph-editor.ts`** — `ADD_ENTITY_MAP` (add-entity menu entry with window function)
|
||||
7. **`graph-editor.ts`** — `ALL_CACHES` array (for new-entity-focus watcher)
|
||||
8. **`graph-editor.ts`** — `_fetchAllEntities()` (add cache fetch + pass to `computeLayout`)
|
||||
9. **`core/state.ts`** — Add/export the new DataCache
|
||||
10. **`app.ts`** — Import and window-export the add/edit/clone functions
|
||||
|
||||
### Adding a new field/connection to an existing entity
|
||||
|
||||
1. **`graph-layout.js`** — `buildGraph()` edges section: add `addEdge()` call
|
||||
2. **`graph-connections.js`** — `CONNECTION_MAP`: add the field entry
|
||||
3. **`graph-edges.js`** — `EDGE_COLORS` if a new edge type is needed
|
||||
1. **`graph-layout.ts`** — `buildGraph()` edges section: add `addEdge()` call
|
||||
2. **`graph-connections.ts`** — `CONNECTION_MAP`: add the field entry
|
||||
3. **`graph-edges.ts`** — `EDGE_COLORS` if a new edge type is needed
|
||||
|
||||
### Adding a new entity subtype
|
||||
|
||||
1. **`graph-nodes.js`** — `SUBTYPE_ICONS[kind]` — add icon for the new subtype
|
||||
2. **`graph-layout.js`** — `buildGraph()` — ensure `subtype` is extracted from the entity data
|
||||
1. **`graph-nodes.ts`** — `SUBTYPE_ICONS[kind]` — add icon for the new subtype
|
||||
2. **`graph-layout.ts`** — `buildGraph()` — ensure `subtype` is extracted from the entity data
|
||||
|
||||
## Features & keyboard shortcuts
|
||||
|
||||
@@ -90,7 +90,7 @@ Rendered as a small SVG with colored rects for each node and a viewport rect. Su
|
||||
|
||||
## Node hover FPS tooltip
|
||||
|
||||
Running `output_target` nodes show a floating HTML tooltip on hover (300ms delay). The tooltip is an absolutely-positioned `<div class="graph-node-tooltip">` inside `.graph-container` (not SVG — needed for Chart.js canvas). It displays errors, uptime, and a FPS sparkline (reusing `createFpsSparkline` from `core/chart-utils.js`). The sparkline is seeded from `/api/v1/system/metrics-history` for instant context.
|
||||
Running `output_target` nodes show a floating HTML tooltip on hover (300ms delay). The tooltip is an absolutely-positioned `<div class="graph-node-tooltip">` inside `.graph-container` (not SVG — needed for Chart.js canvas). It displays errors, uptime, and a FPS sparkline (reusing `createFpsSparkline` from `core/chart-utils.ts`). The sparkline is seeded from `/api/v1/system/metrics-history` for instant context.
|
||||
|
||||
**Hover events** use `pointerover`/`pointerout` with `relatedTarget` check to prevent flicker when the cursor moves between child SVG elements within the same `<g>` node.
|
||||
|
||||
|
||||
78
contexts/server-operations.md
Normal file
78
contexts/server-operations.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Server Operations
|
||||
|
||||
**Read this file when restarting, starting, or managing the server process.**
|
||||
|
||||
## Server Modes
|
||||
|
||||
Two independent server modes with separate configs, ports, and data directories:
|
||||
|
||||
| Mode | Command | Config | Port | API Key | Data |
|
||||
| ---- | ------- | ------ | ---- | ------- | ---- |
|
||||
| **Real** | `python -m wled_controller.main` | `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.
|
||||
|
||||
## Restart Procedure
|
||||
|
||||
### Real server
|
||||
|
||||
Use the PowerShell restart script — it reliably stops only the server process and starts a new detached instance:
|
||||
|
||||
```bash
|
||||
powershell -ExecutionPolicy Bypass -File "c:\Users\Alexei\Documents\wled-screen-controller\server\restart.ps1"
|
||||
```
|
||||
|
||||
### Demo server
|
||||
|
||||
Find and kill the process on port 8081, then restart:
|
||||
|
||||
```bash
|
||||
# Find PID
|
||||
powershell -Command "netstat -ano | Select-String ':8081.*LISTEN'"
|
||||
# Kill it
|
||||
powershell -Command "Stop-Process -Id <PID> -Force"
|
||||
# Restart
|
||||
cd server && python -m wled_controller.demo
|
||||
```
|
||||
|
||||
**Do NOT use** `Stop-Process -Name python` — it kills unrelated Python processes (VS Code extensions, etc.).
|
||||
|
||||
**Do NOT use** bash background `&` jobs — they get killed when the shell session ends.
|
||||
|
||||
## When to Restart
|
||||
|
||||
**Restart required** for changes to:
|
||||
- API routes (`api/routes/`, `api/schemas/`)
|
||||
- Core logic (`core/*.py`)
|
||||
- Configuration (`config.py`)
|
||||
- Utilities (`utils/*.py`)
|
||||
- Data models (`storage/`)
|
||||
|
||||
**No restart needed** for:
|
||||
- Static files (`static/js/`, `static/css/`) — but **must rebuild bundle**: `cd server && npm run build`
|
||||
- Locale files (`static/locales/*.json`) — loaded by frontend
|
||||
- Documentation files (`*.md`)
|
||||
|
||||
## Auto-Reload Note
|
||||
|
||||
Auto-reload is disabled (`reload=False` in `main.py`) due to watchfiles causing an infinite reload loop. Manual restart is required after server code changes.
|
||||
|
||||
## Demo Mode Awareness
|
||||
|
||||
**When adding new entity types, engines, device providers, or stores — keep demo mode in sync:**
|
||||
|
||||
1. **New entity stores**: Add the store's file path to `StorageConfig` in `config.py` — `model_post_init()` auto-rewrites `data/` to `data/demo/` paths when demo is active.
|
||||
2. **New capture engines**: Verify demo mode filtering works — demo engines use `is_demo_mode()` gate in `is_available()`.
|
||||
3. **New audio engines**: Same as capture engines — `is_available()` must respect `is_demo_mode()`.
|
||||
4. **New device providers**: Gate discovery with `is_demo_mode()` like `DemoDeviceProvider.discover()`.
|
||||
5. **New seed data**: Update `server/src/wled_controller/core/demo_seed.py` to include sample entities.
|
||||
6. **Frontend indicators**: Demo state exposed via `GET /api/v1/version` -> `demo_mode: bool`. Frontend stores it as `demoMode` in app state and sets `document.body.dataset.demo = 'true'`.
|
||||
7. **Backup/Restore**: New stores added to `STORE_MAP` in `system.py` automatically work in demo mode since the data directory is already isolated.
|
||||
|
||||
### Key files
|
||||
|
||||
- Config flag: `server/src/wled_controller/config.py` -> `Config.demo`, `is_demo_mode()`
|
||||
- Demo engines: `core/capture_engines/demo_engine.py`, `core/audio/demo_engine.py`
|
||||
- Demo devices: `core/devices/demo_provider.py`
|
||||
- Seed data: `core/demo_seed.py`
|
||||
Reference in New Issue
Block a user