| @@ -1180,6 +1180,7 @@ 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">Queue Fill</div><div class="val" id="health-queue-fill">--</div></div> | |||
| <div class="health-line"><div class="name">Underrun Streak</div><div class="val" id="health-underrun-streak">--</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> | |||
| @@ -1972,7 +1973,7 @@ function render() { | |||
| updateText('info-fmmod', fmtBool(cfg.fm?.fmModulationEnabled)); | |||
| updateText('info-live', engine.state ? `${String(engine.state).toUpperCase()} / ${state.server.runtimeOk ? 'runtime ok' : 'runtime pending'}` : (state.server.configOk ? 'config only' : '--')); | |||
| updateHealth(engine, audioStream); | |||
| updateHealth(engine, driver, audioStream); | |||
| updateFaultHistory(engine); | |||
| updateTransitionHistory(); | |||
| updateResetHint(engine); | |||
| @@ -2077,8 +2078,9 @@ function notifyRuntimeTransition(engine, pushHistory = true) { | |||
| } | |||
| function updateHealth(engine, audioStream) { | |||
| function updateHealth(engine, driver, audioStream) { | |||
| engine = engine || {}; | |||
| driver = driver || {}; | |||
| updateText('health-http', state.server.configOk ? 'OK' : 'OFFLINE'); | |||
| $('health-http').className = 'val ' + (state.server.configOk ? 'good' : 'err'); | |||
| @@ -2175,6 +2177,35 @@ function updateHealth(engine, audioStream) { | |||
| queueFillEl.className = 'val ' + queueFillClass; | |||
| } | |||
| const streakEl = $('health-underrun-streak'); | |||
| if (streakEl) { | |||
| const streakRaw = driver?.underrunStreak; | |||
| const streakMaxRaw = driver?.maxUnderrunStreak; | |||
| const streakCurrent = Number.isFinite(Number(streakRaw)) ? Number(streakRaw) : null; | |||
| const streakMax = Number.isFinite(Number(streakMaxRaw)) ? Number(streakMaxRaw) : null; | |||
| let streakLabel = '--'; | |||
| if (streakCurrent != null) { | |||
| streakLabel = String(streakCurrent); | |||
| if (streakMax != null) { | |||
| streakLabel += ` (max ${streakMax})`; | |||
| } | |||
| } else if (streakMax != null) { | |||
| streakLabel = `Max ${streakMax}`; | |||
| } | |||
| let streakSeverity = ''; | |||
| if (streakCurrent != null || streakMax != null) { | |||
| const highestStreak = Math.max( | |||
| streakCurrent != null ? streakCurrent : 0, | |||
| streakMax != null ? streakMax : 0 | |||
| ); | |||
| if (highestStreak >= 6) streakSeverity = ' err'; | |||
| else if (highestStreak > 0) streakSeverity = ' warn'; | |||
| else streakSeverity = ' good'; | |||
| } | |||
| streakEl.textContent = streakLabel; | |||
| streakEl.className = 'val' + streakSeverity; | |||
| } | |||
| const last = Math.max(state.server.lastConfigAt || 0, state.server.lastRuntimeAt || 0); | |||
| updateText('health-last', ageString(last)); | |||