Port WLED optimizations to KC loop: fix FPS metrics, add keepalive and auto-refresh test

- Fix KC fps_actual to use frame-to-frame timestamps (was inflated by measuring before sleep)
- Add fps_potential, fps_current, frames_skipped, frames_keepalive metrics to KC loop
- Add keepalive broadcast for static frames so WS clients stay in sync
- Expose all KC metrics in get_kc_target_state() and update UI card to show 7 metrics
- Add auto-refresh play/pause button to KC test lightbox (polls every ~1s)
- Fix WebSocket color swatches computing hex from r,g,b when hex field is absent
- Fix WebSocket auth crash by using get_config() instead of module-level config variable
- Fix lightbox closing when clicking auto-refresh button (event bubbling)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 15:57:07 +03:00
parent 9383fb9a53
commit 398f090eca
5 changed files with 160 additions and 28 deletions

View File

@@ -1,6 +1,8 @@
const API_BASE = '/api/v1';
let refreshInterval = null;
let apiKey = null;
let kcTestAutoRefresh = null; // interval ID for KC test auto-refresh
let kcTestTargetId = null; // currently testing KC target
// Toggle hint description visibility next to a label
function toggleHint(btn) {
@@ -78,7 +80,9 @@ function openLightbox(imageSrc, statsHtml) {
}
function closeLightbox(event) {
if (event && event.target && event.target.closest('.lightbox-content')) return;
if (event && event.target && (event.target.closest('.lightbox-content') || event.target.closest('.lightbox-refresh-btn'))) return;
// Stop KC test auto-refresh if running
stopKCTestAutoRefresh();
const lightbox = document.getElementById('image-lightbox');
lightbox.classList.remove('active');
const img = document.getElementById('lightbox-image');
@@ -89,6 +93,9 @@ function closeLightbox(event) {
document.getElementById('lightbox-stats').style.display = 'none';
const spinner = lightbox.querySelector('.lightbox-spinner');
if (spinner) spinner.style.display = 'none';
// Hide auto-refresh button
const refreshBtn = document.getElementById('lightbox-auto-refresh');
if (refreshBtn) { refreshBtn.style.display = 'none'; refreshBtn.classList.remove('active'); }
unlockBody();
}
@@ -4341,20 +4348,32 @@ function createKCTargetCard(target, sourceMap, patternTemplateMap) {
${isProcessing ? `
<div class="metrics-grid">
<div class="metric">
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
<div class="metric-label">${t('targets.metrics.actual_fps')}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.current_fps')}</div>
<div class="metric-value">${state.fps_current ?? '-'}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.target_fps')}</div>
<div class="metric-value">${state.fps_target || 0}</div>
<div class="metric-label">${t('targets.metrics.target_fps')}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.potential_fps')}</div>
<div class="metric-value">${state.fps_potential?.toFixed(0) || '-'}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.frames')}</div>
<div class="metric-value">${metrics.frames_processed || 0}</div>
<div class="metric-label">${t('targets.metrics.frames')}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.keepalive')}</div>
<div class="metric-value">${state.frames_keepalive ?? '-'}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.errors')}</div>
<div class="metric-value">${metrics.errors_count || 0}</div>
<div class="metric-label">${t('targets.metrics.errors')}</div>
</div>
</div>
` : ''}
@@ -4382,7 +4401,21 @@ function createKCTargetCard(target, sourceMap, patternTemplateMap) {
// ===== KEY COLORS TEST =====
async function fetchKCTest(targetId) {
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/test`, {
method: 'POST',
headers: getHeaders(),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.detail || response.statusText);
}
return response.json();
}
async function testKCTarget(targetId) {
kcTestTargetId = targetId;
// Show lightbox immediately with a spinner
const lightbox = document.getElementById('image-lightbox');
const lbImg = document.getElementById('lightbox-image');
@@ -4400,19 +4433,15 @@ async function testKCTarget(targetId) {
}
spinner.style.display = '';
// Show auto-refresh button
const refreshBtn = document.getElementById('lightbox-auto-refresh');
if (refreshBtn) refreshBtn.style.display = '';
lightbox.classList.add('active');
lockBody();
try {
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/test`, {
method: 'POST',
headers: getHeaders(),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.detail || response.statusText);
}
const result = await response.json();
const result = await fetchKCTest(targetId);
displayKCTestResults(result);
} catch (e) {
closeLightbox();
@@ -4420,6 +4449,44 @@ async function testKCTarget(targetId) {
}
}
function toggleKCTestAutoRefresh() {
if (kcTestAutoRefresh) {
stopKCTestAutoRefresh();
} else {
kcTestAutoRefresh = setInterval(async () => {
if (!kcTestTargetId) return;
try {
const result = await fetchKCTest(kcTestTargetId);
displayKCTestResults(result);
} catch (e) {
stopKCTestAutoRefresh();
}
}, 1000);
updateAutoRefreshButton(true);
}
}
function stopKCTestAutoRefresh() {
if (kcTestAutoRefresh) {
clearInterval(kcTestAutoRefresh);
kcTestAutoRefresh = null;
}
kcTestTargetId = null;
updateAutoRefreshButton(false);
}
function updateAutoRefreshButton(active) {
const btn = document.getElementById('lightbox-auto-refresh');
if (!btn) return;
if (active) {
btn.classList.add('active');
btn.innerHTML = '&#x23F8;'; // pause symbol
} else {
btn.classList.remove('active');
btn.innerHTML = '&#x25B6;'; // play symbol
}
}
function displayKCTestResults(result) {
const srcImg = new window.Image();
srcImg.onload = () => {
@@ -4794,12 +4861,15 @@ function updateKCColorSwatches(targetId, colors) {
return;
}
container.innerHTML = entries.map(([name, color]) => `
<div class="kc-swatch">
<div class="kc-swatch-color" style="background-color: ${color.hex}" title="${color.hex}"></div>
<span class="kc-swatch-label" title="${escapeHtml(name)}">${escapeHtml(name)}</span>
</div>
`).join('');
container.innerHTML = entries.map(([name, color]) => {
const hex = color.hex || `#${(color.r || 0).toString(16).padStart(2, '0')}${(color.g || 0).toString(16).padStart(2, '0')}${(color.b || 0).toString(16).padStart(2, '0')}`;
return `
<div class="kc-swatch">
<div class="kc-swatch-color" style="background-color: ${hex}" title="${hex}"></div>
<span class="kc-swatch-label" title="${escapeHtml(name)}">${escapeHtml(name)}</span>
</div>
`;
}).join('');
}
// ===== PATTERN TEMPLATES =====

View File

@@ -867,6 +867,7 @@
<!-- Image Lightbox -->
<div id="image-lightbox" class="lightbox" onclick="closeLightbox(event)">
<button class="lightbox-close" onclick="closeLightbox()" title="Close">&#x2715;</button>
<button id="lightbox-auto-refresh" class="lightbox-refresh-btn" onclick="toggleKCTestAutoRefresh()" title="Auto-refresh" style="display:none">&#x25B6;</button>
<div class="lightbox-content">
<img id="lightbox-image" src="" alt="Full size preview">
<div id="lightbox-stats" class="lightbox-stats" style="display: none;"></div>

View File

@@ -2440,6 +2440,33 @@ input:-webkit-autofill:focus {
background: rgba(255, 255, 255, 0.3);
}
.lightbox-refresh-btn {
position: absolute;
top: 16px;
right: 64px;
background: rgba(255, 255, 255, 0.15);
border: none;
color: white;
font-size: 1.2rem;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
z-index: 1;
}
.lightbox-refresh-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.lightbox-refresh-btn.active {
background: var(--primary-color);
}
.lightbox-stats {
position: absolute;
bottom: 8px;