Live KC test WS, sync clock fix, device card perf, camera icons, tab indicator
Key Colors test: - New WS endpoint for live KC target test streaming (replaces REST polling) - Auto-connect on lightbox open, auto-disconnect on close - Uses same FPS/preview_width as CSS source test (no separate controls) - Removed FPS selector, start/stop toggle, and updateAutoRefreshButton Device cards: - Fix full re-render on every poll caused by relative "Last seen" time in HTML - Last seen label now patched in-place via data attribute (like FPS metrics) - Remove overlay visualization button from LED target cards Sync clocks: - Fix card not updating start/stop icon: invalidate cache before reload Other: - Tab indicator respects bg-anim toggle (hidden when dynamic background off) - Camera backend icon grid uses SVG icons instead of emoji - Frontend context rule: no emoji in IconSelect items Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
import {
|
||||
kcTestAutoRefresh, setKcTestAutoRefresh,
|
||||
kcTestTargetId, setKcTestTargetId,
|
||||
kcTestWs, setKcTestWs,
|
||||
kcTestFps, setKcTestFps,
|
||||
_kcNameManuallyEdited, set_kcNameManuallyEdited,
|
||||
kcWebSockets,
|
||||
PATTERN_RECT_BORDERS,
|
||||
@@ -17,7 +19,7 @@ import { lockBody, showToast, showConfirm, formatUptime, formatCompact, desktopF
|
||||
import { Modal } from '../core/modal.js';
|
||||
import {
|
||||
getValueSourceIcon, getPictureSourceIcon,
|
||||
ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, ICON_PAUSE,
|
||||
ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP,
|
||||
ICON_LINK_SOURCE, ICON_PATTERN_TEMPLATE, ICON_FPS, ICON_PALETTE,
|
||||
} from '../core/icons.js';
|
||||
import * as P from '../core/icon-paths.js';
|
||||
@@ -305,16 +307,54 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
||||
|
||||
// ===== KEY COLORS TEST =====
|
||||
|
||||
export async function fetchKCTest(targetId) {
|
||||
const response = await fetch(`${API_BASE}/output-targets/${targetId}/test`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || response.statusText);
|
||||
function _openKCTestWs(targetId, fps, previewWidth = 480) {
|
||||
// Close any existing WS
|
||||
if (kcTestWs) {
|
||||
try { kcTestWs.close(); } catch (_) {}
|
||||
setKcTestWs(null);
|
||||
}
|
||||
return response.json();
|
||||
|
||||
const key = localStorage.getItem('wled_api_key');
|
||||
if (!key) return;
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}${API_BASE}/output-targets/${targetId}/test/ws?token=${encodeURIComponent(key)}&fps=${fps}&preview_width=${previewWidth}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'frame') {
|
||||
// Hide spinner on first frame
|
||||
const spinner = document.querySelector('.lightbox-spinner');
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
displayKCTestResults(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('KC test WS parse error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (ev) => {
|
||||
setKcTestWs(null);
|
||||
// Only show error if closed unexpectedly (not a normal close)
|
||||
if (ev.code !== 1000 && ev.code !== 1001 && kcTestTargetId) {
|
||||
const reason = ev.reason || t('kc.test.ws_closed');
|
||||
showToast(t('kc.test.error') + ': ' + reason, 'error');
|
||||
// Close lightbox on fatal errors (auth, bad target, etc.)
|
||||
if (ev.code === 4001 || ev.code === 4003 || ev.code === 4004) {
|
||||
if (typeof window.closeLightbox === 'function') window.closeLightbox();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
// onclose will fire after onerror; no need to handle here
|
||||
};
|
||||
|
||||
setKcTestWs(ws);
|
||||
setKcTestTargetId(targetId);
|
||||
}
|
||||
|
||||
export async function testKCTarget(targetId) {
|
||||
@@ -337,38 +377,19 @@ export async function testKCTarget(targetId) {
|
||||
}
|
||||
spinner.style.display = '';
|
||||
|
||||
// Show auto-refresh button
|
||||
// Hide controls — KC test streams automatically
|
||||
const refreshBtn = document.getElementById('lightbox-auto-refresh');
|
||||
if (refreshBtn) refreshBtn.style.display = '';
|
||||
if (refreshBtn) refreshBtn.style.display = 'none';
|
||||
const fpsSelect = document.getElementById('lightbox-fps-select');
|
||||
if (fpsSelect) fpsSelect.style.display = 'none';
|
||||
|
||||
lightbox.classList.add('active');
|
||||
lockBody();
|
||||
|
||||
try {
|
||||
const result = await fetchKCTest(targetId);
|
||||
displayKCTestResults(result);
|
||||
} catch (e) {
|
||||
// Use window.closeLightbox to avoid importing from ui.js circular
|
||||
if (typeof window.closeLightbox === 'function') window.closeLightbox();
|
||||
showToast(t('kc.test.error') + ': ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleKCTestAutoRefresh() {
|
||||
if (kcTestAutoRefresh) {
|
||||
stopKCTestAutoRefresh();
|
||||
} else {
|
||||
setKcTestAutoRefresh(setInterval(async () => {
|
||||
if (!kcTestTargetId) return;
|
||||
try {
|
||||
const result = await fetchKCTest(kcTestTargetId);
|
||||
displayKCTestResults(result);
|
||||
} catch (e) {
|
||||
stopKCTestAutoRefresh();
|
||||
}
|
||||
}, 2000));
|
||||
updateAutoRefreshButton(true);
|
||||
}
|
||||
// Use same FPS from CSS test settings and dynamic preview resolution
|
||||
const fps = parseInt(localStorage.getItem('css_test_fps')) || 15;
|
||||
const previewWidth = Math.round(Math.min(window.innerWidth * 0.8, 1920) * Math.min(window.devicePixelRatio || 1, 2));
|
||||
_openKCTestWs(targetId, fps, previewWidth);
|
||||
}
|
||||
|
||||
export function stopKCTestAutoRefresh() {
|
||||
@@ -376,20 +397,11 @@ export function stopKCTestAutoRefresh() {
|
||||
clearInterval(kcTestAutoRefresh);
|
||||
setKcTestAutoRefresh(null);
|
||||
}
|
||||
setKcTestTargetId(null);
|
||||
updateAutoRefreshButton(false);
|
||||
}
|
||||
|
||||
export function updateAutoRefreshButton(active) {
|
||||
const btn = document.getElementById('lightbox-auto-refresh');
|
||||
if (!btn) return;
|
||||
if (active) {
|
||||
btn.classList.add('active');
|
||||
btn.innerHTML = ICON_PAUSE;
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
btn.innerHTML = ICON_START;
|
||||
if (kcTestWs) {
|
||||
try { kcTestWs.close(1000, 'lightbox closed'); } catch (_) {}
|
||||
setKcTestWs(null);
|
||||
}
|
||||
setKcTestTargetId(null);
|
||||
}
|
||||
|
||||
export function displayKCTestResults(result) {
|
||||
|
||||
Reference in New Issue
Block a user