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:
parent
57e0cdb43c
commit
8243eb10c9
3 changed files with 70 additions and 5 deletions
|
|
@ -158,6 +158,8 @@ func handleListLatestPlayerStatuses(store playerStatusStore) http.HandlerFunc {
|
||||||
func buildScreenStatusOverview(store playerStatusStore, query url.Values) (screenStatusOverview, error) {
|
func buildScreenStatusOverview(store playerStatusStore, query url.Values) (screenStatusOverview, error) {
|
||||||
records := store.List()
|
records := store.List()
|
||||||
|
|
||||||
|
screenIDFilter := strings.ToLower(strings.TrimSpace(query.Get("q")))
|
||||||
|
|
||||||
wantConnectivity := strings.TrimSpace(query.Get("server_connectivity"))
|
wantConnectivity := strings.TrimSpace(query.Get("server_connectivity"))
|
||||||
switch wantConnectivity {
|
switch wantConnectivity {
|
||||||
case "", "online", "degraded", "offline", "unknown":
|
case "", "online", "degraded", "offline", "unknown":
|
||||||
|
|
@ -202,6 +204,9 @@ func buildScreenStatusOverview(store playerStatusStore, query url.Values) (scree
|
||||||
if records[i].Stale {
|
if records[i].Stale {
|
||||||
overview.Summary.Stale++
|
overview.Summary.Stale++
|
||||||
}
|
}
|
||||||
|
if screenIDFilter != "" && !strings.Contains(strings.ToLower(records[i].ScreenID), screenIDFilter) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if updatedSince != nil && !isUpdatedSince(records[i], *updatedSince) {
|
if updatedSince != nil && !isUpdatedSince(records[i], *updatedSince) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestHandleListLatestPlayerStatusesRejectsInvalidServerConnectivity(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=garbage", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=garbage", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ type statusPageData struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type statusPageFilters struct {
|
type statusPageFilters struct {
|
||||||
|
ScreenIDFilter string
|
||||||
ServerConnectivity string
|
ServerConnectivity string
|
||||||
Stale string
|
Stale string
|
||||||
UpdatedSince string
|
UpdatedSince string
|
||||||
|
|
@ -499,6 +500,11 @@ var statusPageTemplate = template.Must(template.New("status-page").Funcs(statusT
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="filter-form" method="get" action="{{.StatusPagePath}}">
|
<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">
|
<div class="field">
|
||||||
<label for="server_connectivity">Server connectivity</label>
|
<label for="server_connectivity">Server connectivity</label>
|
||||||
<select id="server_connectivity" name="server_connectivity">
|
<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 {
|
func buildStatusPageData(store playerStatusStore, query url.Values, overview screenStatusOverview) statusPageData {
|
||||||
filters := statusPageFilters{
|
filters := statusPageFilters{
|
||||||
|
ScreenIDFilter: strings.TrimSpace(query.Get("q")),
|
||||||
ServerConnectivity: strings.TrimSpace(query.Get("server_connectivity")),
|
ServerConnectivity: strings.TrimSpace(query.Get("server_connectivity")),
|
||||||
Stale: strings.TrimSpace(query.Get("stale")),
|
Stale: strings.TrimSpace(query.Get("stale")),
|
||||||
UpdatedSince: strings.TrimSpace(query.Get("updated_since")),
|
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 {
|
func buildStatusQuickFilters(filters statusPageFilters) []statusFilterLink {
|
||||||
|
base := statusPageFilters{ScreenIDFilter: filters.ScreenIDFilter, Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}
|
||||||
return []statusFilterLink{
|
return []statusFilterLink{
|
||||||
{
|
{
|
||||||
Label: "All screens",
|
Label: "All screens",
|
||||||
Href: buildStatusPageHref(statusPageFilters{Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
|
Href: buildStatusPageHref(base),
|
||||||
Class: "",
|
Class: "",
|
||||||
Active: filters.ServerConnectivity == "" && filters.Stale == "",
|
Active: filters.ServerConnectivity == "" && filters.Stale == "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Label: "Connectivity offline",
|
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",
|
Class: "offline",
|
||||||
Active: filters.ServerConnectivity == "offline" && filters.Stale == "",
|
Active: filters.ServerConnectivity == "offline" && filters.Stale == "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Label: "Connectivity degraded",
|
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",
|
Class: "degraded",
|
||||||
Active: filters.ServerConnectivity == "degraded" && filters.Stale == "",
|
Active: filters.ServerConnectivity == "degraded" && filters.Stale == "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Label: "Stale reports",
|
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: "",
|
Class: "",
|
||||||
Active: filters.ServerConnectivity == "" && filters.Stale == "true",
|
Active: filters.ServerConnectivity == "" && filters.Stale == "true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Label: "Fresh reports",
|
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",
|
Class: "online",
|
||||||
Active: filters.ServerConnectivity == "" && filters.Stale == "false",
|
Active: filters.ServerConnectivity == "" && filters.Stale == "false",
|
||||||
},
|
},
|
||||||
|
|
@ -844,6 +852,9 @@ func buildStatusPageHref(filters statusPageFilters) string {
|
||||||
|
|
||||||
func buildOverviewPath(basePath string, filters statusPageFilters) string {
|
func buildOverviewPath(basePath string, filters statusPageFilters) string {
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
|
if filters.ScreenIDFilter != "" {
|
||||||
|
query.Set("q", filters.ScreenIDFilter)
|
||||||
|
}
|
||||||
if filters.ServerConnectivity != "" {
|
if filters.ServerConnectivity != "" {
|
||||||
query.Set("server_connectivity", filters.ServerConnectivity)
|
query.Set("server_connectivity", filters.ServerConnectivity)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue