From 1f4fa3d985bae2cce4c473672f89319b08846a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Sun, 22 Mar 2026 18:41:32 +0100 Subject: [PATCH] Schaerfe Semantik des Statuspfads nach Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- docs/PLAYER-STATUS-HTTP.md | 7 +++++- player/agent/internal/app/app.go | 6 ++++- player/agent/internal/app/app_test.go | 21 ++++++++++++++++-- server/backend/internal/httpapi/meta.go | 5 +++++ .../backend/internal/httpapi/playerstatus.go | 1 + .../internal/httpapi/playerstatus_test.go | 22 +++++++++++++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/PLAYER-STATUS-HTTP.md b/docs/PLAYER-STATUS-HTTP.md index a6e9db4..317bd34 100644 --- a/docs/PLAYER-STATUS-HTTP.md +++ b/docs/PLAYER-STATUS-HTTP.md @@ -92,7 +92,12 @@ Noch nicht Teil dieser Stufe: - Admin-UI-Anzeige des letzten Status - Retry-Queue oder lokale Zwischenspeicherung im Agent -Agent-seitig wird die Server-Erreichbarkeit aktuell lokal als `unknown`, `online` oder `degraded` aus dem Erfolg der HTTP-Reports abgeleitet. +Agent-seitig wird die Server-Erreichbarkeit aktuell lokal als `unknown`, `online`, `degraded` oder `offline` aus dem Erfolg der HTTP-Reports abgeleitet. + +Fuer den transportierten Wert im erfolgreichen HTTP-Report gilt aktuell bewusst einfach: + +- wenn ein Report vom Backend akzeptiert wurde, wird dieser Report selbst als `server_connectivity = online` gespeichert +- anhaltende Ausfaelle werden primaer ueber lokale Agent-Zustaende und serverseitige `stale`-Ableitung sichtbar ## Folgeschritte diff --git a/player/agent/internal/app/app.go b/player/agent/internal/app/app.go index 4c080ff..38659d4 100644 --- a/player/agent/internal/app/app.go +++ b/player/agent/internal/app/app.go @@ -173,10 +173,14 @@ func (a *App) reportStatus(ctx context.Context) { } snapshot := a.Snapshot() + payloadConnectivity := snapshot.ServerConnectivity + if payloadConnectivity == ConnectivityUnknown || payloadConnectivity == ConnectivityOnline || payloadConnectivity == ConnectivityDegraded || payloadConnectivity == ConnectivityOffline { + payloadConnectivity = ConnectivityOnline + } err := a.reporter.Send(ctx, statusreporter.Snapshot{ Status: string(snapshot.Status), - ServerConnectivity: string(snapshot.ServerConnectivity), + ServerConnectivity: string(payloadConnectivity), ScreenID: snapshot.ScreenID, ServerBaseURL: snapshot.ServerBaseURL, MQTTBroker: snapshot.MQTTBroker, diff --git a/player/agent/internal/app/app_test.go b/player/agent/internal/app/app_test.go index 233cc93..1dd3ae0 100644 --- a/player/agent/internal/app/app_test.go +++ b/player/agent/internal/app/app_test.go @@ -16,10 +16,12 @@ type recordingReporter struct { callCount int err error errs []error + snapshots []statusreporter.Snapshot } -func (r *recordingReporter) Send(_ context.Context, _ statusreporter.Snapshot) error { +func (r *recordingReporter) Send(_ context.Context, snapshot statusreporter.Snapshot) error { r.callCount++ + r.snapshots = append(r.snapshots, snapshot) if len(r.errs) > 0 { err := r.errs[0] r.errs = r.errs[1:] @@ -212,13 +214,14 @@ func TestAppRunReportsStatusWithoutStoppingOnReporterError(t *testing.T) { } func TestAppRunMarksServerConnectivityOnlineAfterSuccessfulReport(t *testing.T) { + reporter := &recordingReporter{} application := newApp(config.Config{ ScreenID: "screen-online", ServerBaseURL: "http://127.0.0.1:8080", MQTTBroker: "tcp://127.0.0.1:1883", HeartbeatEvery: 1, StatusReportEvery: 1, - }, log.New(&bytes.Buffer{}, "", 0), time.Now, &recordingReporter{}) + }, log.New(&bytes.Buffer{}, "", 0), time.Now, reporter) ctx, cancel := context.WithCancel(context.Background()) errCh := make(chan error, 1) @@ -239,6 +242,16 @@ func TestAppRunMarksServerConnectivityOnlineAfterSuccessfulReport(t *testing.T) t.Fatalf("ServerConnectivity = %q, want %q", got, want) } + if reporter.callCount == 0 { + cancel() + t.Fatal("reporter was not called") + } + + if got, want := reporter.snapshots[0].ServerConnectivity, string(ConnectivityOnline); got != want { + cancel() + t.Fatalf("first reported connectivity = %q, want %q", got, want) + } + cancel() <-errCh } @@ -287,4 +300,8 @@ func TestReportStatusRecoversFromOfflineToOnline(t *testing.T) { if got, want := application.Snapshot().ServerConnectivity, ConnectivityOnline; got != want { t.Fatalf("recovered state = %q, want %q", got, want) } + + if got, want := reporter.snapshots[len(reporter.snapshots)-1].ServerConnectivity, string(ConnectivityOnline); got != want { + t.Fatalf("recovery payload connectivity = %q, want %q", got, want) + } } diff --git a/server/backend/internal/httpapi/meta.go b/server/backend/internal/httpapi/meta.go index ba2cd59..4192c08 100644 --- a/server/backend/internal/httpapi/meta.go +++ b/server/backend/internal/httpapi/meta.go @@ -25,6 +25,11 @@ func handleMeta(w http.ResponseWriter, _ *http.Request) { "method": http.MethodGet, "path": "/api/v1/screens/{screenId}/status", }, + { + "name": "player-status-ingest", + "method": http.MethodPost, + "path": "/api/v1/player/status", + }, }, }, }) diff --git a/server/backend/internal/httpapi/playerstatus.go b/server/backend/internal/httpapi/playerstatus.go index 6f6e892..bd78116 100644 --- a/server/backend/internal/httpapi/playerstatus.go +++ b/server/backend/internal/httpapi/playerstatus.go @@ -32,6 +32,7 @@ func handlePlayerStatus(store playerStatusStore) http.HandlerFunc { writeError(w, http.StatusBadRequest, "screen_id_required", "screen_id ist erforderlich", nil) return } + request.ScreenID = strings.TrimSpace(request.ScreenID) if strings.TrimSpace(request.Timestamp) == "" { writeError(w, http.StatusBadRequest, "timestamp_required", "ts ist erforderlich", nil) diff --git a/server/backend/internal/httpapi/playerstatus_test.go b/server/backend/internal/httpapi/playerstatus_test.go index cc7a1da..3215521 100644 --- a/server/backend/internal/httpapi/playerstatus_test.go +++ b/server/backend/internal/httpapi/playerstatus_test.go @@ -90,6 +90,28 @@ func TestHandlePlayerStatusRejectsMissingScreenID(t *testing.T) { } } +func TestHandlePlayerStatusStoresNormalizedScreenID(t *testing.T) { + store := newInMemoryPlayerStatusStore() + body := []byte(`{ + "screen_id": " info01-dev ", + "ts": "2026-03-22T16:00:00Z", + "status": "running" + }`) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) + w := httptest.NewRecorder() + + handlePlayerStatus(store)(w, req) + + if got, want := w.Code, http.StatusOK; got != want { + t.Fatalf("status = %d, want %d", got, want) + } + + if _, ok := store.Get("info01-dev"); !ok { + t.Fatal("store.Get(normalized) ok = false, want true") + } +} + func TestHandlePlayerStatusRejectsMissingTimestamp(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev",