Improve template cards UI and fix template editing bugs
Some checks failed
Validate / validate (push) Failing after 7s

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 03:00:47 +03:00
parent 5370d80466
commit e3208e0ca2
5 changed files with 72 additions and 22 deletions

View File

@@ -563,7 +563,6 @@ function createDeviceCard(device) {
healthLabel = `<span class="health-latency offline">${t('device.health.offline')}</span>`;
}
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) {
<div class="card-title">
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
${device.name || device.id}
${device.url ? `<span class="device-url-badge" title="${escapeHtml(device.url)}">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span>` : ''}
${healthLabel}
${isProcessing ? `<span class="badge processing">${t('device.status.processing')}</span>` : ''}
</div>
</div>
<div class="card-subtitle">
${wledVersion ? `<span class="card-meta">v${wledVersion}</span>` : ''}
<span class="card-meta" title="${t('device.display')}">🖥️ ${displayIndex}</span>
${ledCount ? `<span class="card-meta" title="${t('device.led_count')}">💡 ${ledCount}</span>` : ''}
${state.wled_led_type ? `<span class="card-meta">🔌 ${state.wled_led_type.replace(/ RGBW$/, '')}</span>` : ''}
<span class="card-meta" title="${state.wled_rgbw ? 'RGBW' : 'RGB'}"><span class="channel-indicator"><span class="ch" style="background:#e53935"></span><span class="ch" style="background:#43a047"></span><span class="ch" style="background:#1e88e5"></span>${state.wled_rgbw ? '<span class="ch" style="background:#eee"></span>' : ''}</span></span>
</div>
<div class="card-content">
<div class="info-row">
<span class="info-label">${t('device.url')}</span>
<span class="info-value">${device.url || 'N/A'}</span>
</div>
${isProcessing ? `
<div class="metrics-grid">
<div class="metric">
@@ -2357,6 +2352,9 @@ function renderTemplatesList(templates) {
return `
<div class="template-card" data-template-id="${template.id}">
${!template.is_default ? `
<button class="card-remove-btn" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">&#x2715;</button>
` : ''}
<div class="template-card-header">
<div class="template-name">
${engineIcon} ${escapeHtml(template.name)}
@@ -2369,7 +2367,14 @@ function renderTemplatesList(templates) {
${Object.keys(template.engine_config).length > 0 ? `
<details class="template-config-details">
<summary>${t('templates.config.show')}</summary>
<pre>${JSON.stringify(template.engine_config, null, 2)}</pre>
<table class="config-table">
${Object.entries(template.engine_config).map(([key, val]) => `
<tr>
<td class="config-key">${escapeHtml(key)}</td>
<td class="config-value">${escapeHtml(String(val))}</td>
</tr>
`).join('')}
</table>
</details>
` : `
<div class="template-no-config">${t('templates.config.none')}</div>
@@ -2382,9 +2387,6 @@ function renderTemplatesList(templates) {
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">
✏️
</button>
<button class="btn btn-icon btn-danger" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">
🗑️
</button>
` : ''}
</div>
</div>
@@ -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 = `<option value="">${t('templates.test.display.select')}</option>`;
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);

View File

@@ -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 {