Show uptime in target cards, fix dashboard uptime stale after tab switch

Add uptime metric to both LED and KC target cards in the targets tab.
Move formatUptime() to shared ui.js module. Fix dashboard uptime freezing
when switching tabs by re-caching DOM element refs on early return paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 14:36:14 +03:00
parent 425deb9570
commit 48651f0a4e
6 changed files with 29 additions and 13 deletions

View File

@@ -292,3 +292,13 @@ export function hideOverlaySpinner() {
const overlay = document.getElementById('overlay-spinner'); const overlay = document.getElementById('overlay-spinner');
if (overlay) overlay.remove(); if (overlay) overlay.remove();
} }
export function formatUptime(seconds) {
if (!seconds || seconds <= 0) return '-';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return t('time.hours_minutes', { h, m });
if (m > 0) return t('time.minutes_seconds', { m, s });
return t('time.seconds', { s });
}

View File

@@ -5,7 +5,7 @@
import { apiKey, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval } from '../core/state.js'; import { apiKey, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval } from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
import { t } from '../core/i18n.js'; import { t } from '../core/i18n.js';
import { showToast } from '../core/ui.js'; import { showToast, formatUptime } from '../core/ui.js';
import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js'; import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js';
import { startAutoRefresh } from './tabs.js'; import { startAutoRefresh } from './tabs.js';
@@ -292,16 +292,6 @@ function _sectionContent(sectionKey, itemsHtml) {
return `<div class="dashboard-section-content"${isCollapsed ? ' style="display:none"' : ''}>${itemsHtml}</div>`; return `<div class="dashboard-section-content"${isCollapsed ? ' style="display:none"' : ''}>${itemsHtml}</div>`;
} }
function formatUptime(seconds) {
if (!seconds || seconds <= 0) return '-';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return t('time.hours_minutes', { h, m });
if (m > 0) return t('time.minutes_seconds', { m, s });
return t('time.seconds', { s });
}
export async function loadDashboard(forceFullRender = false) { export async function loadDashboard(forceFullRender = false) {
if (_dashboardLoading) return; if (_dashboardLoading) return;
set_dashboardLoading(true); set_dashboardLoading(true);
@@ -356,12 +346,18 @@ export async function loadDashboard(forceFullRender = false) {
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds; const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds;
if (structureUnchanged && !forceFullRender && running.length > 0) { if (structureUnchanged && !forceFullRender && running.length > 0) {
_updateRunningMetrics(running); _updateRunningMetrics(running);
_cacheUptimeElements();
_startUptimeTimer();
startPerfPolling();
set_dashboardLoading(false); set_dashboardLoading(false);
return; return;
} }
if (structureUnchanged && forceFullRender) { if (structureUnchanged && forceFullRender) {
if (running.length > 0) _updateRunningMetrics(running); if (running.length > 0) _updateRunningMetrics(running);
_updateProfilesInPlace(profiles); _updateProfilesInPlace(profiles);
_cacheUptimeElements();
_startUptimeTimer();
startPerfPolling();
set_dashboardLoading(false); set_dashboardLoading(false);
return; return;
} }

View File

@@ -12,7 +12,7 @@ import {
} from '../core/state.js'; } from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
import { t } from '../core/i18n.js'; import { t } from '../core/i18n.js';
import { lockBody, showToast, showConfirm } from '../core/ui.js'; import { lockBody, showToast, showConfirm, formatUptime } from '../core/ui.js';
import { Modal } from '../core/modal.js'; import { Modal } from '../core/modal.js';
class KCEditorModal extends Modal { class KCEditorModal extends Modal {
@@ -116,6 +116,10 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap) {
<div class="metric-label">${t('device.metrics.errors')}</div> <div class="metric-label">${t('device.metrics.errors')}</div>
<div class="metric-value">${metrics.errors_count || 0}</div> <div class="metric-value">${metrics.errors_count || 0}</div>
</div> </div>
<div class="metric">
<div class="metric-label">${t('device.metrics.uptime')}</div>
<div class="metric-value">${formatUptime(metrics.uptime_seconds)}</div>
</div>
</div> </div>
${state.timing_total_ms != null ? ` ${state.timing_total_ms != null ? `
<div class="timing-breakdown"> <div class="timing-breakdown">

View File

@@ -11,7 +11,7 @@ import {
} from '../core/state.js'; } from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
import { t } from '../core/i18n.js'; import { t } from '../core/i18n.js';
import { showToast, showConfirm } from '../core/ui.js'; import { showToast, showConfirm, formatUptime } from '../core/ui.js';
import { Modal } from '../core/modal.js'; import { Modal } from '../core/modal.js';
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, _computeMaxFps } from './devices.js'; import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, _computeMaxFps } from './devices.js';
import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js'; import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js';
@@ -696,6 +696,10 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap) {
<div class="metric-label">${t('device.metrics.errors')}</div> <div class="metric-label">${t('device.metrics.errors')}</div>
<div class="metric-value">${metrics.errors_count || 0}</div> <div class="metric-value">${metrics.errors_count || 0}</div>
</div> </div>
<div class="metric">
<div class="metric-label">${t('device.metrics.uptime')}</div>
<div class="metric-value">${formatUptime(metrics.uptime_seconds)}</div>
</div>
</div> </div>
` : ''} ` : ''}
</div> </div>

View File

@@ -160,6 +160,7 @@
"device.metrics.frames_skipped": "Skipped", "device.metrics.frames_skipped": "Skipped",
"device.metrics.keepalive": "Keepalive", "device.metrics.keepalive": "Keepalive",
"device.metrics.errors": "Errors", "device.metrics.errors": "Errors",
"device.metrics.uptime": "Uptime",
"device.metrics.timing": "Pipeline timing:", "device.metrics.timing": "Pipeline timing:",
"device.metrics.device_fps": "Device refresh rate", "device.metrics.device_fps": "Device refresh rate",
"device.health.online": "Online", "device.health.online": "Online",

View File

@@ -160,6 +160,7 @@
"device.metrics.frames_skipped": "Пропущено", "device.metrics.frames_skipped": "Пропущено",
"device.metrics.keepalive": "Keepalive", "device.metrics.keepalive": "Keepalive",
"device.metrics.errors": "Ошибки", "device.metrics.errors": "Ошибки",
"device.metrics.uptime": "Время работы",
"device.metrics.timing": "Тайминг пайплайна:", "device.metrics.timing": "Тайминг пайплайна:",
"device.metrics.device_fps": "Частота обновления устройства", "device.metrics.device_fps": "Частота обновления устройства",
"device.health.online": "Онлайн", "device.health.online": "Онлайн",