Kaynağa Gözat

feat: add high watermark trend sparkline

tags/v0.9.0
Jan Svabenik 1 ay önce
ebeveyn
işleme
9baea0ea05
2 değiştirilmiş dosya ile 44 ekleme ve 2 silme
  1. +6
    -2
      docs/pro-runtime-hardening-workboard.md
  2. +38
    -0
      internal/control/ui.html

+ 6
- 2
docs/pro-runtime-hardening-workboard.md Dosyayı Görüntüle

@@ -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. |

---



+ 38
- 0
internal/control/ui.html Dosyayı Görüntüle

@@ -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 {
<div class="health-line"><div class="name">Buffer Duration</div><div class="val" id="health-buffer-duration">--</div></div>
<div class="health-line"><div class="name">High Watermark</div><div class="val" id="health-buffer-highwater">--</div></div>
<div class="health-line"><div class="name">Last Update</div><div class="val" id="health-last">--</div></div>
<div class="health-trend">
<div class="health-trend-label">High Watermark Trend</div>
<svg class="spark warn" id="spark-high-watermark" viewBox="0 0 160 34" preserveAspectRatio="none"></svg>
</div>
</div>
</div>

@@ -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();


Yükleniyor…
İptal
Kaydet