diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css
index a2584e5..89a4502 100644
--- a/server/src/wled_controller/static/css/cards.css
+++ b/server/src/wled_controller/static/css/cards.css
@@ -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;
diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js
index 9b05946..803f4d0 100644
--- a/server/src/wled_controller/static/js/features/targets.js
+++ b/server/src/wled_controller/static/js/features/targets.js
@@ -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
${escapeHtml(target.name)}
+ ${ICON_WARNING}
@@ -943,26 +952,31 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
---
- ${state.timing_total_ms != null ? `
-
- ` : ''}
-
-
${t('device.metrics.frames')}
-
---
-
- ${state.needs_keepalive !== false ? `
-
-
${t('device.metrics.keepalive')}
-
---
-
- ` : ''}
-
-
${t('device.metrics.errors')}
-
---
-
-
-
${t('device.metrics.uptime')}
-
---
+
+
+
+
+
+
+
+
${t('device.metrics.frames')}
+
---
+
+ ${state.needs_keepalive !== false ? `
+
+
${t('device.metrics.keepalive')}
+
---
+
+ ` : ''}
+
+
${t('device.metrics.errors')}
+
---
+
+
+
${t('device.metrics.uptime')}
+
---
+
+
` : ''}
diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json
index 925fce4..f9e5aaa 100644
--- a/server/src/wled_controller/static/locales/en.json
+++ b/server/src/wled_controller/static/locales/en.json
@@ -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)",
diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json
index 683f3fd..3419033 100644
--- a/server/src/wled_controller/static/locales/ru.json
+++ b/server/src/wled_controller/static/locales/ru.json
@@ -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)",
diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json
index dd8ba26..0d8cb01 100644
--- a/server/src/wled_controller/static/locales/zh.json
+++ b/server/src/wled_controller/static/locales/zh.json
@@ -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)",
diff --git a/server/src/wled_controller/static/sw.js b/server/src/wled_controller/static/sw.js
index b4111d2..ed6b1dd 100644
--- a/server/src/wled_controller/static/sw.js
+++ b/server/src/wled_controller/static/sw.js
@@ -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.