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;
|
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 bar */
|
||||||
.timing-breakdown {
|
.timing-breakdown {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
|
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
|
||||||
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
|
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_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
|
||||||
|
ICON_WARNING,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
import { EntitySelect } from '../core/entity-palette.js';
|
import { EntitySelect } from '../core/entity-palette.js';
|
||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
@@ -881,6 +882,13 @@ function _patchTargetMetrics(target) {
|
|||||||
const errors = card.querySelector('[data-tm="errors"]');
|
const errors = card.querySelector('[data-tm="errors"]');
|
||||||
if (errors) errors.textContent = metrics.errors_count || 0;
|
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"]');
|
const uptime = card.querySelector('[data-tm="uptime"]');
|
||||||
if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds);
|
if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds);
|
||||||
}
|
}
|
||||||
@@ -922,6 +930,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
||||||
${escapeHtml(target.name)}
|
${escapeHtml(target.name)}
|
||||||
|
<span class="target-error-indicator" title="${t('device.metrics.errors')}">${ICON_WARNING}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stream-card-props">
|
<div class="stream-card-props">
|
||||||
@@ -943,9 +952,12 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<span class="metric-value" data-tm="fps">---</span>
|
<span class="metric-value" data-tm="fps">---</span>
|
||||||
</div>
|
</div>
|
||||||
</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="timing-breakdown" data-tm="timing" style="grid-column:1/-1"></div>
|
||||||
` : ''}
|
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.frames')}</div>
|
<div class="metric-label">${t('device.metrics.frames')}</div>
|
||||||
<div class="metric-value" data-tm="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 class="metric-value" data-tm="uptime">---</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
${_buildLedPreviewHtml(target.id, device, bvsId)}`,
|
${_buildLedPreviewHtml(target.id, device, bvsId)}`,
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
"targets.source": "Source:",
|
"targets.source": "Source:",
|
||||||
"targets.source.hint": "Which picture source to capture and process",
|
"targets.source.hint": "Which picture source to capture and process",
|
||||||
"targets.source.none": "-- No source assigned --",
|
"targets.source.none": "-- No source assigned --",
|
||||||
|
"targets.metrics.pipeline": "Pipeline details",
|
||||||
"targets.fps": "Target FPS:",
|
"targets.fps": "Target FPS:",
|
||||||
"targets.fps.hint": "Target frames per second for capture and LED updates (1-90)",
|
"targets.fps.hint": "Target frames per second for capture and LED updates (1-90)",
|
||||||
"targets.fps.rec": "Hardware max ≈ {fps} fps ({leds} LEDs)",
|
"targets.fps.rec": "Hardware max ≈ {fps} fps ({leds} LEDs)",
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
"targets.source": "Источник:",
|
"targets.source": "Источник:",
|
||||||
"targets.source.hint": "Какой источник изображения захватывать и обрабатывать",
|
"targets.source.hint": "Какой источник изображения захватывать и обрабатывать",
|
||||||
"targets.source.none": "-- Источник не назначен --",
|
"targets.source.none": "-- Источник не назначен --",
|
||||||
|
"targets.metrics.pipeline": "Детали конвейера",
|
||||||
"targets.fps": "Целевой FPS:",
|
"targets.fps": "Целевой FPS:",
|
||||||
"targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (1-90)",
|
"targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (1-90)",
|
||||||
"targets.fps.rec": "Макс. аппаратный ≈ {fps} fps ({leds} LED)",
|
"targets.fps.rec": "Макс. аппаратный ≈ {fps} fps ({leds} LED)",
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
"targets.source": "源:",
|
"targets.source": "源:",
|
||||||
"targets.source.hint": "要采集和处理的图片源",
|
"targets.source.hint": "要采集和处理的图片源",
|
||||||
"targets.source.none": "-- 未分配源 --",
|
"targets.source.none": "-- 未分配源 --",
|
||||||
|
"targets.metrics.pipeline": "管线详情",
|
||||||
"targets.fps": "目标 FPS:",
|
"targets.fps": "目标 FPS:",
|
||||||
"targets.fps.hint": "采集和 LED 更新的目标帧率(1-90)",
|
"targets.fps.hint": "采集和 LED 更新的目标帧率(1-90)",
|
||||||
"targets.fps.rec": "硬件最大 ≈ {fps} fps({leds} 个 LED)",
|
"targets.fps.rec": "硬件最大 ≈ {fps} fps({leds} 个 LED)",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - Navigation: network-first with offline fallback
|
* - Navigation: network-first with offline fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = 'ledgrab-v17';
|
const CACHE_NAME = 'ledgrab-v18';
|
||||||
|
|
||||||
// Only pre-cache static assets (no auth required).
|
// Only pre-cache static assets (no auth required).
|
||||||
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
||||||
|
|||||||
Reference in New Issue
Block a user