From 39e41dfce745f6a900e5194c56df16b5c8f0b786 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sun, 1 Mar 2026 22:07:54 +0300 Subject: [PATCH] Remove per-source speed, fix device dirty check, and add frontend caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Speed is now exclusively controlled via sync clocks — CSS sources no longer carry their own speed/cycle_speed fields. Streams default to 1.0× when no clock is assigned. Also fixes false-positive dirty check on the device settings modal (array reference comparison) and converts several frontend modules to use DataCache for consistent API response caching. Co-Authored-By: Claude Opus 4.6 --- .../api/routes/color_strip_sources.py | 6 -- .../api/schemas/color_strip_sources.py | 6 -- .../core/processing/color_strip_stream.py | 7 +- .../core/processing/effect_stream.py | 3 +- .../static/js/features/calibration.js | 19 ++--- .../static/js/features/color-strips.js | 49 ++--------- .../static/js/features/devices.js | 2 +- .../static/js/features/kc-targets.js | 7 +- .../static/js/features/pattern-templates.js | 4 +- .../static/js/features/streams.js | 82 ++++++++----------- server/src/wled_controller/static/sw.js | 2 +- .../storage/color_strip_source.py | 7 -- .../storage/color_strip_store.py | 10 --- .../templates/modals/css-editor.html | 37 --------- .../templates/modals/sync-clock-editor.html | 2 +- 15 files changed, 56 insertions(+), 187 deletions(-) diff --git a/server/src/wled_controller/api/routes/color_strip_sources.py b/server/src/wled_controller/api/routes/color_strip_sources.py index 69c7b05..643297f 100644 --- a/server/src/wled_controller/api/routes/color_strip_sources.py +++ b/server/src/wled_controller/api/routes/color_strip_sources.py @@ -74,9 +74,7 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe color=getattr(source, "color", None), stops=stops, colors=getattr(source, "colors", None), - cycle_speed=getattr(source, "cycle_speed", None), effect_type=getattr(source, "effect_type", None), - speed=getattr(source, "speed", None), palette=getattr(source, "palette", None), intensity=getattr(source, "intensity", None), scale=getattr(source, "scale", None), @@ -163,9 +161,7 @@ async def create_color_strip_source( frame_interpolation=data.frame_interpolation, animation=data.animation.model_dump() if data.animation else None, colors=data.colors, - cycle_speed=data.cycle_speed, effect_type=data.effect_type, - speed=data.speed, palette=data.palette, intensity=data.intensity, scale=data.scale, @@ -241,9 +237,7 @@ async def update_color_strip_source( frame_interpolation=data.frame_interpolation, animation=data.animation.model_dump() if data.animation else None, colors=data.colors, - cycle_speed=data.cycle_speed, effect_type=data.effect_type, - speed=data.speed, palette=data.palette, intensity=data.intensity, scale=data.scale, diff --git a/server/src/wled_controller/api/schemas/color_strip_sources.py b/server/src/wled_controller/api/schemas/color_strip_sources.py index b14af7b..e660aa4 100644 --- a/server/src/wled_controller/api/schemas/color_strip_sources.py +++ b/server/src/wled_controller/api/schemas/color_strip_sources.py @@ -64,10 +64,8 @@ class ColorStripSourceCreate(BaseModel): stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type") # color_cycle-type fields colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)") - cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier 0.1–10.0 (color_cycle type)", ge=0.1, le=10.0) # effect-type fields effect_type: Optional[str] = Field(None, description="Effect algorithm: fire|meteor|plasma|noise|aurora") - speed: Optional[float] = Field(None, description="Effect speed multiplier 0.1-10.0", ge=0.1, le=10.0) palette: Optional[str] = Field(None, description="Named palette (fire/ocean/lava/forest/rainbow/aurora/sunset/ice)") intensity: Optional[float] = Field(None, description="Effect intensity 0.1-2.0", ge=0.1, le=2.0) scale: Optional[float] = Field(None, description="Spatial scale 0.5-5.0", ge=0.5, le=5.0) @@ -111,10 +109,8 @@ class ColorStripSourceUpdate(BaseModel): stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type") # color_cycle-type fields colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)") - cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier 0.1–10.0 (color_cycle type)", ge=0.1, le=10.0) # effect-type fields effect_type: Optional[str] = Field(None, description="Effect algorithm: fire|meteor|plasma|noise|aurora") - speed: Optional[float] = Field(None, description="Effect speed multiplier 0.1-10.0", ge=0.1, le=10.0) palette: Optional[str] = Field(None, description="Named palette") intensity: Optional[float] = Field(None, description="Effect intensity 0.1-2.0", ge=0.1, le=2.0) scale: Optional[float] = Field(None, description="Spatial scale 0.5-5.0", ge=0.5, le=5.0) @@ -160,10 +156,8 @@ class ColorStripSourceResponse(BaseModel): stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type") # color_cycle-type fields colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)") - cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier (color_cycle type)") # effect-type fields effect_type: Optional[str] = Field(None, description="Effect algorithm") - speed: Optional[float] = Field(None, description="Effect speed multiplier") palette: Optional[str] = Field(None, description="Named palette") intensity: Optional[float] = Field(None, description="Effect intensity") scale: Optional[float] = Field(None, description="Spatial scale") diff --git a/server/src/wled_controller/core/processing/color_strip_stream.py b/server/src/wled_controller/core/processing/color_strip_stream.py index e0effbe..467577f 100644 --- a/server/src/wled_controller/core/processing/color_strip_stream.py +++ b/server/src/wled_controller/core/processing/color_strip_stream.py @@ -684,7 +684,7 @@ class StaticColorStripStream(ColorStripStream): speed = clock.speed t = clock.get_time() else: - speed = float(anim.get("speed", 1.0)) + speed = 1.0 t = wall_start atype = anim.get("type", "breathing") n = self._led_count @@ -798,7 +798,6 @@ class ColorCycleColorStripStream(ColorStripStream): self._color_list = [ c for c in raw if isinstance(c, list) and len(c) == 3 ] or default - self._cycle_speed = float(source.cycle_speed) if source.cycle_speed else 1.0 self._auto_size = not source.led_count self._led_count = source.led_count if source.led_count > 0 else 1 self._rebuild_colors() @@ -892,7 +891,7 @@ class ColorCycleColorStripStream(ColorStripStream): speed = clock.speed t = clock.get_time() else: - speed = self._cycle_speed + speed = 1.0 t = wall_start n = self._led_count num = len(color_list) @@ -1067,7 +1066,7 @@ class GradientColorStripStream(ColorStripStream): speed = clock.speed t = clock.get_time() else: - speed = float(anim.get("speed", 1.0)) + speed = 1.0 t = wall_start atype = anim.get("type", "breathing") n = self._led_count diff --git a/server/src/wled_controller/core/processing/effect_stream.py b/server/src/wled_controller/core/processing/effect_stream.py index 60afe09..87b06e1 100644 --- a/server/src/wled_controller/core/processing/effect_stream.py +++ b/server/src/wled_controller/core/processing/effect_stream.py @@ -203,7 +203,6 @@ class EffectColorStripStream(ColorStripStream): def _update_from_source(self, source) -> None: self._effect_type = getattr(source, "effect_type", "fire") - self._speed = float(getattr(source, "speed", 1.0)) self._auto_size = not source.led_count self._led_count = source.led_count if source.led_count and source.led_count > 0 else 1 self._palette_name = getattr(source, "palette", None) or _EFFECT_DEFAULT_PALETTE.get(self._effect_type, "fire") @@ -307,7 +306,7 @@ class EffectColorStripStream(ColorStripStream): self._effective_speed = clock.speed else: anim_time = wall_start - self._effective_speed = self._speed + self._effective_speed = 1.0 n = self._led_count if n != _pool_n: diff --git a/server/src/wled_controller/static/js/features/calibration.js b/server/src/wled_controller/static/js/features/calibration.js index 91b5262..3f6fb3e 100644 --- a/server/src/wled_controller/static/js/features/calibration.js +++ b/server/src/wled_controller/static/js/features/calibration.js @@ -3,7 +3,7 @@ */ import { - calibrationTestState, EDGE_TEST_COLORS, + calibrationTestState, EDGE_TEST_COLORS, displaysCache, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -134,9 +134,9 @@ export async function toggleCalibrationOverlay() { export async function showCalibration(deviceId) { try { - const [response, displaysResponse] = await Promise.all([ + const [response, displays] = await Promise.all([ fetchWithAuth(`/devices/${deviceId}`), - fetchWithAuth('/config/displays'), + displaysCache.fetch().catch(() => []), ]); if (!response.ok) { showToast(t('calibration.error.load_failed'), 'error'); return; } @@ -145,15 +145,10 @@ export async function showCalibration(deviceId) { const calibration = device.calibration; const preview = document.querySelector('.calibration-preview'); - if (displaysResponse.ok) { - const displaysData = await displaysResponse.json(); - const displayIndex = device.settings?.display_index ?? 0; - const display = (displaysData.displays || []).find(d => d.index === displayIndex); - if (display && display.width && display.height) { - preview.style.aspectRatio = `${display.width} / ${display.height}`; - } else { - preview.style.aspectRatio = ''; - } + const displayIndex = device.settings?.display_index ?? 0; + const display = displays.find(d => d.index === displayIndex); + if (display && display.width && display.height) { + preview.style.aspectRatio = `${display.width} / ${display.height}`; } else { preview.style.aspectRatio = ''; } diff --git a/server/src/wled_controller/static/js/features/color-strips.js b/server/src/wled_controller/static/js/features/color-strips.js index 3e16cc8..8012cad 100644 --- a/server/src/wled_controller/static/js/features/color-strips.js +++ b/server/src/wled_controller/static/js/features/color-strips.js @@ -3,7 +3,7 @@ */ import { fetchWithAuth, escapeHtml } from '../core/api.js'; -import { _cachedSyncClocks } from '../core/state.js'; +import { _cachedSyncClocks, audioSourcesCache, streamsCache } from '../core/state.js'; import { t } from '../core/i18n.js'; import { showToast, showConfirm } from '../core/ui.js'; import { Modal } from '../core/modal.js'; @@ -12,7 +12,7 @@ import { ICON_CLONE, ICON_EDIT, ICON_CALIBRATION, ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC, ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM, - ICON_LINK, ICON_SPARKLES, ICON_FAST_FORWARD, ICON_ACTIVITY, ICON_CLOCK, + ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, } from '../core/icons.js'; import { wrapCard } from '../core/card-colors.js'; @@ -37,11 +37,8 @@ class CSSEditorModal extends Modal { led_count: document.getElementById('css-editor-led-count').value, gradient_stops: type === 'gradient' ? JSON.stringify(_gradientStops) : '[]', animation_type: document.getElementById('css-editor-animation-type').value, - animation_speed: document.getElementById('css-editor-animation-speed').value, - cycle_speed: document.getElementById('css-editor-cycle-speed').value, cycle_colors: JSON.stringify(_colorCycleColors), effect_type: document.getElementById('css-editor-effect-type').value, - effect_speed: document.getElementById('css-editor-effect-speed').value, effect_palette: document.getElementById('css-editor-effect-palette').value, effect_color: document.getElementById('css-editor-effect-color').value, effect_intensity: document.getElementById('css-editor-effect-intensity').value, @@ -141,16 +138,7 @@ function _populateClockDropdown(selectedId) { } export function onCSSClockChange() { - // When a clock is selected, hide speed sliders (speed comes from clock) - const clockId = document.getElementById('css-editor-clock').value; - const type = document.getElementById('css-editor-type').value; - if (type === 'effect') { - document.getElementById('css-editor-effect-speed-group').style.display = clockId ? 'none' : ''; - } else if (type === 'color_cycle') { - document.getElementById('css-editor-cycle-speed-group').style.display = clockId ? 'none' : ''; - } else if (type === 'static' || type === 'gradient') { - document.getElementById('css-editor-animation-speed-group').style.display = clockId ? 'none' : ''; - } + // No-op: speed sliders removed; speed is now clock-only } function _getAnimationPayload() { @@ -158,15 +146,10 @@ function _getAnimationPayload() { return { enabled: type !== 'none', type: type !== 'none' ? type : 'breathing', - speed: parseFloat(document.getElementById('css-editor-animation-speed').value), }; } function _loadAnimationState(anim) { - const speedEl = document.getElementById('css-editor-animation-speed'); - speedEl.value = (anim && anim.speed != null) ? anim.speed : 1.0; - document.getElementById('css-editor-animation-speed-val').textContent = - parseFloat(speedEl.value).toFixed(1); // Set type after onCSSTypeChange() has populated the dropdown if (anim && anim.enabled && anim.type) { document.getElementById('css-editor-animation-type').value = anim.type; @@ -182,8 +165,6 @@ export function onAnimationTypeChange() { function _syncAnimationSpeedState() { const type = document.getElementById('css-editor-animation-type').value; - const isNone = type === 'none'; - document.getElementById('css-editor-animation-speed').disabled = isNone; const descEl = document.getElementById('css-editor-animation-type-desc'); if (descEl) { const desc = t('color_strip.animation.type.' + type + '.desc') || ''; @@ -302,13 +283,6 @@ function _loadColorCycleState(css) { ? raw.map(c => rgbArrayToHex(c)) : [..._DEFAULT_CYCLE_COLORS]; _colorCycleRenderList(); - const speed = (css && css.cycle_speed != null) ? css.cycle_speed : 1.0; - const speedEl = document.getElementById('css-editor-cycle-speed'); - if (speedEl) { - speedEl.value = speed; - document.getElementById('css-editor-cycle-speed-val').textContent = - parseFloat(speed).toFixed(1); - } } /** Convert an [R, G, B] array to a CSS hex color string like "#rrggbb". */ @@ -553,10 +527,7 @@ async function _loadAudioSources() { const select = document.getElementById('css-editor-audio-source'); if (!select) return; try { - const resp = await fetchWithAuth('/audio-sources'); - if (!resp.ok) return; - const data = await resp.json(); - const sources = data.sources || []; + const sources = await audioSourcesCache.fetch(); select.innerHTML = sources.map(s => { const badge = s.source_type === 'multichannel' ? ' [multichannel]' : ' [mono]'; return ``; @@ -626,7 +597,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { const anim = (isStatic || isGradient) && source.animation && source.animation.enabled ? source.animation : null; const animBadge = anim ? `${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}` - + (source.clock_id ? '' : `${ICON_FAST_FORWARD} ${(anim.speed || 1.0).toFixed(1)}×`) : ''; let propsHtml; @@ -647,7 +617,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { ).join(''); propsHtml = ` ${swatches} - ${source.clock_id ? '' : `${ICON_FAST_FORWARD} ${(source.cycle_speed || 1.0).toFixed(1)}×`} ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} ${clockBadge} `; @@ -680,7 +649,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { propsHtml = ` ${ICON_FPS} ${escapeHtml(effectLabel)} ${paletteLabel ? `${ICON_PALETTE} ${escapeHtml(paletteLabel)}` : ''} - ${source.clock_id ? '' : `${ICON_FAST_FORWARD} ${(source.speed || 1.0).toFixed(1)}×`} ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} ${clockBadge} `; @@ -771,8 +739,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { export async function showCSSEditor(cssId = null, cloneData = null) { try { - const sourcesResp = await fetchWithAuth('/picture-sources'); - const sources = sourcesResp.ok ? ((await sourcesResp.json()).streams || []) : []; + const sources = await streamsCache.fetch(); // Fetch all color strip sources for composite layer dropdowns const cssListResp = await fetchWithAuth('/color-strip-sources'); @@ -821,8 +788,6 @@ export async function showCSSEditor(cssId = null, cloneData = null) { } else if (sourceType === 'effect') { document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire'; onEffectTypeChange(); - document.getElementById('css-editor-effect-speed').value = css.speed ?? 1.0; - document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1); document.getElementById('css-editor-effect-palette').value = css.palette || 'fire'; document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]); document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0; @@ -920,8 +885,6 @@ export async function showCSSEditor(cssId = null, cloneData = null) { _loadAnimationState(null); _loadColorCycleState(null); document.getElementById('css-editor-effect-type').value = 'fire'; - document.getElementById('css-editor-effect-speed').value = 1.0; - document.getElementById('css-editor-effect-speed-val').textContent = '1.0'; document.getElementById('css-editor-effect-palette').value = 'fire'; document.getElementById('css-editor-effect-color').value = '#ff5000'; document.getElementById('css-editor-effect-intensity').value = 1.0; @@ -988,7 +951,6 @@ export async function saveCSSEditor() { payload = { name, colors: cycleColors, - cycle_speed: parseFloat(document.getElementById('css-editor-cycle-speed').value), }; if (!cssId) payload.source_type = 'color_cycle'; } else if (sourceType === 'gradient') { @@ -1010,7 +972,6 @@ export async function saveCSSEditor() { payload = { name, effect_type: document.getElementById('css-editor-effect-type').value, - speed: parseFloat(document.getElementById('css-editor-effect-speed').value), palette: document.getElementById('css-editor-effect-palette').value, intensity: parseFloat(document.getElementById('css-editor-effect-intensity').value), scale: parseFloat(document.getElementById('css-editor-effect-scale').value), diff --git a/server/src/wled_controller/static/js/features/devices.js b/server/src/wled_controller/static/js/features/devices.js index de7abc8..9401c3a 100644 --- a/server/src/wled_controller/static/js/features/devices.js +++ b/server/src/wled_controller/static/js/features/devices.js @@ -28,7 +28,7 @@ class DeviceSettingsModal extends Modal { led_count: this.$('settings-led-count').value, led_type: document.getElementById('settings-led-type')?.value || 'rgb', send_latency: document.getElementById('settings-send-latency')?.value || '0', - zones: _getCheckedZones('settings-zone-list'), + zones: JSON.stringify(_getCheckedZones('settings-zone-list')), zoneMode: _getZoneMode('settings-zone-mode'), }; } diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js index 077e230..4f9bcac 100644 --- a/server/src/wled_controller/static/js/features/kc-targets.js +++ b/server/src/wled_controller/static/js/features/kc-targets.js @@ -8,7 +8,7 @@ import { _kcNameManuallyEdited, set_kcNameManuallyEdited, kcWebSockets, PATTERN_RECT_BORDERS, - _cachedValueSources, valueSourcesCache, + _cachedValueSources, valueSourcesCache, streamsCache, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -413,12 +413,11 @@ function _populateKCBrightnessVsDropdown(selectedId = '') { export async function showKCEditor(targetId = null, cloneData = null) { try { // Load sources, pattern templates, and value sources in parallel - const [sourcesResp, patResp, valueSources] = await Promise.all([ - fetchWithAuth('/picture-sources').catch(() => null), + const [sources, patResp, valueSources] = await Promise.all([ + streamsCache.fetch().catch(() => []), fetchWithAuth('/pattern-templates').catch(() => null), valueSourcesCache.fetch(), ]); - const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : []; const patTemplates = (patResp && patResp.ok) ? (await patResp.json()).templates || [] : []; // Populate source select diff --git a/server/src/wled_controller/static/js/features/pattern-templates.js b/server/src/wled_controller/static/js/features/pattern-templates.js index b887f7d..f7ade7c 100644 --- a/server/src/wled_controller/static/js/features/pattern-templates.js +++ b/server/src/wled_controller/static/js/features/pattern-templates.js @@ -13,6 +13,7 @@ import { patternEditorHoverHit, setPatternEditorHoverHit, PATTERN_RECT_COLORS, PATTERN_RECT_BORDERS, + streamsCache, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -76,8 +77,7 @@ export function createPatternTemplateCard(pt) { export async function showPatternTemplateEditor(templateId = null, cloneData = null) { try { // Load sources for background capture - const sourcesResp = await fetchWithAuth('/picture-sources').catch(() => null); - const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : []; + const sources = await streamsCache.fetch().catch(() => []); const bgSelect = document.getElementById('pattern-bg-source'); bgSelect.innerHTML = ''; diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index f9001c6..20eddff 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -247,10 +247,8 @@ function restoreCaptureDuration() { export async function showTestTemplateModal(templateId) { try { - const resp = await fetchWithAuth('/capture-templates'); - if (!resp.ok) throw new Error(`HTTP ${resp.status}`); - const data = await resp.json(); - const template = (data.templates || []).find(tp => tp.id === templateId); + const templates = await captureTemplatesCache.fetch(); + const template = templates.find(tp => tp.id === templateId); if (!template) { showToast(t('templates.error.load'), 'error'); @@ -1597,34 +1595,25 @@ export async function editStream(streamId) { let _streamModalDisplaysEngine = null; async function populateStreamModalDropdowns() { - const [displaysRes, captureTemplatesRes, streamsRes, ppTemplatesRes] = await Promise.all([ - fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }), - fetchWithAuth('/capture-templates'), - fetchWithAuth('/picture-sources'), - fetchWithAuth('/postprocessing-templates'), + const [captureTemplates, streams, ppTemplates] = await Promise.all([ + captureTemplatesCache.fetch().catch(() => []), + streamsCache.fetch().catch(() => []), + ppTemplatesCache.fetch().catch(() => []), + displaysCache.fetch().catch(() => []), ]); - - // Cache desktop displays (used as default unless engine has own displays) - if (displaysRes.ok) { - const displaysData = await displaysRes.json(); - displaysCache.update(displaysData.displays || []); - } _streamModalDisplaysEngine = null; const templateSelect = document.getElementById('stream-capture-template'); templateSelect.innerHTML = ''; - if (captureTemplatesRes.ok) { - const data = await captureTemplatesRes.json(); - (data.templates || []).forEach(tmpl => { - const opt = document.createElement('option'); - opt.value = tmpl.id; - opt.dataset.name = tmpl.name; - opt.dataset.engineType = tmpl.engine_type; - opt.dataset.hasOwnDisplays = availableEngines.find(e => e.type === tmpl.engine_type)?.has_own_displays ? '1' : ''; - opt.textContent = `${tmpl.name} (${tmpl.engine_type})`; - templateSelect.appendChild(opt); - }); - } + captureTemplates.forEach(tmpl => { + const opt = document.createElement('option'); + opt.value = tmpl.id; + opt.dataset.name = tmpl.name; + opt.dataset.engineType = tmpl.engine_type; + opt.dataset.hasOwnDisplays = availableEngines.find(e => e.type === tmpl.engine_type)?.has_own_displays ? '1' : ''; + opt.textContent = `${tmpl.name} (${tmpl.engine_type})`; + templateSelect.appendChild(opt); + }); // When template changes, refresh displays if engine type switched templateSelect.addEventListener('change', _onCaptureTemplateChanged); @@ -1640,32 +1629,25 @@ async function populateStreamModalDropdowns() { const sourceSelect = document.getElementById('stream-source'); sourceSelect.innerHTML = ''; - if (streamsRes.ok) { - const data = await streamsRes.json(); - const editingId = document.getElementById('stream-id').value; - (data.streams || []).forEach(s => { - if (s.id === editingId) return; - const opt = document.createElement('option'); - opt.value = s.id; - opt.dataset.name = s.name; - opt.textContent = s.name; - sourceSelect.appendChild(opt); - }); - } + const editingId = document.getElementById('stream-id').value; + streams.forEach(s => { + if (s.id === editingId) return; + const opt = document.createElement('option'); + opt.value = s.id; + opt.dataset.name = s.name; + opt.textContent = s.name; + sourceSelect.appendChild(opt); + }); - set_streamModalPPTemplates([]); + set_streamModalPPTemplates(ppTemplates); const ppSelect = document.getElementById('stream-pp-template'); ppSelect.innerHTML = ''; - if (ppTemplatesRes.ok) { - const data = await ppTemplatesRes.json(); - set_streamModalPPTemplates(data.templates || []); - _streamModalPPTemplates.forEach(tmpl => { - const opt = document.createElement('option'); - opt.value = tmpl.id; - opt.textContent = tmpl.name; - ppSelect.appendChild(opt); - }); - } + ppTemplates.forEach(tmpl => { + const opt = document.createElement('option'); + opt.value = tmpl.id; + opt.textContent = tmpl.name; + ppSelect.appendChild(opt); + }); _autoGenerateStreamName(); } diff --git a/server/src/wled_controller/static/sw.js b/server/src/wled_controller/static/sw.js index 75ff449..e4c11e5 100644 --- a/server/src/wled_controller/static/sw.js +++ b/server/src/wled_controller/static/sw.js @@ -7,7 +7,7 @@ * - Navigation: network-first with offline fallback */ -const CACHE_NAME = 'ledgrab-v7'; +const CACHE_NAME = 'ledgrab-v8'; // Only pre-cache static assets (no auth required). // Do NOT pre-cache '/' — it requires API key auth and would cache an error page. diff --git a/server/src/wled_controller/storage/color_strip_source.py b/server/src/wled_controller/storage/color_strip_source.py index 33fa5ab..133e2d6 100644 --- a/server/src/wled_controller/storage/color_strip_source.py +++ b/server/src/wled_controller/storage/color_strip_source.py @@ -69,7 +69,6 @@ class ColorStripSource: "stops": None, "animation": None, "colors": None, - "cycle_speed": None, "effect_type": None, "palette": None, "intensity": None, @@ -149,7 +148,6 @@ class ColorStripSource: id=sid, name=name, source_type="color_cycle", created_at=created_at, updated_at=updated_at, description=description, clock_id=clock_id, colors=colors, - cycle_speed=float(data.get("cycle_speed") or 1.0), led_count=data.get("led_count") or 0, ) @@ -198,7 +196,6 @@ class ColorStripSource: id=sid, name=name, source_type="effect", created_at=created_at, updated_at=updated_at, description=description, clock_id=clock_id, effect_type=data.get("effect_type") or "fire", - speed=float(data.get("speed") or 1.0), led_count=data.get("led_count") or 0, palette=data.get("palette") or "fire", color=color, @@ -340,13 +337,11 @@ class ColorCycleColorStripSource(ColorStripSource): [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], ]) - cycle_speed: float = 1.0 # speed multiplier; 1.0 ≈ one full cycle every 20 seconds led_count: int = 0 # 0 = use device LED count def to_dict(self) -> dict: d = super().to_dict() d["colors"] = [list(c) for c in self.colors] - d["cycle_speed"] = self.cycle_speed d["led_count"] = self.led_count return d @@ -361,7 +356,6 @@ class EffectColorStripSource(ColorStripSource): """ effect_type: str = "fire" # fire | meteor | plasma | noise | aurora - speed: float = 1.0 # animation speed multiplier (0.1–10.0) led_count: int = 0 # 0 = use device LED count palette: str = "fire" # named color palette color: list = field(default_factory=lambda: [255, 80, 0]) # [R,G,B] for meteor head @@ -372,7 +366,6 @@ class EffectColorStripSource(ColorStripSource): def to_dict(self) -> dict: d = super().to_dict() d["effect_type"] = self.effect_type - d["speed"] = self.speed d["led_count"] = self.led_count d["palette"] = self.palette d["color"] = list(self.color) diff --git a/server/src/wled_controller/storage/color_strip_store.py b/server/src/wled_controller/storage/color_strip_store.py index 3624ef0..c617be0 100644 --- a/server/src/wled_controller/storage/color_strip_store.py +++ b/server/src/wled_controller/storage/color_strip_store.py @@ -106,9 +106,7 @@ class ColorStripStore: frame_interpolation: bool = False, animation: Optional[dict] = None, colors: Optional[list] = None, - cycle_speed: float = 1.0, effect_type: str = "fire", - speed: float = 1.0, palette: str = "fire", intensity: float = 1.0, scale: float = 1.0, @@ -182,7 +180,6 @@ class ColorStripStore: description=description, clock_id=clock_id, colors=colors if isinstance(colors, list) and len(colors) >= 2 else default_colors, - cycle_speed=float(cycle_speed) if cycle_speed else 1.0, led_count=led_count, ) elif source_type == "effect": @@ -196,7 +193,6 @@ class ColorStripStore: description=description, clock_id=clock_id, effect_type=effect_type or "fire", - speed=float(speed) if speed else 1.0, led_count=led_count, palette=palette or "fire", color=rgb, @@ -311,9 +307,7 @@ class ColorStripStore: frame_interpolation: Optional[bool] = None, animation: Optional[dict] = None, colors: Optional[list] = None, - cycle_speed: Optional[float] = None, effect_type: Optional[str] = None, - speed: Optional[float] = None, palette: Optional[str] = None, intensity: Optional[float] = None, scale: Optional[float] = None, @@ -389,15 +383,11 @@ class ColorStripStore: elif isinstance(source, ColorCycleColorStripSource): if colors is not None and isinstance(colors, list) and len(colors) >= 2: source.colors = colors - if cycle_speed is not None: - source.cycle_speed = float(cycle_speed) if led_count is not None: source.led_count = led_count elif isinstance(source, EffectColorStripSource): if effect_type is not None: source.effect_type = effect_type - if speed is not None: - source.speed = float(speed) if led_count is not None: source.led_count = led_count if palette is not None: diff --git a/server/src/wled_controller/templates/modals/css-editor.html b/server/src/wled_controller/templates/modals/css-editor.html index 7dbf11e..2c39c4d 100644 --- a/server/src/wled_controller/templates/modals/css-editor.html +++ b/server/src/wled_controller/templates/modals/css-editor.html @@ -146,18 +146,6 @@
-
-
- - -
- - -
@@ -226,19 +214,6 @@
-
-
- - -
- - -
-
@@ -492,18 +467,6 @@
-
-
- - -
- - -
diff --git a/server/src/wled_controller/templates/modals/sync-clock-editor.html b/server/src/wled_controller/templates/modals/sync-clock-editor.html index 1770467..3850b5e 100644 --- a/server/src/wled_controller/templates/modals/sync-clock-editor.html +++ b/server/src/wled_controller/templates/modals/sync-clock-editor.html @@ -39,7 +39,7 @@ - +