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:
@@ -742,6 +742,54 @@ ul.section-tip li {
|
||||
color: #ff5252;
|
||||
}
|
||||
|
||||
/* Error indicator next to target name */
|
||||
.target-error-indicator {
|
||||
display: none;
|
||||
color: var(--danger-color);
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.target-error-indicator .icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.target-error-indicator.visible {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Collapsible target pipeline metrics */
|
||||
.target-metrics-collapse {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.target-metrics-toggle {
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 0;
|
||||
user-select: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
.target-metrics-toggle::before {
|
||||
content: '▸ ';
|
||||
}
|
||||
.target-metrics-collapse.open .target-metrics-toggle::before {
|
||||
content: '▾ ';
|
||||
}
|
||||
.target-metrics-animate {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.25s ease;
|
||||
}
|
||||
.target-metrics-collapse.open .target-metrics-animate {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
.target-metrics-animate > .target-metrics-expanded {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Timing breakdown bar */
|
||||
.timing-breakdown {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -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,9 +952,12 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
||||
<span class="metric-value" data-tm="fps">---</span>
|
||||
</div>
|
||||
</div>
|
||||
${state.timing_total_ms != null ? `
|
||||
</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>
|
||||
@@ -965,6 +977,8 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
||||
<div class="metric-value" data-tm="uptime">---</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${_buildLedPreviewHtml(target.id, device, bvsId)}`,
|
||||
|
||||
@@ -458,6 +458,7 @@
|
||||
"targets.source": "Source:",
|
||||
"targets.source.hint": "Which picture source to capture and process",
|
||||
"targets.source.none": "-- No source assigned --",
|
||||
"targets.metrics.pipeline": "Pipeline details",
|
||||
"targets.fps": "Target FPS:",
|
||||
"targets.fps.hint": "Target frames per second for capture and LED updates (1-90)",
|
||||
"targets.fps.rec": "Hardware max ≈ {fps} fps ({leds} LEDs)",
|
||||
|
||||
@@ -458,6 +458,7 @@
|
||||
"targets.source": "Источник:",
|
||||
"targets.source.hint": "Какой источник изображения захватывать и обрабатывать",
|
||||
"targets.source.none": "-- Источник не назначен --",
|
||||
"targets.metrics.pipeline": "Детали конвейера",
|
||||
"targets.fps": "Целевой FPS:",
|
||||
"targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (1-90)",
|
||||
"targets.fps.rec": "Макс. аппаратный ≈ {fps} fps ({leds} LED)",
|
||||
|
||||
@@ -458,6 +458,7 @@
|
||||
"targets.source": "源:",
|
||||
"targets.source.hint": "要采集和处理的图片源",
|
||||
"targets.source.none": "-- 未分配源 --",
|
||||
"targets.metrics.pipeline": "管线详情",
|
||||
"targets.fps": "目标 FPS:",
|
||||
"targets.fps.hint": "采集和 LED 更新的目标帧率(1-90)",
|
||||
"targets.fps.rec": "硬件最大 ≈ {fps} fps({leds} 个 LED)",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Navigation: network-first with offline fallback
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'ledgrab-v17';
|
||||
const CACHE_NAME = 'ledgrab-v18';
|
||||
|
||||
// Only pre-cache static assets (no auth required).
|
||||
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
||||
|
||||
Reference in New Issue
Block a user