From 943553234d3f21fed6423859ca50e8e0369e6d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Sun, 22 Mar 2026 18:33:14 +0100 Subject: [PATCH] Lege Statusuebersicht fuer Screens an Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- DEVELOPMENT.md | 1 + docs/PLAYER-STATUS-HTTP.md | 5 +++- .../backend/internal/httpapi/playerstatus.go | 13 ++++++++ .../internal/httpapi/playerstatus_store.go | 18 +++++++++++ .../internal/httpapi/playerstatus_test.go | 30 +++++++++++++++++++ server/backend/internal/httpapi/router.go | 1 + .../backend/internal/httpapi/router_test.go | 12 ++++++++ 7 files changed, 79 insertions(+), 1 deletion(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9d60a21..531fa98 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -197,6 +197,7 @@ Ergaenzt seit dem ersten Geruest: - erster `POST /api/v1/player/status`-Endpunkt im Backend - letzter bekannter Player-Status wird im Backend pro Screen in-memory vorgehalten und lesbar gemacht - Backend ergaenzt den Read-Pfad um `received_at` und eine einfache `stale`-Ableitung +- Backend bietet zusaetzlich eine kleine Uebersicht aller zuletzt meldenden Screens - 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 b5969ac..a6e9db4 100644 --- a/docs/PLAYER-STATUS-HTTP.md +++ b/docs/PLAYER-STATUS-HTTP.md @@ -65,9 +65,12 @@ Bei ungueltigen Requests wird wie bei den anderen API-Endpunkten der gemeinsame Zusätzlich zur Write-Route gibt es in dieser Stufe: +- `GET /api/v1/screens/status` - `GET /api/v1/screens/{screenId}/status` -Dieser Endpunkt liefert den zuletzt akzeptierten Status fuer einen Screen zurueck. +`GET /api/v1/screens/status` liefert eine kleine Uebersicht aller bisher berichtenden Screens mit ihrem jeweils letzten bekannten Datensatz. + +`GET /api/v1/screens/{screenId}/status` liefert den zuletzt akzeptierten Status fuer einen einzelnen Screen zurueck. Wenn fuer den Screen noch kein Status vorliegt, liefert das Backend `404` mit dem gemeinsamen Fehlerumschlag. Der aktuell zurueckgelieferte Datensatz enthaelt damit sowohl den Lifecycle-Status (`status`) als auch den vom Agenten lokal abgeleiteten Reachability-Zustand (`server_connectivity`). diff --git a/server/backend/internal/httpapi/playerstatus.go b/server/backend/internal/httpapi/playerstatus.go index bb5be4f..6f6e892 100644 --- a/server/backend/internal/httpapi/playerstatus.go +++ b/server/backend/internal/httpapi/playerstatus.go @@ -96,6 +96,19 @@ func handleGetLatestPlayerStatus(store playerStatusStore) http.HandlerFunc { } } +func handleListLatestPlayerStatuses(store playerStatusStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + records := store.List() + for i := range records { + records[i].Stale = isStale(records[i], store.Now()) + } + + writeJSON(w, http.StatusOK, map[string]any{ + "screens": records, + }) + } +} + func validateOptionalRFC3339(value string) error { if strings.TrimSpace(value) == "" { return nil diff --git a/server/backend/internal/httpapi/playerstatus_store.go b/server/backend/internal/httpapi/playerstatus_store.go index 9648772..ec21569 100644 --- a/server/backend/internal/httpapi/playerstatus_store.go +++ b/server/backend/internal/httpapi/playerstatus_store.go @@ -1,6 +1,7 @@ package httpapi import ( + "sort" "sync" "time" ) @@ -22,6 +23,7 @@ type playerStatusRecord struct { type playerStatusStore interface { Save(record playerStatusRecord) Get(screenID string) (playerStatusRecord, bool) + List() []playerStatusRecord Now() time.Time } @@ -55,6 +57,22 @@ func (s *inMemoryPlayerStatusStore) Get(screenID string) (playerStatusRecord, bo return record, ok } +func (s *inMemoryPlayerStatusStore) List() []playerStatusRecord { + s.mu.RLock() + defer s.mu.RUnlock() + + records := make([]playerStatusRecord, 0, len(s.records)) + for _, record := range s.records { + records = append(records, record) + } + + sort.Slice(records, func(i, j int) bool { + return records[i].ScreenID < records[j].ScreenID + }) + + return records +} + func (s *inMemoryPlayerStatusStore) Now() time.Time { if s.now == nil { return time.Now() diff --git a/server/backend/internal/httpapi/playerstatus_test.go b/server/backend/internal/httpapi/playerstatus_test.go index 1f7b7f7..cc7a1da 100644 --- a/server/backend/internal/httpapi/playerstatus_test.go +++ b/server/backend/internal/httpapi/playerstatus_test.go @@ -266,3 +266,33 @@ func TestHandleGetLatestPlayerStatusNotFound(t *testing.T) { t.Fatalf("status = %d, want %d", got, want) } } + +func TestHandleListLatestPlayerStatuses(t *testing.T) { + store := newInMemoryPlayerStatusStore() + store.Save(playerStatusRecord{ScreenID: "screen-b", Timestamp: "2026-03-22T16:00:01Z", Status: "running", ServerConnectivity: "online"}) + store.Save(playerStatusRecord{ScreenID: "screen-a", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "degraded"}) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil) + w := httptest.NewRecorder() + + handleListLatestPlayerStatuses(store)(w, req) + + if got, want := w.Code, http.StatusOK; got != want { + t.Fatalf("status = %d, want %d", got, want) + } + + var response struct { + Screens []playerStatusRecord `json:"screens"` + } + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + + if got, want := len(response.Screens), 2; got != want { + t.Fatalf("len(response.Screens) = %d, want %d", got, want) + } + + if got, want := response.Screens[0].ScreenID, "screen-a"; got != want { + t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) + } +} diff --git a/server/backend/internal/httpapi/router.go b/server/backend/internal/httpapi/router.go index 85137b9..785b19b 100644 --- a/server/backend/internal/httpapi/router.go +++ b/server/backend/internal/httpapi/router.go @@ -27,6 +27,7 @@ func NewRouter(store playerStatusStore) http.Handler { mux.HandleFunc("GET /api/v1/meta", handleMeta) mux.HandleFunc("POST /api/v1/player/status", handlePlayerStatus(store)) + mux.HandleFunc("GET /api/v1/screens/status", handleListLatestPlayerStatuses(store)) mux.HandleFunc("GET /api/v1/screens/{screenId}/status", handleGetLatestPlayerStatus(store)) mux.HandleFunc("POST /api/v1/tools/message-wall/resolve", handleResolveMessageWall) diff --git a/server/backend/internal/httpapi/router_test.go b/server/backend/internal/httpapi/router_test.go index 7902f5f..b59a4d2 100644 --- a/server/backend/internal/httpapi/router_test.go +++ b/server/backend/internal/httpapi/router_test.go @@ -141,3 +141,15 @@ func TestRouterScreenStatusRoute(t *testing.T) { t.Fatalf("status = %d, want %d", got, want) } } + +func TestRouterScreenStatusListRoute(t *testing.T) { + store := newInMemoryPlayerStatusStore() + req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil) + w := httptest.NewRecorder() + + NewRouter(store).ServeHTTP(w, req) + + if got, want := w.Code, http.StatusOK; got != want { + t.Fatalf("status = %d, want %d", got, want) + } +}