Fix critical frontend issues: race conditions, memory leaks, silent failures

- Add loading guard to loadPictureSources to prevent concurrent fetches
- Pause perf chart polling and uptime timer when browser tab is hidden
- Disconnect KC and LED preview WebSockets when leaving targets tab
- Add error toasts to loadCaptureTemplates and saveKCBrightness
- Skip auto-refresh polling when document is hidden
- Widen auto-start dashboard cards for better text display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 17:29:47 +03:00
parent b51839ef3c
commit 82e12ffaac
9 changed files with 86 additions and 17 deletions

View File

@@ -370,23 +370,32 @@ export async function loadDashboard(forceFullRender = false) {
const autoStartTargets = enriched.filter(t => t.auto_start);
if (autoStartTargets.length > 0) {
const autoStartItems = autoStartTargets.map(target => {
const autoStartCards = autoStartTargets.map(target => {
const isRunning = !!(target.state && target.state.processing);
const device = devicesMap[target.device_id];
const deviceName = device ? device.name : '';
const typeIcon = getTargetTypeIcon(target.target_type);
const isLed = target.target_type !== 'key_colors';
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
const subtitleParts = [typeLabel];
if (isLed) {
const device = target.device_id ? devicesMap[target.device_id] : null;
if (device) subtitleParts.push((device.device_type || '').toUpperCase());
const cssId = target.color_strip_source_id || '';
if (cssId) {
const css = cssSourceMap[cssId];
if (css) subtitleParts.push(t(`color_strip.type.${css.source_type}`) || css.source_type);
}
}
const statusBadge = isRunning
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
const subtitle = subtitleParts.length ? `<div class="dashboard-target-subtitle">${escapeHtml(subtitleParts.join(' · '))}</div>` : '';
return `<div class="dashboard-target dashboard-autostart" data-target-id="${target.id}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_AUTOSTART}</span>
<div>
<div class="dashboard-target-name">${escapeHtml(target.name)} ${statusBadge}</div>
${deviceName ? `<div class="dashboard-target-subtitle">${typeIcon} ${escapeHtml(deviceName)}</div>` : `<div class="dashboard-target-subtitle">${typeIcon}</div>`}
${subtitle}
</div>
</div>
<div class="dashboard-target-metrics"></div>
<div class="dashboard-target-actions">
<button class="btn btn-icon ${isRunning ? 'btn-warning' : 'btn-success'}" onclick="${isRunning ? `dashboardStopTarget('${target.id}')` : `dashboardStartTarget('${target.id}')`}" title="${isRunning ? t('device.stop') : t('device.start')}">
${isRunning ? ICON_STOP_PLAIN : '▶'}
@@ -394,6 +403,7 @@ export async function loadDashboard(forceFullRender = false) {
</div>
</div>`;
}).join('');
const autoStartItems = `<div class="dashboard-autostart-grid">${autoStartCards}</div>`;
dynamicHtml += `<div class="dashboard-section">
${_sectionHeader('autostart', t('autostart.title'), autoStartTargets.length)}
@@ -707,3 +717,13 @@ document.addEventListener('languageChanged', () => {
if (perfEl) perfEl.remove();
loadDashboard();
});
// Pause uptime timer when browser tab is hidden, resume when visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
_stopUptimeTimer();
} else if (_isDashboardActive() && _lastRunningIds.length > 0) {
_cacheUptimeElements();
_startUptimeTimer();
}
});