package httpapi import ( "net/http" "strings" "time" ) var allowedStatuses = map[string]struct{}{ "starting": {}, "running": {}, "stopped": {}, } var allowedServerConnectivity = map[string]struct{}{ "": {}, "unknown": {}, "online": {}, "degraded": {}, "offline": {}, } 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 } request.Status = strings.TrimSpace(request.Status) if _, ok := allowedStatuses[request.Status]; !ok { writeError(w, http.StatusBadRequest, "invalid_status", "status ist ungueltig", nil) return } request.ServerConnectivity = strings.TrimSpace(request.ServerConnectivity) if _, ok := allowedServerConnectivity[request.ServerConnectivity]; !ok { writeError(w, http.StatusBadRequest, "invalid_server_connectivity", "server_connectivity ist ungueltig", nil) return } if request.HeartbeatEverySeconds <= 0 { writeError(w, http.StatusBadRequest, "invalid_heartbeat_interval", "heartbeat_every_seconds muss positiv sein", 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() wantConnectivity := strings.TrimSpace(r.URL.Query().Get("server_connectivity")) wantStale := strings.TrimSpace(r.URL.Query().Get("stale")) filtered := make([]playerStatusRecord, 0, len(records)) for i := range records { records[i].Stale = isStale(records[i], store.Now()) if wantConnectivity != "" && records[i].ServerConnectivity != wantConnectivity { continue } if wantStale != "" { if wantStale == "true" && !records[i].Stale { continue } if wantStale == "false" && records[i].Stale { continue } } filtered = append(filtered, records[i]) } writeJSON(w, http.StatusOK, map[string]any{ "screens": filtered, }) } } 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 } threshold := staleThresholdFor(record) return now.Sub(receivedAt) > threshold } func staleThresholdFor(record playerStatusRecord) time.Duration { if record.HeartbeatEverySeconds > 0 { return time.Duration(record.HeartbeatEverySeconds*2) * time.Second } return 2 * time.Minute }