Add max HW FPS line on sparkline chart, fix button click race with polling
- Draw dashed orange line on target FPS sparkline showing hardware max FPS - Prevent loadTargetsTab polling from rebuilding DOM while a button action (start/stop/overlay/delete) is in flight; add reentry guard on the refresh function itself Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,23 +33,32 @@ function _pushTargetFps(targetId, value) {
|
|||||||
if (h.length > _TARGET_MAX_FPS_SAMPLES) h.shift();
|
if (h.length > _TARGET_MAX_FPS_SAMPLES) h.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createTargetFpsChart(canvasId, history, fpsTarget) {
|
function _createTargetFpsChart(canvasId, history, fpsTarget, maxHwFps) {
|
||||||
const canvas = document.getElementById(canvasId);
|
const canvas = document.getElementById(canvasId);
|
||||||
if (!canvas) return null;
|
if (!canvas) return null;
|
||||||
|
const datasets = [{
|
||||||
|
data: [...history],
|
||||||
|
borderColor: '#2196F3',
|
||||||
|
backgroundColor: 'rgba(33,150,243,0.12)',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
|
pointRadius: 0,
|
||||||
|
}];
|
||||||
|
// Flat line showing hardware max FPS
|
||||||
|
if (maxHwFps && maxHwFps < fpsTarget * 1.15) {
|
||||||
|
datasets.push({
|
||||||
|
data: history.map(() => maxHwFps),
|
||||||
|
borderColor: 'rgba(255,152,0,0.5)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderDash: [4, 3],
|
||||||
|
pointRadius: 0,
|
||||||
|
fill: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
return new Chart(canvas, {
|
return new Chart(canvas, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: { labels: history.map(() => ''), datasets },
|
||||||
labels: history.map(() => ''),
|
|
||||||
datasets: [{
|
|
||||||
data: [...history],
|
|
||||||
borderColor: '#2196F3',
|
|
||||||
backgroundColor: 'rgba(33,150,243,0.12)',
|
|
||||||
borderWidth: 1.5,
|
|
||||||
tension: 0.3,
|
|
||||||
fill: true,
|
|
||||||
pointRadius: 0,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
options: {
|
||||||
responsive: true, maintainAspectRatio: false,
|
responsive: true, maintainAspectRatio: false,
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -284,9 +293,15 @@ export function switchTargetSubTab(tabKey) {
|
|||||||
localStorage.setItem('activeTargetSubTab', tabKey);
|
localStorage.setItem('activeTargetSubTab', tabKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _loadTargetsLock = false;
|
||||||
|
let _actionInFlight = false;
|
||||||
|
|
||||||
export async function loadTargetsTab() {
|
export async function loadTargetsTab() {
|
||||||
const container = document.getElementById('targets-panel-content');
|
const container = document.getElementById('targets-panel-content');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
// Skip if another loadTargetsTab or a button action is already running
|
||||||
|
if (_loadTargetsLock || _actionInFlight) return;
|
||||||
|
_loadTargetsLock = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch devices, targets, CSS sources, picture sources, and pattern templates in parallel
|
// Fetch devices, targets, CSS sources, picture sources, and pattern templates in parallel
|
||||||
@@ -488,7 +503,9 @@ export async function loadTargetsTab() {
|
|||||||
}
|
}
|
||||||
const history = _targetFpsHistory[target.id] || [];
|
const history = _targetFpsHistory[target.id] || [];
|
||||||
const fpsTarget = target.state.fps_target || 30;
|
const fpsTarget = target.state.fps_target || 30;
|
||||||
_createTargetFpsChart(`target-fps-${target.id}`, history, fpsTarget);
|
const device = devices.find(d => d.id === target.device_id);
|
||||||
|
const maxHwFps = device ? _computeMaxFps(device.baud_rate, device.led_count, device.device_type) : null;
|
||||||
|
_createTargetFpsChart(`target-fps-${target.id}`, history, fpsTarget, maxHwFps);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Clean up history for targets no longer running
|
// Clean up history for targets no longer running
|
||||||
@@ -500,6 +517,8 @@ export async function loadTargetsTab() {
|
|||||||
if (error.isAuth) return;
|
if (error.isAuth) return;
|
||||||
console.error('Failed to load targets tab:', error);
|
console.error('Failed to load targets tab:', error);
|
||||||
container.innerHTML = `<div class="loading">${t('targets.failed')}</div>`;
|
container.innerHTML = `<div class="loading">${t('targets.failed')}</div>`;
|
||||||
|
} finally {
|
||||||
|
_loadTargetsLock = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,95 +630,86 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startTargetProcessing(targetId) {
|
async function _targetAction(action) {
|
||||||
|
_actionInFlight = true;
|
||||||
try {
|
try {
|
||||||
|
await action();
|
||||||
|
} finally {
|
||||||
|
_actionInFlight = false;
|
||||||
|
_loadTargetsLock = false; // ensure next poll can run
|
||||||
|
loadTargetsTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startTargetProcessing(targetId) {
|
||||||
|
await _targetAction(async () => {
|
||||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/start`, {
|
const response = await fetchWithAuth(`/picture-targets/${targetId}/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showToast(t('device.started'), 'success');
|
showToast(t('device.started'), 'success');
|
||||||
loadTargetsTab();
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showToast(`Failed to start: ${error.detail}`, 'error');
|
showToast(`Failed to start: ${error.detail}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
if (error.isAuth) return;
|
|
||||||
showToast('Failed to start processing', 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopTargetProcessing(targetId) {
|
export async function stopTargetProcessing(targetId) {
|
||||||
try {
|
await _targetAction(async () => {
|
||||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/stop`, {
|
const response = await fetchWithAuth(`/picture-targets/${targetId}/stop`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showToast(t('device.stopped'), 'success');
|
showToast(t('device.stopped'), 'success');
|
||||||
loadTargetsTab();
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showToast(`Failed to stop: ${error.detail}`, 'error');
|
showToast(`Failed to stop: ${error.detail}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
if (error.isAuth) return;
|
|
||||||
showToast('Failed to stop processing', 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startTargetOverlay(targetId) {
|
export async function startTargetOverlay(targetId) {
|
||||||
try {
|
await _targetAction(async () => {
|
||||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/start`, {
|
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showToast(t('overlay.started'), 'success');
|
showToast(t('overlay.started'), 'success');
|
||||||
loadTargetsTab();
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showToast(t('overlay.error.start') + ': ' + error.detail, 'error');
|
showToast(t('overlay.error.start') + ': ' + error.detail, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
if (error.isAuth) return;
|
|
||||||
showToast(t('overlay.error.start'), 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopTargetOverlay(targetId) {
|
export async function stopTargetOverlay(targetId) {
|
||||||
try {
|
await _targetAction(async () => {
|
||||||
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/stop`, {
|
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/stop`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showToast(t('overlay.stopped'), 'success');
|
showToast(t('overlay.stopped'), 'success');
|
||||||
loadTargetsTab();
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showToast(t('overlay.error.stop') + ': ' + error.detail, 'error');
|
showToast(t('overlay.error.stop') + ': ' + error.detail, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
if (error.isAuth) return;
|
|
||||||
showToast(t('overlay.error.stop'), 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTarget(targetId) {
|
export async function deleteTarget(targetId) {
|
||||||
const confirmed = await showConfirm(t('targets.delete.confirm'));
|
const confirmed = await showConfirm(t('targets.delete.confirm'));
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
try {
|
await _targetAction(async () => {
|
||||||
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
|
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showToast(t('targets.deleted'), 'success');
|
showToast(t('targets.deleted'), 'success');
|
||||||
loadTargetsTab();
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showToast(`Failed to delete: ${error.detail}`, 'error');
|
showToast(`Failed to delete: ${error.detail}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
if (error.isAuth) return;
|
|
||||||
showToast('Failed to delete target', 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user