From 8243eb10c9c699c65e0dc9a8f9e560b9d0e92114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Sun, 22 Mar 2026 20:28:01 +0100 Subject: [PATCH] Ergaenze Screen-ID-Filter (q=) fuer Uebersicht und Status-API GET /api/v1/screens/status und GET /status akzeptieren jetzt q= zum Filtern der Ergebnisliste nach ScreenID. Der Vergleich ist case- insensitiv. Leerer Wert bedeutet kein Filter; jeder andere String ist gueltig (keine Validierung noetig). Die Summary-Counts bleiben unveraendert und beschreiben weiterhin den gesamten Store-Bestand. Die Quick-Filter auf /status behalten den aktuellen q-Wert beim Klick, damit der Textfilter nicht verloren geht wenn man z.B. von "All screens" auf "Stale reports" wechselt. Tests: FiltersByScreenIDSubstring, ScreenIDFilterIsCaseInsensitive Co-Authored-By: Claude Sonnet 4.6 --- .../backend/internal/httpapi/playerstatus.go | 5 ++ .../internal/httpapi/playerstatus_test.go | 49 +++++++++++++++++++ server/backend/internal/httpapi/statuspage.go | 21 ++++++-- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/server/backend/internal/httpapi/playerstatus.go b/server/backend/internal/httpapi/playerstatus.go index e865a47..4eb8471 100644 --- a/server/backend/internal/httpapi/playerstatus.go +++ b/server/backend/internal/httpapi/playerstatus.go @@ -158,6 +158,8 @@ func handleListLatestPlayerStatuses(store playerStatusStore) http.HandlerFunc { func buildScreenStatusOverview(store playerStatusStore, query url.Values) (screenStatusOverview, error) { records := store.List() + screenIDFilter := strings.ToLower(strings.TrimSpace(query.Get("q"))) + wantConnectivity := strings.TrimSpace(query.Get("server_connectivity")) switch wantConnectivity { case "", "online", "degraded", "offline", "unknown": @@ -202,6 +204,9 @@ func buildScreenStatusOverview(store playerStatusStore, query url.Values) (scree if records[i].Stale { overview.Summary.Stale++ } + if screenIDFilter != "" && !strings.Contains(strings.ToLower(records[i].ScreenID), screenIDFilter) { + continue + } if updatedSince != nil && !isUpdatedSince(records[i], *updatedSince) { continue } diff --git a/server/backend/internal/httpapi/playerstatus_test.go b/server/backend/internal/httpapi/playerstatus_test.go index 8002ca6..eb90963 100644 --- a/server/backend/internal/httpapi/playerstatus_test.go +++ b/server/backend/internal/httpapi/playerstatus_test.go @@ -579,6 +579,55 @@ func TestHandleListLatestPlayerStatusesAppliesLimit(t *testing.T) { } } +func TestHandleListLatestPlayerStatusesFiltersByScreenIDSubstring(t *testing.T) { + store := newInMemoryPlayerStatusStore() + store.Save(playerStatusRecord{ScreenID: "info01-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) + store.Save(playerStatusRecord{ScreenID: "info02-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) + store.Save(playerStatusRecord{ScreenID: "lobby-main", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?q=info", nil) + w := httptest.NewRecorder() + + handleListLatestPlayerStatuses(store)(w, req) + + 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) + } +} + +func TestHandleListLatestPlayerStatusesScreenIDFilterIsCaseInsensitive(t *testing.T) { + store := newInMemoryPlayerStatusStore() + store.Save(playerStatusRecord{ScreenID: "INFO01-DEV", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) + store.Save(playerStatusRecord{ScreenID: "lobby-main", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?q=info01", nil) + w := httptest.NewRecorder() + + handleListLatestPlayerStatuses(store)(w, req) + + 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), 1; got != want { + t.Fatalf("len(response.Screens) = %d, want %d", got, want) + } + + if got, want := response.Screens[0].ScreenID, "INFO01-DEV"; got != want { + t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) + } +} + func TestHandleListLatestPlayerStatusesRejectsInvalidServerConnectivity(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=garbage", nil) w := httptest.NewRecorder() diff --git a/server/backend/internal/httpapi/statuspage.go b/server/backend/internal/httpapi/statuspage.go index 00d1757..a88bcd9 100644 --- a/server/backend/internal/httpapi/statuspage.go +++ b/server/backend/internal/httpapi/statuspage.go @@ -19,6 +19,7 @@ type statusPageData struct { } type statusPageFilters struct { + ScreenIDFilter string ServerConnectivity string Stale string UpdatedSince string @@ -499,6 +500,11 @@ var statusPageTemplate = template.Must(template.New("status-page").Funcs(statusT
+
+ + +
+