Add collapsible pipeline metrics and error indicator to target cards

FPS chart stays always visible; timing, frames, keepalive, errors, and
uptime are collapsed behind an animated toggle. Error warning icon
appears next to target name when errors_count > 0. Uses CSS grid
0fr→1fr transition for smooth expand/collapse animation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 00:27:08 +03:00
parent 6fc0e20e1d
commit dc4495a117
6 changed files with 86 additions and 21 deletions

View File

@@ -24,6 +24,7 @@ import {
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
ICON_WARNING,
} from '../core/icons.js';
import { EntitySelect } from '../core/entity-palette.js';
import { wrapCard } from '../core/card-colors.js';
@@ -881,6 +882,13 @@ function _patchTargetMetrics(target) {
const errors = card.querySelector('[data-tm="errors"]');
if (errors) errors.textContent = metrics.errors_count || 0;
// Error indicator near target name
const errorIndicator = card.querySelector('.target-error-indicator');
if (errorIndicator) {
const hasErrors = (metrics.errors_count || 0) > 0;
errorIndicator.classList.toggle('visible', hasErrors);
}
const uptime = card.querySelector('[data-tm="uptime"]');
if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds);
}
@@ -922,6 +930,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
<div class="card-title">
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
${escapeHtml(target.name)}
<span class="target-error-indicator" title="${t('device.metrics.errors')}">${ICON_WARNING}</span>
</div>
</div>
<div class="stream-card-props">
@@ -943,26 +952,31 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
<span class="metric-value" data-tm="fps">---</span>
</div>
</div>
${state.timing_total_ms != null ? `
<div class="timing-breakdown" data-tm="timing" style="grid-column:1/-1"></div>
` : ''}
<div class="metric">
<div class="metric-label">${t('device.metrics.frames')}</div>
<div class="metric-value" data-tm="frames">---</div>
</div>
${state.needs_keepalive !== false ? `
<div class="metric">
<div class="metric-label">${t('device.metrics.keepalive')}</div>
<div class="metric-value" data-tm="keepalive">---</div>
</div>
` : ''}
<div class="metric">
<div class="metric-label">${t('device.metrics.errors')}</div>
<div class="metric-value" data-tm="errors">---</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.uptime')}</div>
<div class="metric-value" data-tm="uptime">---</div>
</div>
<div class="target-metrics-collapse">
<button type="button" class="target-metrics-toggle" onclick="this.parentElement.classList.toggle('open')">${t('targets.metrics.pipeline')}</button>
<div class="target-metrics-animate">
<div class="metrics-grid target-metrics-expanded">
<div class="timing-breakdown" data-tm="timing" style="grid-column:1/-1"></div>
<div class="metric">
<div class="metric-label">${t('device.metrics.frames')}</div>
<div class="metric-value" data-tm="frames">---</div>
</div>
${state.needs_keepalive !== false ? `
<div class="metric">
<div class="metric-label">${t('device.metrics.keepalive')}</div>
<div class="metric-value" data-tm="keepalive">---</div>
</div>
` : ''}
<div class="metric">
<div class="metric-label">${t('device.metrics.errors')}</div>
<div class="metric-value" data-tm="errors">---</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.uptime')}</div>
<div class="metric-value" data-tm="uptime">---</div>
</div>
</div>
</div>
</div>
` : ''}