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
@@ -4,6 +4,7 @@
import {
_deviceBrightnessCache, updateDeviceBrightness,
csptCache,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice, isMockDevice, isMqttDevice, isWsDevice, isOpenrgbDevice, isDmxDevice } from '../core/api.js';
import { devicesCache } from '../core/state.js';
@@ -11,11 +12,36 @@ import { _fetchOpenrgbZones, _getCheckedZones, _splitOpenrgbZone, _getZoneMode,
import { t } from '../core/i18n.js';
import { showToast, showConfirm, desktopFocus } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_PLUG, ICON_REFRESH } from '../core/icons.js';
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_PLUG, ICON_REFRESH, ICON_TEMPLATE } from '../core/icons.js';
import { wrapCard } from '../core/card-colors.js';
import { TagInput, renderTagChips } from '../core/tag-input.js';
import { EntitySelect } from '../core/entity-palette.js';
let _deviceTagsInput = null;
let _settingsCsptEntitySelect = null;
function _ensureSettingsCsptSelect() {
const sel = document.getElementById('settings-css-processing-template');
if (!sel) return;
const templates = csptCache.data || [];
sel.innerHTML = `<option value="">—</option>` +
templates.map(tp => `<option value="${tp.id}">${tp.name}</option>`).join('');
if (_settingsCsptEntitySelect) _settingsCsptEntitySelect.destroy();
if (templates.length > 0) {
_settingsCsptEntitySelect = new EntitySelect({
target: sel,
getItems: () => (csptCache.data || []).map(tp => ({
value: tp.id,
label: tp.name,
icon: ICON_TEMPLATE,
desc: '',
})),
placeholder: window.t ? t('palette.search') : 'Search...',
allowNone: true,
noneLabel: '—',
});
}
}
class DeviceSettingsModal extends Modal {
constructor() { super('device-settings-modal'); }
@@ -38,6 +64,7 @@ class DeviceSettingsModal extends Modal {
dmxProtocol: document.getElementById('settings-dmx-protocol')?.value || 'artnet',
dmxStartUniverse: document.getElementById('settings-dmx-start-universe')?.value || '0',
dmxStartChannel: document.getElementById('settings-dmx-start-channel')?.value || '1',
csptId: document.getElementById('settings-css-processing-template')?.value || '',
};
}
@@ -394,6 +421,12 @@ export async function showSettings(deviceId) {
});
_deviceTagsInput.setValue(device.tags || []);
// CSPT template selector
await csptCache.fetch();
_ensureSettingsCsptSelect();
const csptSel = document.getElementById('settings-css-processing-template');
if (csptSel) csptSel.value = device.default_css_processing_template_id || '';
settingsModal.snapshot();
settingsModal.open();
@@ -407,7 +440,7 @@ export async function showSettings(deviceId) {
}
export function isSettingsDirty() { return settingsModal.isDirty(); }
export function forceCloseDeviceSettingsModal() { if (_deviceTagsInput) { _deviceTagsInput.destroy(); _deviceTagsInput = null; } settingsModal.forceClose(); }
export function forceCloseDeviceSettingsModal() { if (_deviceTagsInput) { _deviceTagsInput.destroy(); _deviceTagsInput = null; } if (_settingsCsptEntitySelect) { _settingsCsptEntitySelect.destroy(); _settingsCsptEntitySelect = null; } settingsModal.forceClose(); }
export function closeDeviceSettingsModal() { settingsModal.close(); }
export async function saveDeviceSettings() {
@@ -449,6 +482,8 @@ export async function saveDeviceSettings() {
body.dmx_start_universe = parseInt(document.getElementById('settings-dmx-start-universe')?.value || '0', 10);
body.dmx_start_channel = parseInt(document.getElementById('settings-dmx-start-channel')?.value || '1', 10);
}
const csptId = document.getElementById('settings-css-processing-template')?.value || '';
body.default_css_processing_template_id = csptId;
const deviceResponse = await fetchWithAuth(`/devices/${deviceId}`, {
method: 'PUT',
body: JSON.stringify(body)