diff --git a/server/src/ledgrab/static/css/cards.css b/server/src/ledgrab/static/css/cards.css index dbe2c6e..36d8bd6 100644 --- a/server/src/ledgrab/static/css/cards.css +++ b/server/src/ledgrab/static/css/cards.css @@ -89,8 +89,8 @@ section { .displays-grid, .devices-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; + grid-template-columns: repeat(auto-fill, minmax(min(380px, 100%), 1fr)); + gap: 14px; } .devices-grid > .loading, diff --git a/server/src/ledgrab/static/js/core/mod-card.ts b/server/src/ledgrab/static/js/core/mod-card.ts index 82369e0..c51e617 100644 --- a/server/src/ledgrab/static/js/core/mod-card.ts +++ b/server/src/ledgrab/static/js/core/mod-card.ts @@ -47,6 +47,13 @@ export interface ModMetricOpts { * secondary line (e.g. LED chip + RGB/RGBW). The default display * font is too coarse for string identifiers. */ variant?: 'text-stack'; + /** Extra raw HTML appended after the .v element inside the cell — + * used to embed a sparkline canvas (`.mod-metric-spark-canvas`) + * into the FPS metric, mirroring the dashboard pattern. */ + extra?: string; + /** Extra data attributes on the .v element (e.g. data-fps-text for + * cached lookup during live updates). */ + valueDataAttrs?: Record; } export interface ModChipOpts { @@ -281,8 +288,12 @@ export function renderModMetrics(metrics: ModMetricOpts[]): string { const titleAttr = m.title ? ` title="${escapeHtml(m.title)}"` : ''; const vCls = ['v', m.accent ? 'signal' : '', m.error ? 'has-errors' : ''].filter(Boolean).join(' '); const vIdAttr = m.valueId ? ` id="${m.valueId}"` : ''; + const vDataAttrs = m.valueDataAttrs + ? ' ' + Object.entries(m.valueDataAttrs).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(' ') + : ''; const labelHtml = `${m.icon || ''} ${escapeHtml(m.k)}`; - return `
${labelHtml}${m.v}
`; + const extraHtml = m.extra || ''; + return `
${labelHtml}${m.v}${extraHtml}
`; }).join(''); return `
${cells}
`; } diff --git a/server/src/ledgrab/static/js/features/ha-light-targets.ts b/server/src/ledgrab/static/js/features/ha-light-targets.ts index 26af582..9cf2f6d 100644 --- a/server/src/ledgrab/static/js/features/ha-light-targets.ts +++ b/server/src/ledgrab/static/js/features/ha-light-targets.ts @@ -8,7 +8,7 @@ import { logError } from '../core/log.ts'; import { t } from '../core/i18n.ts'; import { Modal } from '../core/modal.ts'; import { showToast, showConfirm, formatUptime } from '../core/ui.ts'; -import { ICON_EDIT, ICON_START, ICON_STOP, ICON_TRASH, ICON_FPS, ICON_OK, ICON_WARNING, ICON_SUN, getColorStripIcon, getValueSourceIcon, getHAEntityIcon } from '../core/icons.ts'; +import { ICON_EDIT, ICON_START, ICON_STOP, ICON_TRASH, ICON_OK, ICON_WARNING, ICON_CLOCK, ICON_SUN, getColorStripIcon, getValueSourceIcon, getHAEntityIcon } from '../core/icons.ts'; import * as P from '../core/icon-paths.ts'; import { EntitySelect } from '../core/entity-palette.ts'; import { wrapCard } from '../core/card-colors.ts'; @@ -560,19 +560,20 @@ export function createHALightTargetCard(target: any, haSourceMap: Record---', accent: true, title: t('targets.fps'), }); metrics.push({ - k: t('device.metrics.uptime') || 'UPTIME', + k: t('device.metrics.uptime') || 'Uptime', + icon: ICON_CLOCK, v: '---', title: t('device.metrics.uptime'), }); metrics.push({ k: 'HA', + icon: state.ha_connected ? ICON_OK : ICON_WARNING, v: '---', title: 'Home Assistant connection', }); diff --git a/server/src/ledgrab/static/js/features/targets.ts b/server/src/ledgrab/static/js/features/targets.ts index ad2f73d..7b03618 100644 --- a/server/src/ledgrab/static/js/features/targets.ts +++ b/server/src/ledgrab/static/js/features/targets.ts @@ -22,7 +22,7 @@ import { createHALightTargetCard, initHALightTargetDelegation, patchHALightTarge import { getValueSourceIcon, getTargetTypeIcon, getDeviceTypeIcon, getColorStripIcon, ICON_EDIT, ICON_START, ICON_STOP, - ICON_FPS, ICON_LED_PREVIEW, + ICON_LED_PREVIEW, ICON_CLOCK, ICON_OK, ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP, ICON_WARNING, ICON_PALETTE, ICON_WRENCH, ICON_TEMPLATE, ICON_TRASH, } from '../core/icons.ts'; @@ -919,11 +919,14 @@ function _patchTargetMetrics(target: any) { if (fps) { const effFps = state.fps_effective; const tgtFps = state.fps_target || 0; - const fpsLabel = (effFps != null && effFps < tgtFps) - ? `${state.fps_current ?? 0}/${effFps}↓${tgtFps}` - : `${state.fps_current ?? 0}/${tgtFps}`; + const fpsCurrent = state.fps_current ?? 0; + const fpsActual = state.fps_actual?.toFixed(1) ?? '0.0'; + const fpsTargetLabel = (effFps != null && effFps < tgtFps) + ? `${fpsCurrent}/${effFps}↓${tgtFps}` + : `${fpsCurrent}/${tgtFps}`; const unreachableClass = state.device_streaming_reachable === false ? ' fps-unreachable' : ''; - fps.innerHTML = `${fpsLabel}`; + fps.innerHTML = `${fpsTargetLabel}` + + `avg ${fpsActual}`; } // Update health dot to reflect streaming reachability when processing @@ -1068,39 +1071,41 @@ export function createTargetCard(target: LedOutputTarget & { state?: any; metric } // ── Headline metric strip — shown when running so live FPS / errors - // pop in the instrument-readout style. The full pipeline detail - // sits below in the collapsible block. ── + // pop in the instrument-readout style. FPS cell embeds a + // sparkline canvas (mod-metric-spark-canvas) underneath the + // big numeric, mirroring the dashboard pattern. The full + // pipeline detail sits below in the collapsible block. ── const metrics: ModMetricOpts[] = []; if (isProcessing) { + const fpsTargetVal = state.fps_target || target.fps || 30; metrics.push({ - k: t('targets.fps') || 'FPS', - icon: ICON_FPS, - v: '---', + k: 'FPS', + v: '---', accent: true, title: t('targets.fps'), - }); - const errCount = tgtMetrics.errors_count || 0; - metrics.push({ - k: t('device.metrics.errors') || 'ERR', - v: '---', - error: errCount > 0, - icon: errCount > 0 ? ICON_WARNING : '', - title: t('device.metrics.errors'), + valueDataAttrs: { 'data-tm': 'fps' }, + extra: ``, }); metrics.push({ - k: t('device.metrics.uptime') || 'UPTIME', + k: t('device.metrics.uptime') || 'Uptime', + icon: ICON_CLOCK, v: '---', title: t('device.metrics.uptime'), }); + const errCount = tgtMetrics.errors_count || 0; + metrics.push({ + k: t('device.metrics.errors') || 'Errors', + icon: errCount > 0 ? ICON_WARNING : ICON_OK, + v: '---', + error: errCount > 0, + title: t('device.metrics.errors'), + }); } - // ── Live-update raw HTML: FPS sparkline + collapsible pipeline ── + // ── Live-update raw HTML: collapsible pipeline detail (frames / + // keepalive / timing breakdown). The FPS sparkline now lives + // inside the FPS metric cell, not in a separate row. ── const extraHtml = isProcessing ? ` -
-
- -
-