From 9fbe4e5bf914b2a8e1677cbad401255d3422245e Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sun, 5 Apr 2026 23:54:57 +0200 Subject: [PATCH] ui: show runtime state in control health --- internal/control/control.go | 3 +++ internal/control/control_test.go | 21 ++++++++++++++++++-- internal/control/ui.html | 33 +++++++++++++++++++++++++++++--- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/internal/control/control.go b/internal/control/control.go index 5509199..5ec9a97 100644 --- a/internal/control/control.go +++ b/internal/control/control.go @@ -145,6 +145,9 @@ func (s *Server) handleStatus(w http.ResponseWriter, _ *http.Request) { if queue, ok := stats["queue"]; ok { status["queue"] = queue } + if runtimeState, ok := stats["state"]; ok { + status["runtimeState"] = runtimeState + } } } diff --git a/internal/control/control_test.go b/internal/control/control_test.go index a42ca51..f7e1c4d 100644 --- a/internal/control/control_test.go +++ b/internal/control/control_test.go @@ -8,8 +8,8 @@ import ( "net/http/httptest" "testing" - cfgpkg "github.com/jan/fm-rds-tx/internal/config" "github.com/jan/fm-rds-tx/internal/audio" + cfgpkg "github.com/jan/fm-rds-tx/internal/config" "github.com/jan/fm-rds-tx/internal/output" ) @@ -92,6 +92,23 @@ func TestStatusReportsQueueStats(t *testing.T) { } } +func TestStatusReportsRuntimeState(t *testing.T) { + srv := NewServer(cfgpkg.Default()) + srv.SetTXController(&fakeTXController{stats: map[string]any{"state": "faulted"}}) + rec := httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil)) + if rec.Code != 200 { + t.Fatalf("status: %d", rec.Code) + } + var body map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal runtime state: %v", err) + } + if body["runtimeState"] != "faulted" { + t.Fatalf("expected runtimeState faulted, got %v", body["runtimeState"]) + } +} + func TestDryRunEndpoint(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() @@ -301,4 +318,4 @@ func (f *fakeTXController) TXStats() map[string]any { return map[string]any{} } func (f *fakeTXController) UpdateConfig(_ LivePatch) error { return f.updateErr } -func (f *fakeTXController) ResetFault() error { return f.resetErr } +func (f *fakeTXController) ResetFault() error { return f.resetErr } diff --git a/internal/control/ui.html b/internal/control/ui.html index 156d1e1..374948d 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -1764,14 +1764,41 @@ function renderToggle(key, toggleId, labelId) { updateText(labelId, busy ? '...' : (on ? 'ON' : 'OFF')); } +function runtimeStateClass(engineState) { + const normalized = String(engineState || '').toLowerCase(); + if (!normalized) { + return 'warn'; + } + switch (normalized) { + case 'faulted': + return 'err'; + case 'muted': + case 'degraded': + case 'prebuffering': + case 'arming': + case 'stopping': + case 'idle': + case 'unknown': + return 'warn'; + default: + return 'good'; + } +} + function updateHealth(engine, audioStream) { engine = engine || {}; updateText('health-http', state.server.configOk ? 'OK' : 'OFFLINE'); $('health-http').className = 'val ' + (state.server.configOk ? 'good' : 'err'); - const runtimeState = state.server.runtimeOk ? 'OK' : 'WAITING'; - updateText('health-runtime', runtimeState); - $('health-runtime').className = 'val ' + (state.server.runtimeOk ? 'good' : 'warn'); + let runtimeLabel = 'WAITING'; + let runtimeClass = 'warn'; + if (state.server.runtimeOk) { + const engineStateName = String(engine.state || 'unknown'); + runtimeLabel = engineStateName.toUpperCase(); + runtimeClass = runtimeStateClass(engineStateName); + } + updateText('health-runtime', runtimeLabel); + $('health-runtime').className = 'val ' + runtimeClass; const runtimeIndicator = engine.runtimeIndicator; const indicatorLabels = {