From 9baea0ea057aec1133914461f857958de2e6eb6f Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Mon, 6 Apr 2026 03:56:42 +0200 Subject: [PATCH] feat: add high watermark trend sparkline --- docs/pro-runtime-hardening-workboard.md | 8 ++++-- internal/control/ui.html | 38 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docs/pro-runtime-hardening-workboard.md b/docs/pro-runtime-hardening-workboard.md index 8fb6a52..e0f5f3b 100644 --- a/docs/pro-runtime-hardening-workboard.md +++ b/docs/pro-runtime-hardening-workboard.md @@ -382,10 +382,14 @@ Vollständige Sichtbarkeit auf Runtime, Queue, Writer, Generator, RF-Selbsttests - `rf_selftest_rds_57k_db` ## WS-04 Entscheidungslog -- Noch leer +| Datum | Entscheidung | Notiz | +| --- | --- | --- | +| 2026-04-06 | High-watermark trend sparkline | Captured audio high-watermark duration history and surface it as a new Health-panel sparkline for queue pressure visibility. | ## WS-04 Verifikation -- Noch leer +| Datum | Fokus | Ergebnis | +| --- | --- | --- | +| 2026-04-06 | High-watermark trend sparkline | `go test ./...` plus manual UI check confirm the new sparkline updates with runtime audio stats. | --- diff --git a/internal/control/ui.html b/internal/control/ui.html index e1a1eaa..8764bff 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -770,6 +770,16 @@ input.input-error { .health-line .val.good { color: var(--green); } .health-line .val.warn { color: var(--amber); } .health-line .val.err { color: var(--accent); } +.health-trend { + margin-top: 10px; +} +.health-trend-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-muted); + margin-bottom: 6px; +} .fault-history { margin-top: 12px; @@ -1170,6 +1180,10 @@ input.input-error {
Buffer Duration
--
High Watermark
--
Last Update
--
+
+
High Watermark Trend
+ +
@@ -1304,6 +1318,7 @@ const state = { audio: [], underruns: [], tx: [], + highWatermark: [], }, runtimeTransitions: [], freqPresetIndex: 0, @@ -1479,6 +1494,9 @@ function pushHistory(runtime) { const driver = runtime.driver || {}; const audio = runtime.audioStream || {}; pushChart(state.charts.audio, typeof audio.buffered === 'number' ? audio.buffered : 0); + const highWatermarkDurationSeconds = Number(audio.highWatermarkDurationSeconds); + const normalizedHighWatermark = Number.isFinite(highWatermarkDurationSeconds) ? highWatermarkDurationSeconds : 0; + pushChart(state.charts.highWatermark, normalizedHighWatermark); pushChart(state.charts.underruns, Number(engine.underruns ?? driver.underruns ?? 0)); const txState = String(engine.state || 'idle').toLowerCase(); pushChart(state.charts.tx, txState === 'running' ? 1 : state.txBusy ? 0.55 : 0.05); @@ -1951,7 +1969,27 @@ function render() { updateTransitionHistory(); updateResetHint(engine); updateMeters(engine, driver, audioStream); + const highWatermarkDurationSecondsRaw = audioStream?.highWatermarkDurationSeconds; + const highWatermarkDurationSeconds = Number(highWatermarkDurationSecondsRaw); + const highWatermarkFramesRaw = audioStream?.highWatermark; + const highWatermarkFrames = Number.isFinite(Number(highWatermarkFramesRaw)) ? Number(highWatermarkFramesRaw) : 0; + const capacityRaw = audioStream?.capacity; + const capacity = Number.isFinite(Number(capacityRaw)) ? Number(capacityRaw) : 0; + const bufferedDurationSecondsRaw = audioStream?.bufferedDurationSeconds; + const bufferedDurationSeconds = Number(bufferedDurationSecondsRaw); + const hasBufferedDuration = Number.isFinite(bufferedDurationSeconds); + const hasHighWatermarkDuration = Number.isFinite(highWatermarkDurationSeconds); + const highWatermarkRatio = capacity > 0 ? Math.min(1, highWatermarkFrames / capacity) : 0; + let highWatermarkMode = 'good'; + if (highWatermarkRatio >= 0.95) highWatermarkMode = 'err'; + else if (highWatermarkRatio >= 0.65) highWatermarkMode = 'warn'; + const sparkHighWatermarkMax = Math.max( + 1, + hasHighWatermarkDuration ? highWatermarkDurationSeconds : 0, + hasBufferedDuration ? bufferedDurationSeconds : 0 + ); drawSparkline('spark-audio', state.charts.audio, 'good', 1); + drawSparkline('spark-high-watermark', state.charts.highWatermark, highWatermarkMode, sparkHighWatermarkMax); drawSparkline('spark-underruns', state.charts.underruns, underruns > 0 ? 'err' : 'warn'); drawSparkline('spark-tx', state.charts.tx, txStateValue === 'running' ? 'good' : 'warn', 1); applyMobilePanelDefaults();