package httpapi import ( "net/http" "strings" "time" ) const staleThreshold = 2 * time.Minute type playerStatusRequest struct { ScreenID string `json:"screen_id"` Timestamp string `json:"ts"` Status string `json:"status"` ServerConnectivity string `json:"server_connectivity"` ServerURL string `json:"server_url"` MQTTBroker string `json:"mqtt_broker"` HeartbeatEverySeconds int `json:"heartbeat_every_seconds"` StartedAt string `json:"started_at"` LastHeartbeatAt string `json:"last_heartbeat_at"` } func handlePlayerStatus(store playerStatusStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var request playerStatusRequest if err := decodeJSON(r, &request); err != nil { writeError(w, http.StatusBadRequest, "invalid_json", "ungueltiger JSON-Body", nil) return } if strings.TrimSpace(request.ScreenID) == "" { writeError(w, http.StatusBadRequest, "screen_id_required", "screen_id ist erforderlich", nil) return } request.ScreenID = strings.TrimSpace(request.ScreenID) if strings.TrimSpace(request.Timestamp) == "" { writeError(w, http.StatusBadRequest, "timestamp_required", "ts ist erforderlich", nil) return } if strings.TrimSpace(request.Status) == "" { writeError(w, http.StatusBadRequest, "status_required", "status ist erforderlich", nil) return } if err := validateOptionalRFC3339(request.Timestamp); err != nil { writeError(w, http.StatusBadRequest, "invalid_timestamp", "ts ist kein gueltiger RFC3339-Zeitstempel", nil) return } if err := validateOptionalRFC3339(request.StartedAt); err != nil { writeError(w, http.StatusBadRequest, "invalid_started_at", "started_at ist kein gueltiger RFC3339-Zeitstempel", nil) return } if err := validateOptionalRFC3339(request.LastHeartbeatAt); err != nil { writeError(w, http.StatusBadRequest, "invalid_last_heartbeat_at", "last_heartbeat_at ist kein gueltiger RFC3339-Zeitstempel", nil) return } store.Save(playerStatusRecord{ ScreenID: request.ScreenID, Timestamp: request.Timestamp, Status: request.Status, ServerConnectivity: request.ServerConnectivity, ServerURL: request.ServerURL, MQTTBroker: request.MQTTBroker, HeartbeatEverySeconds: request.HeartbeatEverySeconds, StartedAt: request.StartedAt, LastHeartbeatAt: request.LastHeartbeatAt, }) writeJSON(w, http.StatusOK, map[string]string{ "status": "accepted", }) } } func handleGetLatestPlayerStatus(store playerStatusStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { screenID := strings.TrimSpace(r.PathValue("screenId")) if screenID == "" { writeError(w, http.StatusBadRequest, "screen_id_required", "screenId ist erforderlich", nil) return } record, ok := store.Get(screenID) if !ok { writeError(w, http.StatusNotFound, "screen_status_not_found", "Fuer diesen Screen liegt noch kein Status vor", nil) return } record.Stale = isStale(record, store.Now()) writeJSON(w, http.StatusOK, record) } } 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 { if strings.TrimSpace(value) == "" { return nil } _, err := time.Parse(time.RFC3339, value) return err } func isStale(record playerStatusRecord, now time.Time) bool { if strings.TrimSpace(record.ReceivedAt) == "" { return false } receivedAt, err := time.Parse(time.RFC3339, record.ReceivedAt) if err != nil { return false } return now.Sub(receivedAt) > staleThreshold }