diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a58b26d..26195de 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -199,6 +199,7 @@ Ergaenzt seit dem ersten Geruest: - Backend ergaenzt den Read-Pfad um `received_at` und eine einfache `stale`-Ableitung - Backend bietet zusaetzlich eine kleine Uebersicht aller zuletzt meldenden Screens - Backend validiert den Statuspfad jetzt enger auf erlaubte Lifecycle-/Connectivity-Werte und leitet `stale` aus dem gemeldeten Intervall ab +- Backend leitet im Read-Pfad zusaetzlich ein kompaktes `derived_state` fuer Diagnosekonsumenten ab - dateibasierte Agent-Konfiguration zusaetzlich zu Env-Overrides - strukturierte Agent-Logs mit internem Health-Snapshot und signalgesteuertem Shutdown - erster periodischer HTTP-Status-Reporter im Agent diff --git a/docs/PLAYER-STATUS-HTTP.md b/docs/PLAYER-STATUS-HTTP.md index f113f60..b4f59f6 100644 --- a/docs/PLAYER-STATUS-HTTP.md +++ b/docs/PLAYER-STATUS-HTTP.md @@ -85,10 +85,17 @@ Zusaetzlich fuegt das Backend im Read-Pfad derzeit hinzu: - `received_at` als serverseitigen Annahmezeitpunkt des letzten gueltigen Reports - `stale` als einfache serverseitige Einordnung, ob der letzte Report bereits veraltet wirkt +- `derived_state` als zusammengefasste Diagnoseeinschaetzung fuer Konsumenten des Read-Pfads `stale` ist aktuell bewusst nur eine kleine Diagnosehilfe fuer die Entwicklungsstufe und noch kein vollstaendiges Online-/Offline-Modell fuer spaetere Admin-Oberflaechen. Die Schwelle wird derzeit einfach aus dem gemeldeten `heartbeat_every_seconds` abgeleitet: mehr als zwei Intervalle ohne neuen Report gelten als veraltet. +`derived_state` wird aktuell bewusst einfach abgeleitet: + +- `offline` bei `stale = true` oder `server_connectivity = offline` +- `degraded` bei `server_connectivity = degraded|unknown` oder wenn `status` nicht `running` ist +- `online` in den verbleibenden Faellen + ## Abgrenzung Noch nicht Teil dieser Stufe: diff --git a/server/backend/internal/httpapi/playerstatus.go b/server/backend/internal/httpapi/playerstatus.go index 6ddaa66..0727021 100644 --- a/server/backend/internal/httpapi/playerstatus.go +++ b/server/backend/internal/httpapi/playerstatus.go @@ -120,6 +120,7 @@ func handleGetLatestPlayerStatus(store playerStatusStore) http.HandlerFunc { } record.Stale = isStale(record, store.Now()) + record.DerivedState = deriveState(record) writeJSON(w, http.StatusOK, record) } @@ -133,6 +134,7 @@ func handleListLatestPlayerStatuses(store playerStatusStore) http.HandlerFunc { filtered := make([]playerStatusRecord, 0, len(records)) for i := range records { records[i].Stale = isStale(records[i], store.Now()) + records[i].DerivedState = deriveState(records[i]) if wantConnectivity != "" && records[i].ServerConnectivity != wantConnectivity { continue } @@ -183,3 +185,15 @@ func staleThresholdFor(record playerStatusRecord) time.Duration { return 2 * time.Minute } + +func deriveState(record playerStatusRecord) string { + if record.Stale || record.ServerConnectivity == "offline" { + return "offline" + } + + if record.ServerConnectivity == "degraded" || record.ServerConnectivity == "unknown" || record.Status != "running" { + return "degraded" + } + + return "online" +} diff --git a/server/backend/internal/httpapi/playerstatus_store.go b/server/backend/internal/httpapi/playerstatus_store.go index ec21569..3ffc345 100644 --- a/server/backend/internal/httpapi/playerstatus_store.go +++ b/server/backend/internal/httpapi/playerstatus_store.go @@ -13,6 +13,7 @@ type playerStatusRecord struct { ServerConnectivity string `json:"server_connectivity,omitempty"` ReceivedAt string `json:"received_at,omitempty"` Stale bool `json:"stale,omitempty"` + DerivedState string `json:"derived_state,omitempty"` ServerURL string `json:"server_url,omitempty"` MQTTBroker string `json:"mqtt_broker,omitempty"` HeartbeatEverySeconds int `json:"heartbeat_every_seconds,omitempty"` diff --git a/server/backend/internal/httpapi/playerstatus_test.go b/server/backend/internal/httpapi/playerstatus_test.go index ecf7615..1669e6d 100644 --- a/server/backend/internal/httpapi/playerstatus_test.go +++ b/server/backend/internal/httpapi/playerstatus_test.go @@ -295,6 +295,10 @@ func TestHandleGetLatestPlayerStatus(t *testing.T) { if got, want := response.Stale, false; got != want { t.Fatalf("response.Stale = %v, want %v", got, want) } + + if got, want := response.DerivedState, "degraded"; got != want { + t.Fatalf("response.DerivedState = %q, want %q", got, want) + } } func TestHandleGetLatestPlayerStatusMarksStaleRecords(t *testing.T) { @@ -329,6 +333,10 @@ func TestHandleGetLatestPlayerStatusMarksStaleRecords(t *testing.T) { if got, want := response.Stale, true; got != want { t.Fatalf("response.Stale = %v, want %v", got, want) } + + if got, want := response.DerivedState, "offline"; got != want { + t.Fatalf("response.DerivedState = %q, want %q", got, want) + } } func TestHandleGetLatestPlayerStatusUsesHeartbeatIntervalForFreshness(t *testing.T) { @@ -361,6 +369,36 @@ func TestHandleGetLatestPlayerStatusUsesHeartbeatIntervalForFreshness(t *testing } } +func TestHandleGetLatestPlayerStatusDerivesOnlineState(t *testing.T) { + store := newInMemoryPlayerStatusStore() + store.now = func() time.Time { + return time.Date(2026, 3, 22, 16, 0, 30, 0, time.UTC) + } + store.Save(playerStatusRecord{ + ScreenID: "online-screen", + Timestamp: "2026-03-22T16:00:00Z", + Status: "running", + ServerConnectivity: "online", + ReceivedAt: "2026-03-22T16:00:00Z", + HeartbeatEverySeconds: 30, + }) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/online-screen/status", nil) + req.SetPathValue("screenId", "online-screen") + w := httptest.NewRecorder() + + handleGetLatestPlayerStatus(store)(w, req) + + var response playerStatusRecord + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + + if got, want := response.DerivedState, "online"; got != want { + t.Fatalf("response.DerivedState = %q, want %q", got, want) + } +} + func TestHandleGetLatestPlayerStatusNotFound(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/missing/status", nil) req.SetPathValue("screenId", "missing")