Improve WGC cleanup, add capture duration persistence, simplify test errors
Some checks failed
Validate / validate (push) Failing after 9s
Some checks failed
Validate / validate (push) Failing after 9s
- Add WGC engine with aggressive COM object cleanup (multiple GC passes) - Persist capture test duration to localStorage - Show only short error messages in snackbar (details in console) Note: WGC cleanup still has issues with yellow border persistence - needs further investigation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -164,12 +164,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Show content now that translations are loaded
|
||||
document.body.style.visibility = 'visible';
|
||||
|
||||
// Restore active tab
|
||||
initTabs();
|
||||
|
||||
// Load API key from localStorage
|
||||
apiKey = localStorage.getItem('wled_api_key');
|
||||
|
||||
// Restore active tab (after API key is loaded)
|
||||
initTabs();
|
||||
|
||||
// Setup form handler
|
||||
document.getElementById('add-device-form').addEventListener('submit', handleAddDevice);
|
||||
|
||||
@@ -206,6 +206,31 @@ function getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Fetch wrapper that automatically includes auth headers
|
||||
async function fetchWithAuth(url, options = {}) {
|
||||
// Build full URL if relative path provided
|
||||
const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`;
|
||||
|
||||
// Merge auth headers with any custom headers
|
||||
const headers = options.headers
|
||||
? { ...getHeaders(), ...options.headers }
|
||||
: getHeaders();
|
||||
|
||||
// Make request with merged options
|
||||
return fetch(fullUrl, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Handle 401 errors by showing login modal
|
||||
function handle401Error() {
|
||||
// Clear invalid API key
|
||||
@@ -319,6 +344,9 @@ function switchTab(name) {
|
||||
if (name === 'displays' && _cachedDisplays) {
|
||||
requestAnimationFrame(() => renderDisplayLayout(_cachedDisplays));
|
||||
}
|
||||
if (name === 'templates') {
|
||||
loadCaptureTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
@@ -709,10 +737,11 @@ async function removeDevice(deviceId) {
|
||||
|
||||
async function showSettings(deviceId) {
|
||||
try {
|
||||
// Fetch device data and displays in parallel
|
||||
const [deviceResponse, displaysResponse] = await Promise.all([
|
||||
// Fetch device data, displays, and templates in parallel
|
||||
const [deviceResponse, displaysResponse, templatesResponse] = await Promise.all([
|
||||
fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
]);
|
||||
|
||||
if (deviceResponse.status === 401) {
|
||||
@@ -747,6 +776,27 @@ async function showSettings(deviceId) {
|
||||
}
|
||||
displaySelect.value = String(device.settings.display_index ?? 0);
|
||||
|
||||
// Populate capture template select
|
||||
const templateSelect = document.getElementById('settings-capture-template');
|
||||
templateSelect.innerHTML = '';
|
||||
if (templatesResponse.ok) {
|
||||
const templatesData = await templatesResponse.json();
|
||||
(templatesData.templates || []).forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.id;
|
||||
const engineIcon = getEngineIcon(t.engine_type);
|
||||
opt.textContent = `${engineIcon} ${t.name}`;
|
||||
templateSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
if (templateSelect.options.length === 0) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = 'tpl_mss_default';
|
||||
opt.textContent = 'MSS (Default)';
|
||||
templateSelect.appendChild(opt);
|
||||
}
|
||||
templateSelect.value = device.capture_template_id || 'tpl_mss_default';
|
||||
|
||||
// Populate other fields
|
||||
document.getElementById('settings-device-id').value = device.id;
|
||||
document.getElementById('settings-device-name').value = device.name;
|
||||
@@ -759,6 +809,7 @@ async function showSettings(deviceId) {
|
||||
url: device.url,
|
||||
display_index: String(device.settings.display_index ?? 0),
|
||||
state_check_interval: String(device.settings.state_check_interval || 30),
|
||||
capture_template_id: device.capture_template_id || 'tpl_mss_default',
|
||||
};
|
||||
|
||||
// Show modal
|
||||
@@ -782,7 +833,8 @@ function isSettingsDirty() {
|
||||
document.getElementById('settings-device-name').value !== settingsInitialValues.name ||
|
||||
document.getElementById('settings-device-url').value !== settingsInitialValues.url ||
|
||||
document.getElementById('settings-display-index').value !== settingsInitialValues.display_index ||
|
||||
document.getElementById('settings-health-interval').value !== settingsInitialValues.state_check_interval
|
||||
document.getElementById('settings-health-interval').value !== settingsInitialValues.state_check_interval ||
|
||||
document.getElementById('settings-capture-template').value !== settingsInitialValues.capture_template_id
|
||||
);
|
||||
}
|
||||
|
||||
@@ -809,6 +861,7 @@ async function saveDeviceSettings() {
|
||||
const url = document.getElementById('settings-device-url').value.trim();
|
||||
const display_index = parseInt(document.getElementById('settings-display-index').value) || 0;
|
||||
const state_check_interval = parseInt(document.getElementById('settings-health-interval').value) || 30;
|
||||
const capture_template_id = document.getElementById('settings-capture-template').value;
|
||||
const error = document.getElementById('settings-error');
|
||||
|
||||
// Validation
|
||||
@@ -819,11 +872,11 @@ async function saveDeviceSettings() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Update device info (name, url)
|
||||
// Update device info (name, url, capture_template_id)
|
||||
const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ name, url })
|
||||
body: JSON.stringify({ name, url, capture_template_id })
|
||||
});
|
||||
|
||||
if (deviceResponse.status === 401) {
|
||||
@@ -2126,3 +2179,513 @@ function handleTutorialKey(e) {
|
||||
else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); tutorialNext(); }
|
||||
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); tutorialPrev(); }
|
||||
}
|
||||
|
||||
|
||||
// ===========================
|
||||
// Capture Templates Functions
|
||||
// ===========================
|
||||
|
||||
let availableEngines = [];
|
||||
let currentEditingTemplateId = null;
|
||||
|
||||
// Load and render capture templates
|
||||
async function loadCaptureTemplates() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/capture-templates');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load templates: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
renderTemplatesList(data.templates || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading capture templates:', error);
|
||||
document.getElementById('templates-list').innerHTML = `
|
||||
<div class="error-message">${t('templates.error.load')}: ${error.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render templates list
|
||||
function renderTemplatesList(templates) {
|
||||
const container = document.getElementById('templates-list');
|
||||
|
||||
if (templates.length === 0) {
|
||||
container.innerHTML = `<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('templates.add')}</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = templates.map(template => {
|
||||
const engineIcon = getEngineIcon(template.engine_type);
|
||||
const defaultBadge = template.is_default
|
||||
? `<span class="badge badge-default">${t('templates.default')}</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="template-card" data-template-id="${template.id}">
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">
|
||||
${engineIcon} ${escapeHtml(template.name)}
|
||||
</div>
|
||||
${defaultBadge}
|
||||
</div>
|
||||
<div class="template-config">
|
||||
<strong>${t('templates.engine')}</strong> ${template.engine_type.toUpperCase()}
|
||||
</div>
|
||||
${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>
|
||||
</details>
|
||||
` : `
|
||||
<div class="template-no-config">${t('templates.config.none')}</div>
|
||||
`}
|
||||
<div class="template-card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">
|
||||
🧪
|
||||
</button>
|
||||
${!template.is_default ? `
|
||||
<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>
|
||||
`;
|
||||
}).join('') + `<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('templates.add')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Get engine icon
|
||||
function getEngineIcon(engineType) {
|
||||
return '🖥️';
|
||||
}
|
||||
|
||||
// Show add template modal
|
||||
async function showAddTemplateModal() {
|
||||
currentEditingTemplateId = null;
|
||||
document.getElementById('template-modal-title').textContent = t('templates.add');
|
||||
document.getElementById('template-form').reset();
|
||||
document.getElementById('template-id').value = '';
|
||||
document.getElementById('engine-config-section').style.display = 'none';
|
||||
document.getElementById('template-error').style.display = 'none';
|
||||
|
||||
// Load available engines
|
||||
await loadAvailableEngines();
|
||||
|
||||
document.getElementById('template-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Edit template
|
||||
async function editTemplate(templateId) {
|
||||
try {
|
||||
const response = await fetchWithAuth(`/capture-templates/${templateId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load template: ${response.status}`);
|
||||
}
|
||||
|
||||
const template = await response.json();
|
||||
|
||||
currentEditingTemplateId = templateId;
|
||||
document.getElementById('template-modal-title').textContent = t('templates.edit');
|
||||
document.getElementById('template-id').value = templateId;
|
||||
document.getElementById('template-name').value = template.name;
|
||||
|
||||
// Load available engines
|
||||
await loadAvailableEngines();
|
||||
|
||||
// Set engine and load config
|
||||
document.getElementById('template-engine').value = template.engine_type;
|
||||
await onEngineChange();
|
||||
|
||||
// Populate engine config fields
|
||||
populateEngineConfig(template.engine_config);
|
||||
|
||||
// Load displays for test
|
||||
await loadDisplaysForTest();
|
||||
|
||||
document.getElementById('template-test-results').style.display = 'none';
|
||||
document.getElementById('template-error').style.display = 'none';
|
||||
|
||||
document.getElementById('template-modal').style.display = 'flex';
|
||||
} catch (error) {
|
||||
console.error('Error loading template:', error);
|
||||
showToast(t('templates.error.load') + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Close template modal
|
||||
function closeTemplateModal() {
|
||||
document.getElementById('template-modal').style.display = 'none';
|
||||
currentEditingTemplateId = null;
|
||||
}
|
||||
|
||||
// Update capture duration and save to localStorage
|
||||
function updateCaptureDuration(value) {
|
||||
document.getElementById('test-template-duration-value').textContent = value;
|
||||
localStorage.setItem('capture_duration', value);
|
||||
}
|
||||
|
||||
// Restore capture duration from localStorage
|
||||
function restoreCaptureDuration() {
|
||||
const savedDuration = localStorage.getItem('capture_duration');
|
||||
if (savedDuration) {
|
||||
const durationInput = document.getElementById('test-template-duration');
|
||||
const durationValue = document.getElementById('test-template-duration-value');
|
||||
durationInput.value = savedDuration;
|
||||
durationValue.textContent = savedDuration;
|
||||
}
|
||||
}
|
||||
|
||||
// Show test template modal
|
||||
async function showTestTemplateModal(templateId) {
|
||||
const templates = await fetchWithAuth('/capture-templates').then(r => r.json());
|
||||
const template = templates.templates.find(t => t.id === templateId);
|
||||
|
||||
if (!template) {
|
||||
showToast(t('templates.error.load'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store current template for testing
|
||||
window.currentTestingTemplate = template;
|
||||
|
||||
// Load displays
|
||||
await loadDisplaysForTest();
|
||||
|
||||
// Restore last used capture duration
|
||||
restoreCaptureDuration();
|
||||
|
||||
// Reset results
|
||||
document.getElementById('test-template-results').style.display = 'none';
|
||||
|
||||
// Show modal
|
||||
document.getElementById('test-template-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Close test template modal
|
||||
function closeTestTemplateModal() {
|
||||
document.getElementById('test-template-modal').style.display = 'none';
|
||||
window.currentTestingTemplate = null;
|
||||
}
|
||||
|
||||
// Load available engines
|
||||
async function loadAvailableEngines() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/capture-engines');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load engines: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
availableEngines = data.engines || [];
|
||||
|
||||
const select = document.getElementById('template-engine');
|
||||
select.innerHTML = `<option value="">${t('templates.engine.select')}</option>`;
|
||||
|
||||
availableEngines.forEach(engine => {
|
||||
const option = document.createElement('option');
|
||||
option.value = engine.type;
|
||||
option.textContent = `${getEngineIcon(engine.type)} ${engine.name}`;
|
||||
if (!engine.available) {
|
||||
option.disabled = true;
|
||||
option.textContent += ` (${t('templates.engine.unavailable')})`;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading engines:', error);
|
||||
showToast(t('templates.error.engines') + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle engine selection change
|
||||
async function onEngineChange() {
|
||||
const engineType = document.getElementById('template-engine').value;
|
||||
const configSection = document.getElementById('engine-config-section');
|
||||
const configFields = document.getElementById('engine-config-fields');
|
||||
|
||||
if (!engineType) {
|
||||
configSection.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const engine = availableEngines.find(e => e.type === engineType);
|
||||
if (!engine) {
|
||||
configSection.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show availability hint
|
||||
const hint = document.getElementById('engine-availability-hint');
|
||||
if (!engine.available) {
|
||||
hint.textContent = t('templates.engine.unavailable.hint');
|
||||
hint.style.display = 'block';
|
||||
hint.style.color = 'var(--error-color)';
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
}
|
||||
|
||||
// Render config fields based on default_config
|
||||
configFields.innerHTML = '';
|
||||
const defaultConfig = engine.default_config || {};
|
||||
|
||||
if (Object.keys(defaultConfig).length === 0) {
|
||||
configFields.innerHTML = `<p class="text-muted">${t('templates.config.none')}</p>`;
|
||||
} else {
|
||||
Object.entries(defaultConfig).forEach(([key, value]) => {
|
||||
const fieldType = typeof value === 'number' ? 'number' : 'text';
|
||||
const fieldValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : value;
|
||||
|
||||
const fieldHtml = `
|
||||
<div class="form-group">
|
||||
<label for="config-${key}">${key}:</label>
|
||||
${typeof value === 'boolean' ? `
|
||||
<select id="config-${key}" data-config-key="${key}">
|
||||
<option value="true" ${value ? 'selected' : ''}>true</option>
|
||||
<option value="false" ${!value ? 'selected' : ''}>false</option>
|
||||
</select>
|
||||
` : `
|
||||
<input type="${fieldType}" id="config-${key}" data-config-key="${key}" value="${fieldValue}">
|
||||
`}
|
||||
<small class="form-hint">${t('templates.config.default')}: ${JSON.stringify(value)}</small>
|
||||
</div>
|
||||
`;
|
||||
configFields.innerHTML += fieldHtml;
|
||||
});
|
||||
}
|
||||
|
||||
configSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// Populate engine config fields with values
|
||||
function populateEngineConfig(config) {
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
const field = document.getElementById(`config-${key}`);
|
||||
if (field) {
|
||||
if (field.tagName === 'SELECT') {
|
||||
field.value = value.toString();
|
||||
} else {
|
||||
field.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collect engine config from form
|
||||
function collectEngineConfig() {
|
||||
const config = {};
|
||||
const fields = document.querySelectorAll('[data-config-key]');
|
||||
|
||||
fields.forEach(field => {
|
||||
const key = field.dataset.configKey;
|
||||
let value = field.value;
|
||||
|
||||
// Type conversion
|
||||
if (field.type === 'number') {
|
||||
value = parseFloat(value);
|
||||
} else if (field.tagName === 'SELECT' && (value === 'true' || value === 'false')) {
|
||||
value = value === 'true';
|
||||
}
|
||||
|
||||
config[key] = value;
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// Load displays for test selector
|
||||
async function loadDisplaysForTest() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/config/displays');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load displays: ${response.status}`);
|
||||
}
|
||||
|
||||
const displaysData = await response.json();
|
||||
const select = document.getElementById('test-template-display');
|
||||
select.innerHTML = `<option value="">${t('templates.test.display.select')}</option>`;
|
||||
|
||||
(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 += ' - Primary';
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading displays:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run template test
|
||||
async function runTemplateTest() {
|
||||
if (!window.currentTestingTemplate) {
|
||||
showToast(t('templates.test.error.no_engine'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const displayIndex = document.getElementById('test-template-display').value;
|
||||
const captureDuration = parseFloat(document.getElementById('test-template-duration').value);
|
||||
|
||||
if (displayIndex === '') {
|
||||
showToast(t('templates.test.error.no_display'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = window.currentTestingTemplate;
|
||||
const resultsDiv = document.getElementById('test-template-results');
|
||||
|
||||
// Show loading state without destroying the structure
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'loading';
|
||||
loadingDiv.textContent = t('templates.test.running');
|
||||
loadingDiv.style.position = 'absolute';
|
||||
loadingDiv.style.inset = '0';
|
||||
loadingDiv.style.background = 'var(--bg-primary)';
|
||||
loadingDiv.style.display = 'flex';
|
||||
loadingDiv.style.alignItems = 'center';
|
||||
loadingDiv.style.justifyContent = 'center';
|
||||
loadingDiv.style.zIndex = '10';
|
||||
loadingDiv.id = 'test-loading-overlay';
|
||||
|
||||
// Remove old loading overlay if exists
|
||||
const oldLoading = document.getElementById('test-loading-overlay');
|
||||
if (oldLoading) oldLoading.remove();
|
||||
|
||||
resultsDiv.style.display = 'block';
|
||||
resultsDiv.style.position = 'relative';
|
||||
resultsDiv.appendChild(loadingDiv);
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/capture-templates/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
engine_type: template.engine_type,
|
||||
engine_config: template.engine_config,
|
||||
display_index: parseInt(displayIndex),
|
||||
capture_duration: captureDuration
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || error.message || 'Test failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
displayTestResults(result);
|
||||
} catch (error) {
|
||||
console.error('Error running test:', error);
|
||||
// Remove loading overlay
|
||||
const loadingOverlay = document.getElementById('test-loading-overlay');
|
||||
if (loadingOverlay) loadingOverlay.remove();
|
||||
// Show short error in snack, details are in console
|
||||
showToast(t('templates.test.error.failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Display test results
|
||||
function displayTestResults(result) {
|
||||
const resultsDiv = document.getElementById('test-template-results');
|
||||
|
||||
// Remove loading overlay
|
||||
const loadingOverlay = document.getElementById('test-loading-overlay');
|
||||
if (loadingOverlay) loadingOverlay.remove();
|
||||
|
||||
// Full capture preview
|
||||
const previewImg = document.getElementById('test-template-preview-image');
|
||||
previewImg.innerHTML = `<img src="${result.full_capture.image}" alt="Capture preview" style="max-width: 100%; border-radius: 4px;">`;
|
||||
|
||||
// Performance stats
|
||||
document.getElementById('test-template-actual-duration').textContent = `${result.performance.capture_duration_s.toFixed(2)}s`;
|
||||
document.getElementById('test-template-frame-count').textContent = result.performance.frame_count;
|
||||
document.getElementById('test-template-actual-fps').textContent = `${result.performance.actual_fps.toFixed(1)} FPS`;
|
||||
document.getElementById('test-template-avg-capture-time').textContent = `${result.performance.avg_capture_time_ms.toFixed(1)}ms`;
|
||||
|
||||
// Show results
|
||||
resultsDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
// Save template
|
||||
async function saveTemplate() {
|
||||
const templateId = document.getElementById('template-id').value;
|
||||
const name = document.getElementById('template-name').value.trim();
|
||||
const engineType = document.getElementById('template-engine').value;
|
||||
|
||||
if (!name || !engineType) {
|
||||
showToast(t('templates.error.required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const engineConfig = collectEngineConfig();
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
engine_type: engineType,
|
||||
engine_config: engineConfig
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (templateId) {
|
||||
// Update existing template
|
||||
response = await fetchWithAuth(`/capture-templates/${templateId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
} else {
|
||||
// Create new template
|
||||
response = await fetchWithAuth('/capture-templates', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || error.message || 'Failed to save template');
|
||||
}
|
||||
|
||||
showToast(templateId ? t('templates.updated') : t('templates.created'), 'success');
|
||||
closeTemplateModal();
|
||||
await loadCaptureTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error saving template:', error);
|
||||
document.getElementById('template-error').textContent = error.message;
|
||||
document.getElementById('template-error').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Delete template
|
||||
async function deleteTemplate(templateId) {
|
||||
const confirmed = await showConfirm(t('templates.delete.confirm'));
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/capture-templates/${templateId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || error.message || 'Failed to delete template');
|
||||
}
|
||||
|
||||
showToast(t('templates.deleted'), 'success');
|
||||
await loadCaptureTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error deleting template:', error);
|
||||
showToast(t('templates.error.delete') + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user