Ergaenze derived_state als Query-Filter fuer Uebersicht und Status-API
GET /api/v1/screens/status und GET /status akzeptieren jetzt derived_state=online|degraded|offline zum direkten Filtern nach der serverseitig abgeleiteten Diagnoseeinschaetzung. Erlaubte Werte sind online, degraded und offline; unknown ist explizit nicht erlaubt, da derived_state immer auf einen der drei Werte abgebildet wird. Abgrenzung zu server_connectivity: derived_state filtert nach dem zusammengefassten Zustand (stale + connectivity + status), waehrend server_connectivity nur den gemeldeten Connectivity-Wert betrifft. Beide Filter koennen kombiniert werden. Tests: FiltersByDerivedState, RejectsInvalidDerivedState Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8243eb10c9
commit
ea90af1403
3 changed files with 72 additions and 0 deletions
|
|
@ -176,6 +176,14 @@ func buildScreenStatusOverview(store playerStatusStore, query url.Values) (scree
|
|||
return screenStatusOverview{}, errInvalidStale
|
||||
}
|
||||
|
||||
wantDerivedState := strings.TrimSpace(query.Get("derived_state"))
|
||||
switch wantDerivedState {
|
||||
case "", "online", "degraded", "offline":
|
||||
// valid
|
||||
default:
|
||||
return screenStatusOverview{}, errInvalidDerivedState
|
||||
}
|
||||
|
||||
updatedSince, err := parseOptionalRFC3339(query.Get("updated_since"))
|
||||
if err != nil {
|
||||
return screenStatusOverview{}, errInvalidUpdatedSince
|
||||
|
|
@ -221,6 +229,9 @@ func buildScreenStatusOverview(store playerStatusStore, query url.Values) (scree
|
|||
continue
|
||||
}
|
||||
}
|
||||
if wantDerivedState != "" && records[i].DerivedState != wantDerivedState {
|
||||
continue
|
||||
}
|
||||
overview.Screens = append(overview.Screens, records[i])
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +257,7 @@ var (
|
|||
errInvalidLimit = errors.New("invalid limit")
|
||||
errInvalidServerConnectivity = errors.New("invalid server_connectivity")
|
||||
errInvalidStale = errors.New("invalid stale")
|
||||
errInvalidDerivedState = errors.New("invalid derived_state")
|
||||
)
|
||||
|
||||
// overviewQueryErrorCode returns the machine-readable error code for query
|
||||
|
|
@ -260,6 +272,8 @@ func overviewQueryErrorCode(err error) string {
|
|||
return "invalid_server_connectivity"
|
||||
case errInvalidStale:
|
||||
return "invalid_stale"
|
||||
case errInvalidDerivedState:
|
||||
return "invalid_derived_state"
|
||||
default:
|
||||
return "invalid_query"
|
||||
}
|
||||
|
|
@ -278,6 +292,8 @@ func overviewQueryErrorMessage(err error) string {
|
|||
return "server_connectivity muss online, offline, degraded oder unknown sein"
|
||||
case errInvalidStale:
|
||||
return "stale muss true oder false sein"
|
||||
case errInvalidDerivedState:
|
||||
return "derived_state muss online, degraded oder offline sein"
|
||||
default:
|
||||
return "ungueltige Query-Parameter"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,6 +628,47 @@ func TestHandleListLatestPlayerStatusesScreenIDFilterIsCaseInsensitive(t *testin
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleListLatestPlayerStatusesFiltersByDerivedState(t *testing.T) {
|
||||
store := newInMemoryPlayerStatusStore()
|
||||
store.now = func() time.Time {
|
||||
return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC)
|
||||
}
|
||||
store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30})
|
||||
store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30})
|
||||
store.Save(playerStatusRecord{ScreenID: "screen-degraded", Timestamp: "2026-03-22T16:09:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:09:00Z", HeartbeatEverySeconds: 30})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?derived_state=online", 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, "screen-online"; got != want {
|
||||
t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleListLatestPlayerStatusesRejectsInvalidDerivedState(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?derived_state=unknown", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleListLatestPlayerStatusesRejectsInvalidServerConnectivity(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=garbage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type statusPageData struct {
|
|||
|
||||
type statusPageFilters struct {
|
||||
ScreenIDFilter string
|
||||
DerivedState string
|
||||
ServerConnectivity string
|
||||
Stale string
|
||||
UpdatedSince string
|
||||
|
|
@ -525,6 +526,16 @@ var statusPageTemplate = template.Must(template.New("status-page").Funcs(statusT
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="derived_state">Derived state</label>
|
||||
<select id="derived_state" name="derived_state">
|
||||
<option value="" {{if eq .Filters.DerivedState ""}}selected{{end}}>Any</option>
|
||||
<option value="online" {{if eq .Filters.DerivedState "online"}}selected{{end}}>Online</option>
|
||||
<option value="degraded" {{if eq .Filters.DerivedState "degraded"}}selected{{end}}>Degraded</option>
|
||||
<option value="offline" {{if eq .Filters.DerivedState "offline"}}selected{{end}}>Offline</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="updated_since">Updated since (RFC3339)</label>
|
||||
<input id="updated_since" name="updated_since" type="text" placeholder="2026-03-22T16:05:00Z" value="{{.Filters.UpdatedSince}}">
|
||||
|
|
@ -793,6 +804,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")),
|
||||
DerivedState: strings.TrimSpace(query.Get("derived_state")),
|
||||
ServerConnectivity: strings.TrimSpace(query.Get("server_connectivity")),
|
||||
Stale: strings.TrimSpace(query.Get("stale")),
|
||||
UpdatedSince: strings.TrimSpace(query.Get("updated_since")),
|
||||
|
|
@ -855,6 +867,9 @@ func buildOverviewPath(basePath string, filters statusPageFilters) string {
|
|||
if filters.ScreenIDFilter != "" {
|
||||
query.Set("q", filters.ScreenIDFilter)
|
||||
}
|
||||
if filters.DerivedState != "" {
|
||||
query.Set("derived_state", filters.DerivedState)
|
||||
}
|
||||
if filters.ServerConnectivity != "" {
|
||||
query.Set("server_connectivity", filters.ServerConnectivity)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue