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>
This commit is contained in:
2026-03-15 02:16:59 +03:00
parent 7e78323c9c
commit 294d704eb0
72 changed files with 2992 additions and 1416 deletions

View File

@@ -5,6 +5,7 @@
import {
_discoveryScanRunning, set_discoveryScanRunning,
_discoveryCache, set_discoveryCache,
csptCache,
} from '../core/state.js';
import { API_BASE, fetchWithAuth, isSerialDevice, isMockDevice, isMqttDevice, isWsDevice, isOpenrgbDevice, isDmxDevice, isEspnowDevice, isHueDevice, isUsbhidDevice, isSpiDevice, isChromaDevice, isGameSenseDevice, escapeHtml } from '../core/api.js';
import { devicesCache } from '../core/state.js';
@@ -12,7 +13,8 @@ import { t } from '../core/i18n.js';
import { showToast, desktopFocus } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { _computeMaxFps, _renderFpsHint } from './devices.js';
import { getDeviceTypeIcon, ICON_RADIO, ICON_GLOBE, ICON_CPU, ICON_KEYBOARD, ICON_MOUSE, ICON_HEADPHONES, ICON_PLUG, ICON_TARGET_ICON, ICON_ACTIVITY } from '../core/icons.js';
import { getDeviceTypeIcon, ICON_RADIO, ICON_GLOBE, ICON_CPU, ICON_KEYBOARD, ICON_MOUSE, ICON_HEADPHONES, ICON_PLUG, ICON_TARGET_ICON, ICON_ACTIVITY, ICON_TEMPLATE } from '../core/icons.js';
import { EntitySelect } from '../core/entity-palette.js';
import { IconSelect, showTypePicker } from '../core/icon-select.js';
class AddDeviceModal extends Modal {
@@ -30,6 +32,7 @@ class AddDeviceModal extends Modal {
sendLatency: document.getElementById('device-send-latency')?.value || '0',
zones: JSON.stringify(_getCheckedZones('device-zone-list')),
zoneMode: _getZoneMode(),
csptId: document.getElementById('device-css-processing-template')?.value || '',
dmxProtocol: document.getElementById('device-dmx-protocol')?.value || 'artnet',
dmxStartUniverse: document.getElementById('device-dmx-start-universe')?.value || '0',
dmxStartChannel: document.getElementById('device-dmx-start-channel')?.value || '1',
@@ -53,6 +56,7 @@ function _buildDeviceTypeItems() {
}
let _deviceTypeIconSelect = null;
let _csptEntitySelect = null;
function _ensureDeviceTypeIconSelect() {
const sel = document.getElementById('device-type');
@@ -61,6 +65,30 @@ function _ensureDeviceTypeIconSelect() {
_deviceTypeIconSelect = new IconSelect({ target: sel, items: _buildDeviceTypeItems(), columns: 3 });
}
function _ensureCsptEntitySelect() {
const sel = document.getElementById('device-css-processing-template');
if (!sel) return;
const templates = csptCache.data || [];
// Populate native <select> options
sel.innerHTML = `<option value="">—</option>` +
templates.map(tp => `<option value="${tp.id}">${tp.name}</option>`).join('');
if (_csptEntitySelect) _csptEntitySelect.destroy();
if (templates.length > 0) {
_csptEntitySelect = new EntitySelect({
target: sel,
getItems: () => (csptCache.data || []).map(tp => ({
value: tp.id,
label: tp.name,
icon: ICON_TEMPLATE,
desc: '',
})),
placeholder: t('palette.search'),
allowNone: true,
noneLabel: '—',
});
}
}
/* ── Icon-grid DMX protocol selector ─────────────────────────── */
function _buildDmxProtocolItems() {
@@ -583,6 +611,8 @@ export function showAddDevice(presetType = null) {
const scanBtn = document.getElementById('scan-network-btn');
if (scanBtn) scanBtn.disabled = false;
_ensureDeviceTypeIconSelect();
// Populate CSPT template selector
csptCache.fetch().then(() => _ensureCsptEntitySelect());
// Pre-select type and hide the type selector (already chosen)
document.getElementById('device-type').value = presetType;
@@ -775,6 +805,8 @@ export async function handleAddDevice(event) {
if (isGameSenseDevice(deviceType)) {
body.gamesense_device_type = document.getElementById('device-gamesense-device-type')?.value || 'keyboard';
}
const csptId = document.getElementById('device-css-processing-template')?.value;
if (csptId) body.default_css_processing_template_id = csptId;
if (lastTemplateId) body.capture_template_id = lastTemplateId;
const response = await fetchWithAuth('/devices', {