Improve template cards UI and fix template editing bugs
Some checks failed
Validate / validate (push) Failing after 7s
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:
@@ -794,8 +794,9 @@ async def get_template(
|
|||||||
template_store: TemplateStore = Depends(get_template_store),
|
template_store: TemplateStore = Depends(get_template_store),
|
||||||
):
|
):
|
||||||
"""Get template by ID."""
|
"""Get template by ID."""
|
||||||
template = template_store.get_template(template_id)
|
try:
|
||||||
if not template:
|
template = template_store.get_template(template_id)
|
||||||
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail=f"Template {template_id} not found")
|
raise HTTPException(status_code=404, detail=f"Template {template_id} not found")
|
||||||
|
|
||||||
return TemplateResponse(
|
return TemplateResponse(
|
||||||
@@ -822,6 +823,7 @@ async def update_template(
|
|||||||
template = template_store.update_template(
|
template = template_store.update_template(
|
||||||
template_id=template_id,
|
template_id=template_id,
|
||||||
name=update_data.name,
|
name=update_data.name,
|
||||||
|
engine_type=update_data.engine_type,
|
||||||
engine_config=update_data.engine_config,
|
engine_config=update_data.engine_config,
|
||||||
description=update_data.description,
|
description=update_data.description,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ class TemplateUpdate(BaseModel):
|
|||||||
"""Request to update a template."""
|
"""Request to update a template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
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")
|
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
|
|
||||||
|
|||||||
@@ -563,7 +563,6 @@ function createDeviceCard(device) {
|
|||||||
healthLabel = `<span class="health-latency offline">${t('device.health.offline')}</span>`;
|
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;
|
const ledCount = state.wled_led_count || device.led_count;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -573,22 +572,18 @@ function createDeviceCard(device) {
|
|||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
||||||
${device.name || device.id}
|
${device.name || device.id}
|
||||||
|
${device.url ? `<span class="device-url-badge" title="${escapeHtml(device.url)}">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span>` : ''}
|
||||||
${healthLabel}
|
${healthLabel}
|
||||||
${isProcessing ? `<span class="badge processing">${t('device.status.processing')}</span>` : ''}
|
${isProcessing ? `<span class="badge processing">${t('device.status.processing')}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-subtitle">
|
<div class="card-subtitle">
|
||||||
${wledVersion ? `<span class="card-meta">v${wledVersion}</span>` : ''}
|
${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>` : ''}
|
${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>` : ''}
|
${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>
|
<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>
|
||||||
<div class="card-content">
|
<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 ? `
|
${isProcessing ? `
|
||||||
<div class="metrics-grid">
|
<div class="metrics-grid">
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
@@ -2357,6 +2352,9 @@ function renderTemplatesList(templates) {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="template-card" data-template-id="${template.id}">
|
<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')}">✕</button>
|
||||||
|
` : ''}
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<div class="template-name">
|
<div class="template-name">
|
||||||
${engineIcon} ${escapeHtml(template.name)}
|
${engineIcon} ${escapeHtml(template.name)}
|
||||||
@@ -2369,7 +2367,14 @@ function renderTemplatesList(templates) {
|
|||||||
${Object.keys(template.engine_config).length > 0 ? `
|
${Object.keys(template.engine_config).length > 0 ? `
|
||||||
<details class="template-config-details">
|
<details class="template-config-details">
|
||||||
<summary>${t('templates.config.show')}</summary>
|
<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>
|
</details>
|
||||||
` : `
|
` : `
|
||||||
<div class="template-no-config">${t('templates.config.none')}</div>
|
<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 class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">
|
||||||
✏️
|
✏️
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-icon btn-danger" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">
|
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2463,7 +2465,8 @@ async function editTemplate(templateId) {
|
|||||||
// Load displays for test
|
// Load displays for test
|
||||||
await loadDisplaysForTest();
|
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';
|
document.getElementById('template-error').style.display = 'none';
|
||||||
|
|
||||||
// Show modal
|
// Show modal
|
||||||
@@ -2789,17 +2792,27 @@ async function loadDisplaysForTest() {
|
|||||||
|
|
||||||
const displaysData = await response.json();
|
const displaysData = await response.json();
|
||||||
const select = document.getElementById('test-template-display');
|
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 => {
|
(displaysData.displays || []).forEach(display => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = display.index;
|
option.value = display.index;
|
||||||
option.textContent = `Display ${display.index} (${display.width}x${display.height})`;
|
option.textContent = `Display ${display.index} (${display.width}x${display.height})`;
|
||||||
if (display.is_primary) {
|
if (display.is_primary) {
|
||||||
option.textContent += ' ★';
|
option.textContent += ' ★';
|
||||||
|
primaryIndex = display.index;
|
||||||
}
|
}
|
||||||
select.appendChild(option);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading displays:', error);
|
console.error('Error loading displays:', error);
|
||||||
}
|
}
|
||||||
@@ -2843,6 +2856,7 @@ async function runTemplateTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
localStorage.setItem('lastTestDisplayIndex', displayIndex);
|
||||||
displayTestResults(result);
|
displayTestResults(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error running test:', error);
|
console.error('Error running test:', error);
|
||||||
|
|||||||
@@ -282,6 +282,17 @@ section {
|
|||||||
flex-wrap: wrap;
|
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 {
|
.card-subtitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1737,6 +1748,7 @@ input:-webkit-autofill:focus {
|
|||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-card:hover {
|
.template-card:hover {
|
||||||
@@ -1785,6 +1797,7 @@ input:-webkit-autofill:focus {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
padding-right: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-name {
|
.template-name {
|
||||||
@@ -1843,15 +1856,31 @@ input:-webkit-autofill:focus {
|
|||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-config-details pre {
|
.config-table {
|
||||||
background: var(--bg-secondary);
|
width: 100%;
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
overflow-x: auto;
|
border-collapse: collapse;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
}
|
||||||
|
|
||||||
|
.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 {
|
.template-card-actions {
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ class TemplateStore:
|
|||||||
self,
|
self,
|
||||||
template_id: str,
|
template_id: str,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
engine_type: Optional[str] = None,
|
||||||
engine_config: Optional[Dict[str, any]] = None,
|
engine_config: Optional[Dict[str, any]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
) -> CaptureTemplate:
|
) -> CaptureTemplate:
|
||||||
@@ -200,6 +201,7 @@ class TemplateStore:
|
|||||||
Args:
|
Args:
|
||||||
template_id: Template ID
|
template_id: Template ID
|
||||||
name: New name (optional)
|
name: New name (optional)
|
||||||
|
engine_type: New engine type (optional)
|
||||||
engine_config: New engine config (optional)
|
engine_config: New engine config (optional)
|
||||||
description: New description (optional)
|
description: New description (optional)
|
||||||
|
|
||||||
@@ -220,6 +222,8 @@ class TemplateStore:
|
|||||||
# Update fields
|
# Update fields
|
||||||
if name is not None:
|
if name is not None:
|
||||||
template.name = name
|
template.name = name
|
||||||
|
if engine_type is not None:
|
||||||
|
template.engine_type = engine_type
|
||||||
if engine_config is not None:
|
if engine_config is not None:
|
||||||
template.engine_config = engine_config
|
template.engine_config = engine_config
|
||||||
if description is not None:
|
if description is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user