Lege Statusuebersicht fuer Screens an
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
cc06b5a728
commit
943553234d
7 changed files with 79 additions and 1 deletions
|
|
@ -197,6 +197,7 @@ Ergaenzt seit dem ersten Geruest:
|
||||||
- erster `POST /api/v1/player/status`-Endpunkt im Backend
|
- erster `POST /api/v1/player/status`-Endpunkt im Backend
|
||||||
- letzter bekannter Player-Status wird im Backend pro Screen in-memory vorgehalten und lesbar gemacht
|
- 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 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
|
- dateibasierte Agent-Konfiguration zusaetzlich zu Env-Overrides
|
||||||
- strukturierte Agent-Logs mit internem Health-Snapshot und signalgesteuertem Shutdown
|
- strukturierte Agent-Logs mit internem Health-Snapshot und signalgesteuertem Shutdown
|
||||||
- erster periodischer HTTP-Status-Reporter im Agent
|
- erster periodischer HTTP-Status-Reporter im Agent
|
||||||
|
|
|
||||||
|
|
@ -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:
|
Zusätzlich zur Write-Route gibt es in dieser Stufe:
|
||||||
|
|
||||||
|
- `GET /api/v1/screens/status`
|
||||||
- `GET /api/v1/screens/{screenId}/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.
|
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`).
|
Der aktuell zurueckgelieferte Datensatz enthaelt damit sowohl den Lifecycle-Status (`status`) als auch den vom Agenten lokal abgeleiteten Reachability-Zustand (`server_connectivity`).
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func validateOptionalRFC3339(value string) error {
|
||||||
if strings.TrimSpace(value) == "" {
|
if strings.TrimSpace(value) == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package httpapi
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -22,6 +23,7 @@ type playerStatusRecord struct {
|
||||||
type playerStatusStore interface {
|
type playerStatusStore interface {
|
||||||
Save(record playerStatusRecord)
|
Save(record playerStatusRecord)
|
||||||
Get(screenID string) (playerStatusRecord, bool)
|
Get(screenID string) (playerStatusRecord, bool)
|
||||||
|
List() []playerStatusRecord
|
||||||
Now() time.Time
|
Now() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,6 +57,22 @@ func (s *inMemoryPlayerStatusStore) Get(screenID string) (playerStatusRecord, bo
|
||||||
return record, ok
|
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 {
|
func (s *inMemoryPlayerStatusStore) Now() time.Time {
|
||||||
if s.now == nil {
|
if s.now == nil {
|
||||||
return time.Now()
|
return time.Now()
|
||||||
|
|
|
||||||
|
|
@ -266,3 +266,33 @@ func TestHandleGetLatestPlayerStatusNotFound(t *testing.T) {
|
||||||
t.Fatalf("status = %d, want %d", got, want)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ func NewRouter(store playerStatusStore) http.Handler {
|
||||||
mux.HandleFunc("GET /api/v1/meta", handleMeta)
|
mux.HandleFunc("GET /api/v1/meta", handleMeta)
|
||||||
|
|
||||||
mux.HandleFunc("POST /api/v1/player/status", handlePlayerStatus(store))
|
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("GET /api/v1/screens/{screenId}/status", handleGetLatestPlayerStatus(store))
|
||||||
|
|
||||||
mux.HandleFunc("POST /api/v1/tools/message-wall/resolve", handleResolveMessageWall)
|
mux.HandleFunc("POST /api/v1/tools/message-wall/resolve", handleResolveMessageWall)
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,15 @@ func TestRouterScreenStatusRoute(t *testing.T) {
|
||||||
t.Fatalf("status = %d, want %d", got, want)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue