Ergaenze Screen-ID-Filter (q=) fuer Uebersicht und Status-API

GET /api/v1/screens/status und GET /status akzeptieren jetzt q=<substring>
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 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-22 20:28:01 +01:00
parent 57e0cdb43c
commit 8243eb10c9
3 changed files with 70 additions and 5 deletions

View file

@ -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
}

View file

@ -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()

View file

@ -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
</div>
<form class="filter-form" method="get" action="{{.StatusPagePath}}">
<div class="field full">
<label for="q">Screen ID contains</label>
<input id="q" name="q" type="text" placeholder="e.g. info01" value="{{.Filters.ScreenIDFilter}}">
</div>
<div class="field">
<label for="server_connectivity">Server connectivity</label>
<select id="server_connectivity" name="server_connectivity">
@ -786,6 +792,7 @@ func handleScreenDetailPage(store playerStatusStore) http.HandlerFunc {
func buildStatusPageData(store playerStatusStore, query url.Values, overview screenStatusOverview) statusPageData {
filters := statusPageFilters{
ScreenIDFilter: strings.TrimSpace(query.Get("q")),
ServerConnectivity: strings.TrimSpace(query.Get("server_connectivity")),
Stale: strings.TrimSpace(query.Get("stale")),
UpdatedSince: strings.TrimSpace(query.Get("updated_since")),
@ -804,34 +811,35 @@ func buildStatusPageData(store playerStatusStore, query url.Values, overview scr
}
func buildStatusQuickFilters(filters statusPageFilters) []statusFilterLink {
base := statusPageFilters{ScreenIDFilter: filters.ScreenIDFilter, Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}
return []statusFilterLink{
{
Label: "All screens",
Href: buildStatusPageHref(statusPageFilters{Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Href: buildStatusPageHref(base),
Class: "",
Active: filters.ServerConnectivity == "" && filters.Stale == "",
},
{
Label: "Connectivity offline",
Href: buildStatusPageHref(statusPageFilters{ServerConnectivity: "offline", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Href: buildStatusPageHref(statusPageFilters{ScreenIDFilter: base.ScreenIDFilter, ServerConnectivity: "offline", Limit: base.Limit, UpdatedSince: base.UpdatedSince}),
Class: "offline",
Active: filters.ServerConnectivity == "offline" && filters.Stale == "",
},
{
Label: "Connectivity degraded",
Href: buildStatusPageHref(statusPageFilters{ServerConnectivity: "degraded", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Href: buildStatusPageHref(statusPageFilters{ScreenIDFilter: base.ScreenIDFilter, ServerConnectivity: "degraded", Limit: base.Limit, UpdatedSince: base.UpdatedSince}),
Class: "degraded",
Active: filters.ServerConnectivity == "degraded" && filters.Stale == "",
},
{
Label: "Stale reports",
Href: buildStatusPageHref(statusPageFilters{Stale: "true", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Href: buildStatusPageHref(statusPageFilters{ScreenIDFilter: base.ScreenIDFilter, Stale: "true", Limit: base.Limit, UpdatedSince: base.UpdatedSince}),
Class: "",
Active: filters.ServerConnectivity == "" && filters.Stale == "true",
},
{
Label: "Fresh reports",
Href: buildStatusPageHref(statusPageFilters{Stale: "false", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Href: buildStatusPageHref(statusPageFilters{ScreenIDFilter: base.ScreenIDFilter, Stale: "false", Limit: base.Limit, UpdatedSince: base.UpdatedSince}),
Class: "online",
Active: filters.ServerConnectivity == "" && filters.Stale == "false",
},
@ -844,6 +852,9 @@ func buildStatusPageHref(filters statusPageFilters) string {
func buildOverviewPath(basePath string, filters statusPageFilters) string {
query := url.Values{}
if filters.ScreenIDFilter != "" {
query.Set("q", filters.ScreenIDFilter)
}
if filters.ServerConnectivity != "" {
query.Set("server_connectivity", filters.ServerConnectivity)
}