diff --git a/docs/pro-runtime-hardening-workboard.md b/docs/pro-runtime-hardening-workboard.md
index e0f5f3b..ce14fdf 100644
--- a/docs/pro-runtime-hardening-workboard.md
+++ b/docs/pro-runtime-hardening-workboard.md
@@ -385,11 +385,13 @@ Vollständige Sichtbarkeit auf Runtime, Queue, Writer, Generator, RF-Selbsttests
| 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. |
+| 2026-04-06 | Queue fill visibility | Added queue fill ratio health line and sparklines to highlight real-time queue pressure alongside high-watermark trends. |
## WS-04 Verifikation
| 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. |
+| 2026-04-06 | Queue fill visibility | `go test ./...` plus UI smoke check confirm queue fill stats stay available and the new sparkline/health line react to queue health changes. |
---
diff --git a/internal/control/ui.html b/internal/control/ui.html
index 8764bff..7ff20f8 100644
--- a/internal/control/ui.html
+++ b/internal/control/ui.html
@@ -1179,11 +1179,16 @@ input.input-error {
+
+
@@ -1319,6 +1324,7 @@ const state = {
underruns: [],
tx: [],
highWatermark: [],
+ queueFill: [],
},
runtimeTransitions: [],
freqPresetIndex: 0,
@@ -1497,6 +1503,8 @@ function pushHistory(runtime) {
const highWatermarkDurationSeconds = Number(audio.highWatermarkDurationSeconds);
const normalizedHighWatermark = Number.isFinite(highWatermarkDurationSeconds) ? highWatermarkDurationSeconds : 0;
pushChart(state.charts.highWatermark, normalizedHighWatermark);
+ const queueFill = Number(engine.queue?.fillLevel ?? 0);
+ pushChart(state.charts.queueFill, Number.isFinite(queueFill) ? queueFill : 0);
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);
@@ -1988,8 +1996,13 @@ function render() {
hasHighWatermarkDuration ? highWatermarkDurationSeconds : 0,
hasBufferedDuration ? bufferedDurationSeconds : 0
);
+ const queueHealthRaw = String(engine.queue?.health || '').toLowerCase();
+ let queueSparkMode = 'good';
+ if (queueHealthRaw === 'critical') queueSparkMode = 'err';
+ else if (queueHealthRaw === 'low') queueSparkMode = 'warn';
drawSparkline('spark-audio', state.charts.audio, 'good', 1);
drawSparkline('spark-high-watermark', state.charts.highWatermark, highWatermarkMode, sparkHighWatermarkMax);
+ drawSparkline('spark-queue-fill', state.charts.queueFill, queueSparkMode, 1);
drawSparkline('spark-underruns', state.charts.underruns, underruns > 0 ? 'err' : 'warn');
drawSparkline('spark-tx', state.charts.tx, txStateValue === 'running' ? 'good' : 'warn', 1);
applyMobilePanelDefaults();
@@ -2143,6 +2156,25 @@ function updateHealth(engine, audioStream) {
}
updateText('health-buffer-highwater', highWatermarkLabel);
+ const queueFill = Number(engine.queue?.fillLevel);
+ const queueHealthRaw = String(engine.queue?.health || '').toLowerCase();
+ const queueHealthLabel = queueHealthRaw ? queueHealthRaw[0].toUpperCase() + queueHealthRaw.slice(1) : '';
+ let queueFillLabel = '--';
+ if (Number.isFinite(queueFill)) {
+ queueFillLabel = fmtPercent(queueFill);
+ if (queueHealthLabel) queueFillLabel += ` · ${queueHealthLabel}`;
+ } else if (queueHealthLabel) {
+ queueFillLabel = queueHealthLabel;
+ }
+ updateText('health-queue-fill', queueFillLabel);
+ const queueFillEl = $('health-queue-fill');
+ if (queueFillEl) {
+ let queueFillClass = 'good';
+ if (queueHealthRaw === 'critical') queueFillClass = 'err';
+ else if (queueHealthRaw === 'low') queueFillClass = 'warn';
+ queueFillEl.className = 'val ' + queueFillClass;
+ }
+
const last = Math.max(state.server.lastConfigAt || 0, state.server.lastRuntimeAt || 0);
updateText('health-last', ageString(last));