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