From 91225157bf268f0446a34d3c2edf866ce6056790 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sun, 5 Apr 2026 23:28:08 +0200 Subject: [PATCH] ui: show fault telemetry in control health panel --- docs/pro-runtime-hardening-workboard.md | 2 ++ internal/control/ui.html | 42 ++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/pro-runtime-hardening-workboard.md b/docs/pro-runtime-hardening-workboard.md index 67219b7..362031b 100644 --- a/docs/pro-runtime-hardening-workboard.md +++ b/docs/pro-runtime-hardening-workboard.md @@ -277,6 +277,8 @@ Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständ - `evaluateRuntimeState` now waits for a short healthy streak before leaving `muted`, logging a degraded-severity recovery event once the queue settles. - Persistent queue-critical streaks while `muted` now escalate to `faulted` with `FaultSeverityFaulted`, keeping `RuntimeStateFaulted` observable. - `EngineStats` and `txBridge` now expose transition/fault counters plus `lastFault`, surfacing the new telemetry through `/runtime`. +- Control-plane UI now renders those WS-02 transition counters, fault count, and last-fault summary so operators can watch runtime escalations without digging through logs. + ## Zielzustände laut Konzept - `idle` diff --git a/internal/control/ui.html b/internal/control/ui.html index ff13122..59558e8 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -1079,6 +1079,9 @@ input.input-error {
Runtime
--
Runtime Signal
--
Runtime Alert
--
+
Transitions (D/M/F)
--
+
Fault Count
--
+
Last Fault
--
Audio Buffer
--
Last Update
--
@@ -1782,6 +1785,43 @@ function updateHealth(engine, audioStream) { const last = Math.max(state.server.lastConfigAt || 0, state.server.lastRuntimeAt || 0); updateText('health-last', ageString(last)); + + const transitionsAvailable = engine.degradedTransitions != null || engine.mutedTransitions != null || engine.faultedTransitions != null; + const transitionsText = transitionsAvailable ? `${Number(engine.degradedTransitions ?? 0)} / ${Number(engine.mutedTransitions ?? 0)} / ${Number(engine.faultedTransitions ?? 0)}` : '--'; + updateText('health-transitions', transitionsText); + + const faultCountValue = engine.faultCount != null ? Number(engine.faultCount) : 0; + const hasFaultCount = engine.faultCount != null; + updateText('health-fault-count', hasFaultCount ? String(faultCountValue) : '--'); + const faultCountEl = $('health-fault-count'); + if (faultCountEl) { + faultCountEl.className = 'val' + (hasFaultCount ? (faultCountValue > 0 ? ' warn' : ' good') : ''); + } + + const lastFaultEl = $('health-last-fault'); + const lastFault = engine.lastFault; + if (lastFaultEl) { + if (lastFault) { + const severity = String(lastFault.severity || '').toLowerCase(); + const severityClass = severity === 'faulted' ? 'err' : 'warn'; + const severityLabel = (lastFault.severity || 'Fault').toUpperCase(); + const reasonLabel = lastFault.reason ? ` ${lastFault.reason}` : ''; + const messageLabel = lastFault.message ? ` - ${lastFault.message}` : ''; + let whenLabel = ''; + if (lastFault.time) { + const parsed = new Date(lastFault.time); + if (!Number.isNaN(parsed.getTime())) { + whenLabel = ` @ ${parsed.toLocaleTimeString()}`; + } + } + const title = `${severityLabel}${reasonLabel}`; + updateText('health-last-fault', `${title}${messageLabel}${whenLabel}`); + lastFaultEl.className = 'val ' + severityClass; + } else { + lastFaultEl.className = 'val good'; + updateText('health-last-fault', 'None'); + } + } } function updateMeters(engine, driver, audioStream) { @@ -2004,4 +2044,4 @@ async function init() { init(); - \ No newline at end of file +