Schaerfe Semantik des Statuspfads nach

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jesko Anschütz 2026-03-22 18:41:32 +01:00
parent 45e7b776ab
commit 1f4fa3d985
6 changed files with 58 additions and 4 deletions

View file

@ -92,7 +92,12 @@ Noch nicht Teil dieser Stufe:
- Admin-UI-Anzeige des letzten Status - Admin-UI-Anzeige des letzten Status
- Retry-Queue oder lokale Zwischenspeicherung im Agent - Retry-Queue oder lokale Zwischenspeicherung im Agent
Agent-seitig wird die Server-Erreichbarkeit aktuell lokal als `unknown`, `online` oder `degraded` aus dem Erfolg der HTTP-Reports abgeleitet. Agent-seitig wird die Server-Erreichbarkeit aktuell lokal als `unknown`, `online`, `degraded` oder `offline` aus dem Erfolg der HTTP-Reports abgeleitet.
Fuer den transportierten Wert im erfolgreichen HTTP-Report gilt aktuell bewusst einfach:
- wenn ein Report vom Backend akzeptiert wurde, wird dieser Report selbst als `server_connectivity = online` gespeichert
- anhaltende Ausfaelle werden primaer ueber lokale Agent-Zustaende und serverseitige `stale`-Ableitung sichtbar
## Folgeschritte ## Folgeschritte

View file

@ -173,10 +173,14 @@ func (a *App) reportStatus(ctx context.Context) {
} }
snapshot := a.Snapshot() snapshot := a.Snapshot()
payloadConnectivity := snapshot.ServerConnectivity
if payloadConnectivity == ConnectivityUnknown || payloadConnectivity == ConnectivityOnline || payloadConnectivity == ConnectivityDegraded || payloadConnectivity == ConnectivityOffline {
payloadConnectivity = ConnectivityOnline
}
err := a.reporter.Send(ctx, statusreporter.Snapshot{ err := a.reporter.Send(ctx, statusreporter.Snapshot{
Status: string(snapshot.Status), Status: string(snapshot.Status),
ServerConnectivity: string(snapshot.ServerConnectivity), ServerConnectivity: string(payloadConnectivity),
ScreenID: snapshot.ScreenID, ScreenID: snapshot.ScreenID,
ServerBaseURL: snapshot.ServerBaseURL, ServerBaseURL: snapshot.ServerBaseURL,
MQTTBroker: snapshot.MQTTBroker, MQTTBroker: snapshot.MQTTBroker,

View file

@ -16,10 +16,12 @@ type recordingReporter struct {
callCount int callCount int
err error err error
errs []error errs []error
snapshots []statusreporter.Snapshot
} }
func (r *recordingReporter) Send(_ context.Context, _ statusreporter.Snapshot) error { func (r *recordingReporter) Send(_ context.Context, snapshot statusreporter.Snapshot) error {
r.callCount++ r.callCount++
r.snapshots = append(r.snapshots, snapshot)
if len(r.errs) > 0 { if len(r.errs) > 0 {
err := r.errs[0] err := r.errs[0]
r.errs = r.errs[1:] r.errs = r.errs[1:]
@ -212,13 +214,14 @@ func TestAppRunReportsStatusWithoutStoppingOnReporterError(t *testing.T) {
} }
func TestAppRunMarksServerConnectivityOnlineAfterSuccessfulReport(t *testing.T) { func TestAppRunMarksServerConnectivityOnlineAfterSuccessfulReport(t *testing.T) {
reporter := &recordingReporter{}
application := newApp(config.Config{ application := newApp(config.Config{
ScreenID: "screen-online", ScreenID: "screen-online",
ServerBaseURL: "http://127.0.0.1:8080", ServerBaseURL: "http://127.0.0.1:8080",
MQTTBroker: "tcp://127.0.0.1:1883", MQTTBroker: "tcp://127.0.0.1:1883",
HeartbeatEvery: 1, HeartbeatEvery: 1,
StatusReportEvery: 1, StatusReportEvery: 1,
}, log.New(&bytes.Buffer{}, "", 0), time.Now, &recordingReporter{}) }, log.New(&bytes.Buffer{}, "", 0), time.Now, reporter)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1) errCh := make(chan error, 1)
@ -239,6 +242,16 @@ func TestAppRunMarksServerConnectivityOnlineAfterSuccessfulReport(t *testing.T)
t.Fatalf("ServerConnectivity = %q, want %q", got, want) t.Fatalf("ServerConnectivity = %q, want %q", got, want)
} }
if reporter.callCount == 0 {
cancel()
t.Fatal("reporter was not called")
}
if got, want := reporter.snapshots[0].ServerConnectivity, string(ConnectivityOnline); got != want {
cancel()
t.Fatalf("first reported connectivity = %q, want %q", got, want)
}
cancel() cancel()
<-errCh <-errCh
} }
@ -287,4 +300,8 @@ func TestReportStatusRecoversFromOfflineToOnline(t *testing.T) {
if got, want := application.Snapshot().ServerConnectivity, ConnectivityOnline; got != want { if got, want := application.Snapshot().ServerConnectivity, ConnectivityOnline; got != want {
t.Fatalf("recovered state = %q, want %q", got, want) t.Fatalf("recovered state = %q, want %q", got, want)
} }
if got, want := reporter.snapshots[len(reporter.snapshots)-1].ServerConnectivity, string(ConnectivityOnline); got != want {
t.Fatalf("recovery payload connectivity = %q, want %q", got, want)
}
} }

View file

@ -25,6 +25,11 @@ func handleMeta(w http.ResponseWriter, _ *http.Request) {
"method": http.MethodGet, "method": http.MethodGet,
"path": "/api/v1/screens/{screenId}/status", "path": "/api/v1/screens/{screenId}/status",
}, },
{
"name": "player-status-ingest",
"method": http.MethodPost,
"path": "/api/v1/player/status",
},
}, },
}, },
}) })

View file

@ -32,6 +32,7 @@ func handlePlayerStatus(store playerStatusStore) http.HandlerFunc {
writeError(w, http.StatusBadRequest, "screen_id_required", "screen_id ist erforderlich", nil) writeError(w, http.StatusBadRequest, "screen_id_required", "screen_id ist erforderlich", nil)
return return
} }
request.ScreenID = strings.TrimSpace(request.ScreenID)
if strings.TrimSpace(request.Timestamp) == "" { if strings.TrimSpace(request.Timestamp) == "" {
writeError(w, http.StatusBadRequest, "timestamp_required", "ts ist erforderlich", nil) writeError(w, http.StatusBadRequest, "timestamp_required", "ts ist erforderlich", nil)

View file

@ -90,6 +90,28 @@ func TestHandlePlayerStatusRejectsMissingScreenID(t *testing.T) {
} }
} }
func TestHandlePlayerStatusStoresNormalizedScreenID(t *testing.T) {
store := newInMemoryPlayerStatusStore()
body := []byte(`{
"screen_id": " info01-dev ",
"ts": "2026-03-22T16:00:00Z",
"status": "running"
}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
w := httptest.NewRecorder()
handlePlayerStatus(store)(w, req)
if got, want := w.Code, http.StatusOK; got != want {
t.Fatalf("status = %d, want %d", got, want)
}
if _, ok := store.Get("info01-dev"); !ok {
t.Fatal("store.Get(normalized) ok = false, want true")
}
}
func TestHandlePlayerStatusRejectsMissingTimestamp(t *testing.T) { func TestHandlePlayerStatusRejectsMissingTimestamp(t *testing.T) {
body := []byte(`{ body := []byte(`{
"screen_id": "info01-dev", "screen_id": "info01-dev",