Show both fps_current and fps_actual in WebUI charts and labels
- Charts: blue filled area for fps_actual (rolling avg), green line for fps_current (real-time sends/sec) - Labels: fps_current/fps_target as primary, avg fps_actual as secondary - Track fps_current in metrics history for dashboard chart preload - Applied to both LED targets page and dashboard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,7 @@ class MetricsHistory:
|
|||||||
self._targets[target_id].append({
|
self._targets[target_id].append({
|
||||||
"t": now,
|
"t": now,
|
||||||
"fps": state.get("fps_actual"),
|
"fps": state.get("fps_actual"),
|
||||||
|
"fps_current": state.get("fps_current"),
|
||||||
"fps_target": state.get("fps_target"),
|
"fps_target": state.get("fps_target"),
|
||||||
"timing": state.get("timing_total_ms"),
|
"timing": state.get("timing_total_ms"),
|
||||||
"errors": state.get("errors_count", 0),
|
"errors": state.get("errors_count", 0),
|
||||||
|
|||||||
@@ -540,6 +540,15 @@ ul.section-tip li {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.target-fps-avg {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.45;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
/* Timing breakdown bar */
|
/* Timing breakdown bar */
|
||||||
.timing-breakdown {
|
.timing-breakdown {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|||||||
@@ -171,6 +171,15 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-fps-avg {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.45;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-target-actions {
|
.dashboard-target-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
||||||
const MAX_FPS_SAMPLES = 120;
|
const MAX_FPS_SAMPLES = 120;
|
||||||
|
|
||||||
let _fpsHistory = {}; // { targetId: number[] }
|
let _fpsHistory = {}; // { targetId: number[] } — fps_actual
|
||||||
|
let _fpsCurrentHistory = {}; // { targetId: number[] } — fps_current
|
||||||
let _fpsCharts = {}; // { targetId: Chart }
|
let _fpsCharts = {}; // { targetId: Chart }
|
||||||
let _lastRunningIds = []; // sorted target IDs from previous render
|
let _lastRunningIds = []; // sorted target IDs from previous render
|
||||||
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
||||||
@@ -25,10 +26,14 @@ let _uptimeTimer = null;
|
|||||||
let _uptimeElements = {}; // { targetId: HTMLElement } — cached DOM refs
|
let _uptimeElements = {}; // { targetId: HTMLElement } — cached DOM refs
|
||||||
let _metricsElements = new Map();
|
let _metricsElements = new Map();
|
||||||
|
|
||||||
function _pushFps(targetId, value) {
|
function _pushFps(targetId, actual, current) {
|
||||||
if (!_fpsHistory[targetId]) _fpsHistory[targetId] = [];
|
if (!_fpsHistory[targetId]) _fpsHistory[targetId] = [];
|
||||||
_fpsHistory[targetId].push(value);
|
_fpsHistory[targetId].push(actual);
|
||||||
if (_fpsHistory[targetId].length > MAX_FPS_SAMPLES) _fpsHistory[targetId].shift();
|
if (_fpsHistory[targetId].length > MAX_FPS_SAMPLES) _fpsHistory[targetId].shift();
|
||||||
|
|
||||||
|
if (!_fpsCurrentHistory[targetId]) _fpsCurrentHistory[targetId] = [];
|
||||||
|
_fpsCurrentHistory[targetId].push(current);
|
||||||
|
if (_fpsCurrentHistory[targetId].length > MAX_FPS_SAMPLES) _fpsCurrentHistory[targetId].shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _setUptimeBase(targetId, seconds) {
|
function _setUptimeBase(targetId, seconds) {
|
||||||
@@ -80,22 +85,32 @@ function _destroyFpsCharts() {
|
|||||||
_fpsCharts = {};
|
_fpsCharts = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createFpsChart(canvasId, history, fpsTarget) {
|
function _createFpsChart(canvasId, actualHistory, currentHistory, fpsTarget) {
|
||||||
const canvas = document.getElementById(canvasId);
|
const canvas = document.getElementById(canvasId);
|
||||||
if (!canvas) return null;
|
if (!canvas) return null;
|
||||||
return new Chart(canvas, {
|
return new Chart(canvas, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: history.map(() => ''),
|
labels: actualHistory.map(() => ''),
|
||||||
datasets: [{
|
datasets: [
|
||||||
data: [...history],
|
{
|
||||||
|
data: [...actualHistory],
|
||||||
borderColor: '#2196F3',
|
borderColor: '#2196F3',
|
||||||
backgroundColor: 'rgba(33,150,243,0.12)',
|
backgroundColor: 'rgba(33,150,243,0.12)',
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
fill: true,
|
fill: true,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
}],
|
},
|
||||||
|
{
|
||||||
|
data: [...currentHistory],
|
||||||
|
borderColor: '#4CAF50',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
@@ -124,6 +139,7 @@ async function _initFpsCharts(runningTargetIds) {
|
|||||||
for (const id of runningTargetIds) {
|
for (const id of runningTargetIds) {
|
||||||
const samples = serverTargets[id] || [];
|
const samples = serverTargets[id] || [];
|
||||||
_fpsHistory[id] = samples.map(s => s.fps).filter(v => v != null);
|
_fpsHistory[id] = samples.map(s => s.fps).filter(v => v != null);
|
||||||
|
_fpsCurrentHistory[id] = samples.map(s => s.fps_current).filter(v => v != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -133,14 +149,15 @@ async function _initFpsCharts(runningTargetIds) {
|
|||||||
|
|
||||||
// Clean up history for targets that are no longer running
|
// Clean up history for targets that are no longer running
|
||||||
for (const id of Object.keys(_fpsHistory)) {
|
for (const id of Object.keys(_fpsHistory)) {
|
||||||
if (!runningTargetIds.includes(id)) delete _fpsHistory[id];
|
if (!runningTargetIds.includes(id)) { delete _fpsHistory[id]; delete _fpsCurrentHistory[id]; }
|
||||||
}
|
}
|
||||||
for (const id of runningTargetIds) {
|
for (const id of runningTargetIds) {
|
||||||
const canvas = document.getElementById(`dashboard-fps-${id}`);
|
const canvas = document.getElementById(`dashboard-fps-${id}`);
|
||||||
if (!canvas) continue;
|
if (!canvas) continue;
|
||||||
const history = _fpsHistory[id] || [];
|
const actualH = _fpsHistory[id] || [];
|
||||||
|
const currentH = _fpsCurrentHistory[id] || [];
|
||||||
const fpsTarget = parseFloat(canvas.dataset.fpsTarget) || 30;
|
const fpsTarget = parseFloat(canvas.dataset.fpsTarget) || 30;
|
||||||
_fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, history, fpsTarget);
|
_fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, actualH, currentH, fpsTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cacheMetricsElements(runningTargetIds);
|
_cacheMetricsElements(runningTargetIds);
|
||||||
@@ -162,19 +179,22 @@ function _updateRunningMetrics(enrichedRunning) {
|
|||||||
for (const target of enrichedRunning) {
|
for (const target of enrichedRunning) {
|
||||||
const state = target.state || {};
|
const state = target.state || {};
|
||||||
const metrics = target.metrics || {};
|
const metrics = target.metrics || {};
|
||||||
|
const fpsCurrent = state.fps_current ?? 0;
|
||||||
const fpsActual = state.fps_actual != null ? state.fps_actual.toFixed(1) : '-';
|
const fpsActual = state.fps_actual != null ? state.fps_actual.toFixed(1) : '-';
|
||||||
const fpsTarget = state.fps_target || (target.settings || target.key_colors_settings || {}).fps || '-';
|
const fpsTarget = state.fps_target || (target.settings || target.key_colors_settings || {}).fps || '-';
|
||||||
const errors = metrics.errors_count || 0;
|
const errors = metrics.errors_count || 0;
|
||||||
|
|
||||||
// Push FPS and update chart
|
// Push FPS and update chart
|
||||||
if (state.fps_actual != null) {
|
if (state.fps_actual != null) {
|
||||||
_pushFps(target.id, state.fps_actual);
|
_pushFps(target.id, state.fps_actual, fpsCurrent);
|
||||||
}
|
}
|
||||||
const chart = _fpsCharts[target.id];
|
const chart = _fpsCharts[target.id];
|
||||||
if (chart) {
|
if (chart) {
|
||||||
const history = _fpsHistory[target.id] || [];
|
const actualH = _fpsHistory[target.id] || [];
|
||||||
chart.data.datasets[0].data = [...history];
|
const currentH = _fpsCurrentHistory[target.id] || [];
|
||||||
chart.data.labels = history.map(() => '');
|
chart.data.datasets[0].data = [...actualH];
|
||||||
|
chart.data.datasets[1].data = [...currentH];
|
||||||
|
chart.data.labels = actualH.map(() => '');
|
||||||
chart.update();
|
chart.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +206,8 @@ function _updateRunningMetrics(enrichedRunning) {
|
|||||||
// Update text values (use cached refs, fallback to querySelector)
|
// Update text values (use cached refs, fallback to querySelector)
|
||||||
const cached = _metricsElements.get(target.id);
|
const cached = _metricsElements.get(target.id);
|
||||||
const fpsEl = cached?.fps || document.querySelector(`[data-fps-text="${target.id}"]`);
|
const fpsEl = cached?.fps || document.querySelector(`[data-fps-text="${target.id}"]`);
|
||||||
if (fpsEl) fpsEl.innerHTML = `${fpsActual}<span class="dashboard-fps-target">/${fpsTarget}</span>`;
|
if (fpsEl) fpsEl.innerHTML = `${fpsCurrent}<span class="dashboard-fps-target">/${fpsTarget}</span>`
|
||||||
|
+ `<span class="dashboard-fps-avg">avg ${fpsActual}</span>`;
|
||||||
|
|
||||||
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
|
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
|
||||||
if (errorsEl) errorsEl.textContent = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`;
|
if (errorsEl) errorsEl.textContent = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`;
|
||||||
@@ -509,6 +530,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
|
const fpsCurrent = state.fps_current ?? 0;
|
||||||
const fpsActual = state.fps_actual != null ? state.fps_actual.toFixed(1) : '-';
|
const fpsActual = state.fps_actual != null ? state.fps_actual.toFixed(1) : '-';
|
||||||
const fpsTarget = state.fps_target || (target.settings || target.key_colors_settings || {}).fps || '-';
|
const fpsTarget = state.fps_target || (target.settings || target.key_colors_settings || {}).fps || '-';
|
||||||
const uptime = formatUptime(metrics.uptime_seconds);
|
const uptime = formatUptime(metrics.uptime_seconds);
|
||||||
@@ -519,9 +541,9 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
_setUptimeBase(target.id, metrics.uptime_seconds);
|
_setUptimeBase(target.id, metrics.uptime_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push FPS sample to history
|
// Push FPS samples to history
|
||||||
if (state.fps_actual != null) {
|
if (state.fps_actual != null) {
|
||||||
_pushFps(target.id, state.fps_actual);
|
_pushFps(target.id, state.fps_actual, fpsCurrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
let healthDot = '';
|
let healthDot = '';
|
||||||
@@ -544,7 +566,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
<canvas id="dashboard-fps-${target.id}" data-fps-target="${fpsTarget}"></canvas>
|
<canvas id="dashboard-fps-${target.id}" data-fps-target="${fpsTarget}"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-fps-label">
|
<div class="dashboard-fps-label">
|
||||||
<span class="dashboard-metric-value" data-fps-text="${target.id}">${fpsActual}<span class="dashboard-fps-target">/${fpsTarget}</span></span>
|
<span class="dashboard-metric-value" data-fps-text="${target.id}">${fpsCurrent}<span class="dashboard-fps-target">/${fpsTarget}</span><span class="dashboard-fps-avg">avg ${fpsActual}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-metric" title="${t('dashboard.uptime')}">
|
<div class="dashboard-metric" title="${t('dashboard.uptime')}">
|
||||||
|
|||||||
@@ -42,32 +42,49 @@ document.addEventListener('languageChanged', () => {
|
|||||||
|
|
||||||
// --- FPS sparkline history and chart instances for target cards ---
|
// --- FPS sparkline history and chart instances for target cards ---
|
||||||
const _TARGET_MAX_FPS_SAMPLES = 30;
|
const _TARGET_MAX_FPS_SAMPLES = 30;
|
||||||
const _targetFpsHistory = {};
|
const _targetFpsHistory = {}; // fps_actual (rolling avg)
|
||||||
|
const _targetFpsCurrentHistory = {}; // fps_current (sends/sec)
|
||||||
const _targetFpsCharts = {};
|
const _targetFpsCharts = {};
|
||||||
|
|
||||||
function _pushTargetFps(targetId, value) {
|
function _pushTargetFps(targetId, actual, current) {
|
||||||
if (!_targetFpsHistory[targetId]) _targetFpsHistory[targetId] = [];
|
if (!_targetFpsHistory[targetId]) _targetFpsHistory[targetId] = [];
|
||||||
const h = _targetFpsHistory[targetId];
|
const h = _targetFpsHistory[targetId];
|
||||||
h.push(value);
|
h.push(actual);
|
||||||
if (h.length > _TARGET_MAX_FPS_SAMPLES) h.shift();
|
if (h.length > _TARGET_MAX_FPS_SAMPLES) h.shift();
|
||||||
|
|
||||||
|
if (!_targetFpsCurrentHistory[targetId]) _targetFpsCurrentHistory[targetId] = [];
|
||||||
|
const c = _targetFpsCurrentHistory[targetId];
|
||||||
|
c.push(current);
|
||||||
|
if (c.length > _TARGET_MAX_FPS_SAMPLES) c.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createTargetFpsChart(canvasId, history, fpsTarget, maxHwFps) {
|
function _createTargetFpsChart(canvasId, actualHistory, currentHistory, fpsTarget, maxHwFps) {
|
||||||
const canvas = document.getElementById(canvasId);
|
const canvas = document.getElementById(canvasId);
|
||||||
if (!canvas) return null;
|
if (!canvas) return null;
|
||||||
const datasets = [{
|
const labels = actualHistory.map(() => '');
|
||||||
data: [...history],
|
const datasets = [
|
||||||
|
{
|
||||||
|
data: [...actualHistory],
|
||||||
borderColor: '#2196F3',
|
borderColor: '#2196F3',
|
||||||
backgroundColor: 'rgba(33,150,243,0.12)',
|
backgroundColor: 'rgba(33,150,243,0.12)',
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
fill: true,
|
fill: true,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
}];
|
},
|
||||||
|
{
|
||||||
|
data: [...currentHistory],
|
||||||
|
borderColor: '#4CAF50',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
// Flat line showing hardware max FPS
|
// Flat line showing hardware max FPS
|
||||||
if (maxHwFps && maxHwFps < fpsTarget * 1.15) {
|
if (maxHwFps && maxHwFps < fpsTarget * 1.15) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
data: history.map(() => maxHwFps),
|
data: actualHistory.map(() => maxHwFps),
|
||||||
borderColor: 'rgba(255,152,0,0.5)',
|
borderColor: 'rgba(255,152,0,0.5)',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderDash: [4, 3],
|
borderDash: [4, 3],
|
||||||
@@ -77,7 +94,7 @@ function _createTargetFpsChart(canvasId, history, fpsTarget, maxHwFps) {
|
|||||||
}
|
}
|
||||||
return new Chart(canvas, {
|
return new Chart(canvas, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: { labels: history.map(() => ''), datasets },
|
data: { labels, datasets },
|
||||||
options: {
|
options: {
|
||||||
responsive: true, maintainAspectRatio: false,
|
responsive: true, maintainAspectRatio: false,
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -93,9 +110,11 @@ function _createTargetFpsChart(canvasId, history, fpsTarget, maxHwFps) {
|
|||||||
function _updateTargetFpsChart(targetId, fpsTarget) {
|
function _updateTargetFpsChart(targetId, fpsTarget) {
|
||||||
const chart = _targetFpsCharts[targetId];
|
const chart = _targetFpsCharts[targetId];
|
||||||
if (!chart) return;
|
if (!chart) return;
|
||||||
const history = _targetFpsHistory[targetId] || [];
|
const actualH = _targetFpsHistory[targetId] || [];
|
||||||
chart.data.labels = history.map(() => '');
|
const currentH = _targetFpsCurrentHistory[targetId] || [];
|
||||||
chart.data.datasets[0].data = [...history];
|
chart.data.labels = actualH.map(() => '');
|
||||||
|
chart.data.datasets[0].data = [...actualH];
|
||||||
|
chart.data.datasets[1].data = [...currentH];
|
||||||
chart.options.scales.y.max = fpsTarget * 1.15;
|
chart.options.scales.y.max = fpsTarget * 1.15;
|
||||||
chart.update('none');
|
chart.update('none');
|
||||||
}
|
}
|
||||||
@@ -630,15 +649,16 @@ export async function loadTargetsTab() {
|
|||||||
if (target.state && target.state.processing) {
|
if (target.state && target.state.processing) {
|
||||||
runningIds.add(target.id);
|
runningIds.add(target.id);
|
||||||
if (target.state.fps_actual != null) {
|
if (target.state.fps_actual != null) {
|
||||||
_pushTargetFps(target.id, target.state.fps_actual);
|
_pushTargetFps(target.id, target.state.fps_actual, target.state.fps_current ?? 0);
|
||||||
}
|
}
|
||||||
// Create chart if it doesn't exist (new or replaced card)
|
// Create chart if it doesn't exist (new or replaced card)
|
||||||
if (!_targetFpsCharts[target.id]) {
|
if (!_targetFpsCharts[target.id]) {
|
||||||
const history = _targetFpsHistory[target.id] || [];
|
const actualH = _targetFpsHistory[target.id] || [];
|
||||||
|
const currentH = _targetFpsCurrentHistory[target.id] || [];
|
||||||
const fpsTarget = target.state.fps_target || 30;
|
const fpsTarget = target.state.fps_target || 30;
|
||||||
const device = devices.find(d => d.id === target.device_id);
|
const device = devices.find(d => d.id === target.device_id);
|
||||||
const maxHwFps = device ? _computeMaxFps(device.baud_rate, device.led_count, device.device_type) : null;
|
const maxHwFps = device ? _computeMaxFps(device.baud_rate, device.led_count, device.device_type) : null;
|
||||||
const chart = _createTargetFpsChart(`target-fps-${target.id}`, history, fpsTarget, maxHwFps);
|
const chart = _createTargetFpsChart(`target-fps-${target.id}`, actualH, currentH, fpsTarget, maxHwFps);
|
||||||
if (chart) _targetFpsCharts[target.id] = chart;
|
if (chart) _targetFpsCharts[target.id] = chart;
|
||||||
} else {
|
} else {
|
||||||
// Chart survived reconcile — just update data
|
// Chart survived reconcile — just update data
|
||||||
@@ -648,7 +668,7 @@ export async function loadTargetsTab() {
|
|||||||
});
|
});
|
||||||
// Clean up history and charts for targets no longer running
|
// Clean up history and charts for targets no longer running
|
||||||
Object.keys(_targetFpsHistory).forEach(id => {
|
Object.keys(_targetFpsHistory).forEach(id => {
|
||||||
if (!runningIds.has(id)) delete _targetFpsHistory[id];
|
if (!runningIds.has(id)) { delete _targetFpsHistory[id]; delete _targetFpsCurrentHistory[id]; }
|
||||||
});
|
});
|
||||||
Object.keys(_targetFpsCharts).forEach(id => {
|
Object.keys(_targetFpsCharts).forEach(id => {
|
||||||
if (!runningIds.has(id)) {
|
if (!runningIds.has(id)) {
|
||||||
@@ -715,7 +735,8 @@ function _patchTargetMetrics(target) {
|
|||||||
const metrics = target.metrics || {};
|
const metrics = target.metrics || {};
|
||||||
|
|
||||||
const fps = card.querySelector('[data-tm="fps"]');
|
const fps = card.querySelector('[data-tm="fps"]');
|
||||||
if (fps) fps.innerHTML = `${state.fps_actual?.toFixed(1) || '0.0'}<span class="target-fps-target">/${state.fps_target || 0}</span>`;
|
if (fps) fps.innerHTML = `${state.fps_current ?? 0}<span class="target-fps-target">/${state.fps_target || 0}</span>`
|
||||||
|
+ `<span class="target-fps-avg">avg ${state.fps_actual?.toFixed(1) || '0.0'}</span>`;
|
||||||
|
|
||||||
const timing = card.querySelector('[data-tm="timing"]');
|
const timing = card.querySelector('[data-tm="timing"]');
|
||||||
if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);
|
if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);
|
||||||
|
|||||||
Reference in New Issue
Block a user