From e3208e0ca2358d9f07cbcc4e9a69290334b12c9a Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Tue, 10 Feb 2026 03:00:47 +0300 Subject: [PATCH] Improve template cards UI and fix template editing bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move delete button to cross (✕) at top-right corner of custom template cards - Display template config as table instead of raw JSON - Add engine_type to TemplateUpdate schema so engine changes are saved - Fix editTemplate crash on missing template-test-results element - Fix get_template route to catch ValueError for 404 responses - Move device URL to pill badge next to device name - Remove display index indicator from device cards - Remember last used display in Test Capture via localStorage Co-Authored-By: Claude Opus 4.6 --- server/src/wled_controller/api/routes.py | 6 ++- server/src/wled_controller/api/schemas.py | 1 + server/src/wled_controller/static/app.js | 38 +++++++++++----- server/src/wled_controller/static/style.css | 45 +++++++++++++++---- .../wled_controller/storage/template_store.py | 4 ++ 5 files changed, 72 insertions(+), 22 deletions(-) diff --git a/server/src/wled_controller/api/routes.py b/server/src/wled_controller/api/routes.py index ffa5d5a..9fc93f3 100644 --- a/server/src/wled_controller/api/routes.py +++ b/server/src/wled_controller/api/routes.py @@ -794,8 +794,9 @@ async def get_template( template_store: TemplateStore = Depends(get_template_store), ): """Get template by ID.""" - template = template_store.get_template(template_id) - if not template: + try: + template = template_store.get_template(template_id) + except ValueError: raise HTTPException(status_code=404, detail=f"Template {template_id} not found") return TemplateResponse( @@ -822,6 +823,7 @@ async def update_template( template = template_store.update_template( template_id=template_id, name=update_data.name, + engine_type=update_data.engine_type, engine_config=update_data.engine_config, description=update_data.description, ) diff --git a/server/src/wled_controller/api/schemas.py b/server/src/wled_controller/api/schemas.py index 98bf3c6..030aca6 100644 --- a/server/src/wled_controller/api/schemas.py +++ b/server/src/wled_controller/api/schemas.py @@ -230,6 +230,7 @@ class TemplateUpdate(BaseModel): """Request to update a template.""" name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100) + engine_type: Optional[str] = Field(None, description="Capture engine type (mss, dxcam, wgc)") engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration") description: Optional[str] = Field(None, description="Template description", max_length=500) diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index 6a3167d..461f3be 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -563,7 +563,6 @@ function createDeviceCard(device) { healthLabel = `${t('device.health.offline')}`; } - const displayIndex = settings.display_index !== undefined ? settings.display_index : 0; const ledCount = state.wled_led_count || device.led_count; return ` @@ -573,22 +572,18 @@ function createDeviceCard(device) {
${device.name || device.id} + ${device.url ? `${escapeHtml(device.url.replace(/^https?:\/\//, ''))}` : ''} ${healthLabel} ${isProcessing ? `${t('device.status.processing')}` : ''}
${wledVersion ? `v${wledVersion}` : ''} - 🖥️ ${displayIndex} ${ledCount ? `💡 ${ledCount}` : ''} ${state.wled_led_type ? `🔌 ${state.wled_led_type.replace(/ RGBW$/, '')}` : ''} ${state.wled_rgbw ? '' : ''}
-
- ${t('device.url')} - ${device.url || 'N/A'} -
${isProcessing ? `
@@ -2357,6 +2352,9 @@ function renderTemplatesList(templates) { return `
+ ${!template.is_default ? ` + + ` : ''}
${engineIcon} ${escapeHtml(template.name)} @@ -2369,7 +2367,14 @@ function renderTemplatesList(templates) { ${Object.keys(template.engine_config).length > 0 ? `
${t('templates.config.show')} -
${JSON.stringify(template.engine_config, null, 2)}
+ + ${Object.entries(template.engine_config).map(([key, val]) => ` + + + + + `).join('')} +
${escapeHtml(key)}${escapeHtml(String(val))}
` : `
${t('templates.config.none')}
@@ -2382,9 +2387,6 @@ function renderTemplatesList(templates) { - ` : ''}
@@ -2463,7 +2465,8 @@ async function editTemplate(templateId) { // Load displays for test await loadDisplaysForTest(); - document.getElementById('template-test-results').style.display = 'none'; + const testResults = document.getElementById('template-test-results'); + if (testResults) testResults.style.display = 'none'; document.getElementById('template-error').style.display = 'none'; // Show modal @@ -2789,17 +2792,27 @@ async function loadDisplaysForTest() { const displaysData = await response.json(); const select = document.getElementById('test-template-display'); - select.innerHTML = ``; + select.innerHTML = ''; + let primaryIndex = null; (displaysData.displays || []).forEach(display => { const option = document.createElement('option'); option.value = display.index; option.textContent = `Display ${display.index} (${display.width}x${display.height})`; if (display.is_primary) { option.textContent += ' ★'; + primaryIndex = display.index; } select.appendChild(option); }); + + // Auto-select: last used display, or primary as fallback + const lastDisplay = localStorage.getItem('lastTestDisplayIndex'); + if (lastDisplay !== null && select.querySelector(`option[value="${lastDisplay}"]`)) { + select.value = lastDisplay; + } else if (primaryIndex !== null) { + select.value = String(primaryIndex); + } } catch (error) { console.error('Error loading displays:', error); } @@ -2843,6 +2856,7 @@ async function runTemplateTest() { } const result = await response.json(); + localStorage.setItem('lastTestDisplayIndex', displayIndex); displayTestResults(result); } catch (error) { console.error('Error running test:', error); diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css index b10a0f5..62bd399 100644 --- a/server/src/wled_controller/static/style.css +++ b/server/src/wled_controller/static/style.css @@ -282,6 +282,17 @@ section { flex-wrap: wrap; } +.device-url-badge { + font-size: 0.7rem; + font-weight: 400; + color: var(--text-secondary); + background: var(--border-color); + padding: 2px 8px; + border-radius: 10px; + letter-spacing: 0.03em; + font-family: monospace; +} + .card-subtitle { display: flex; align-items: center; @@ -1737,6 +1748,7 @@ input:-webkit-autofill:focus { transition: box-shadow 0.2s; display: flex; flex-direction: column; + position: relative; } .template-card:hover { @@ -1785,6 +1797,7 @@ input:-webkit-autofill:focus { justify-content: space-between; align-items: center; margin-bottom: 12px; + padding-right: 24px; } .template-name { @@ -1843,15 +1856,31 @@ input:-webkit-autofill:focus { padding: 4px 0; } -.template-config-details pre { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 12px; +.config-table { + width: 100%; margin-top: 8px; - overflow-x: auto; - font-size: 12px; - line-height: 1.5; + border-collapse: collapse; + font-size: 13px; +} + +.config-table td { + padding: 4px 8px; + border-bottom: 1px solid var(--border-color); +} + +.config-table tr:last-child td { + border-bottom: none; +} + +.config-key { + color: var(--text-secondary); + white-space: nowrap; + width: 1%; +} + +.config-value { + color: var(--text-primary); + font-family: monospace; } .template-card-actions { diff --git a/server/src/wled_controller/storage/template_store.py b/server/src/wled_controller/storage/template_store.py index 6f6b0e7..9f8710b 100644 --- a/server/src/wled_controller/storage/template_store.py +++ b/server/src/wled_controller/storage/template_store.py @@ -192,6 +192,7 @@ class TemplateStore: self, template_id: str, name: Optional[str] = None, + engine_type: Optional[str] = None, engine_config: Optional[Dict[str, any]] = None, description: Optional[str] = None, ) -> CaptureTemplate: @@ -200,6 +201,7 @@ class TemplateStore: Args: template_id: Template ID name: New name (optional) + engine_type: New engine type (optional) engine_config: New engine config (optional) description: New description (optional) @@ -220,6 +222,8 @@ class TemplateStore: # Update fields if name is not None: template.name = name + if engine_type is not None: + template.engine_type = engine_type if engine_config is not None: template.engine_config = engine_config if description is not None: