From 14501a3925c41dda7a30bc981e6e62d285cceb69 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Mon, 6 Apr 2026 00:05:48 +0200 Subject: [PATCH] feat: highlight runtime state transitions --- docs/pro-runtime-hardening-workboard.md | 4 +++ internal/control/ui.html | 37 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/docs/pro-runtime-hardening-workboard.md b/docs/pro-runtime-hardening-workboard.md index c0e2653..7be3c93 100644 --- a/docs/pro-runtime-hardening-workboard.md +++ b/docs/pro-runtime-hardening-workboard.md @@ -281,6 +281,8 @@ Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständ - Control-plane now exposes `POST /runtime/fault/reset` so operators can acknowledge `faulted` state; `TestRuntimeFaultReset*` covers the new HTTP path. - Control-plane UI now also offers a Danger Zone `Reset Fault` button that calls the same endpoint so operators can acknowledge faults from the dashboard. +- Control-plane UI now posts an ops toast/log entry whenever the runtime state shifts so escalations and manual acknowledgements are immediately visible. + ## Zielzustände laut Konzept - `idle` @@ -331,12 +333,14 @@ Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständ | 2026-04-05 | Faulted escalation on persistent critical queue | `muted` now surfaces `RuntimeStateFaulted` when queue health stays critical and metrics capture every transition. | | 2026-04-05 | Manual fault reset endpoint | Added `POST /runtime/fault/reset` so operators can acknowledge `faulted` before the supervisor re-enters recovery. | | 2026-04-05 | Fault-reset UI shortcut | Danger Zone now hosts a Reset Fault button wired to `/runtime/fault/reset` so operators get an in-app acknowledgement path without manual HTTP calls. | +| 2026-04-06 | Runtime transition visibility cue | Control UI now posts toast/log entries for runtime state shifts so ops instantly sees escalations and manual reset acknowledgements. | ## WS-02 Verifikation | Datum | Fokus | Ergebnis | |---|---|---| | 2026-04-05 | Faulted path + transition counters | `go test ./...` exercises `TestEngineFaultsAfterMutedCriticalStreak` and `TestRuntimeTransitionCounters`, while `/runtime` now surfaces `engine.degradedTransitions`, `engine.mutedTransitions`, `engine.faultedTransitions`, `engine.faultCount`, and the last fault via `txBridge`. | | 2026-04-05 | Runtime fault reset API | `go test ./...` now runs `TestRuntimeFaultReset*`, verifying the new HTTP path and controller error scenarios. | +| 2026-04-06 | Runtime transition visibility | ✅ `go test ./...`; manual UI smoke verification still pending to ensure the toast/log flow shows every runtime shift. | --- diff --git a/internal/control/ui.html b/internal/control/ui.html index 374948d..d105404 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -1163,6 +1163,7 @@ const state = { configOk: false, runtimeOk: false, }, + lastRuntimeState: '', draft: { frequencyMHz: undefined, ps: undefined, @@ -1338,6 +1339,7 @@ async function loadRuntime({ silent = true } = {}) { state.server.runtime = runtime; state.server.runtimeOk = true; state.server.lastRuntimeAt = nowTs(); + notifyRuntimeTransition(runtime.engine); pushHistory(runtime); setConnection(true, state.pendingRequests > 0 ? 'busy' : 'connected'); render(); @@ -1785,6 +1787,41 @@ function runtimeStateClass(engineState) { } } + + +function normalizeRuntimeState(stateName) { + const normalized = (typeof stateName === 'string' ? stateName.trim().toLowerCase() : ''); + return normalized || 'idle'; +} + +function runtimeStateSeverity(stateName) { + const normalized = normalizeRuntimeState(stateName); + switch (normalized) { + case 'running': + return 'ok'; + case 'degraded': + case 'muted': + return 'warn'; + case 'faulted': + return 'err'; + default: + return 'info'; + } +} + +function notifyRuntimeTransition(engine) { + if (!engine) return; + const next = normalizeRuntimeState(engine.state); + const prev = state.lastRuntimeState; + state.lastRuntimeState = next; + if (!prev || prev === next) return; + const message = `Runtime ${prev.toUpperCase()} → ${next.toUpperCase()}`; + const severity = runtimeStateSeverity(next); + const logLevel = severity === 'err' ? 'err' : (severity === 'warn' ? 'warn' : 'info'); + toast(message, severity); + log(message, logLevel); +} + function updateHealth(engine, audioStream) { engine = engine || {}; updateText('health-http', state.server.configOk ? 'OK' : 'OFFLINE');