package api import ( "net/http" "strings" "time" "reanimator/internal/domain" "reanimator/internal/repository/failures" "reanimator/internal/repository/registry" ) type FailureDependencies struct { Failures *failures.FailureRepository Components *registry.ComponentRepository Assets *registry.AssetRepository } type failureHandlers struct { deps FailureDependencies } func RegisterFailureRoutes(mux *http.ServeMux, deps FailureDependencies) { h := failureHandlers{deps: deps} mux.HandleFunc("/failures", h.handleFailures) mux.HandleFunc("/ingest/failures", h.handleFailureIngest) } type failureIngestRequest struct { Source string `json:"source"` Failures []failureIngestItem `json:"failures"` } type failureIngestItem struct { ExternalID string `json:"external_id"` ComponentID string `json:"component_id"` AssetID *string `json:"asset_id"` FailureType string `json:"failure_type"` FailureTime string `json:"failure_time"` Details *string `json:"details"` Confidence *float64 `json:"confidence"` } func (h failureHandlers) handleFailureIngest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if h.deps.Failures == nil { writeError(w, http.StatusInternalServerError, "failures unavailable") return } var req failureIngestRequest if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } if strings.TrimSpace(req.Source) == "" { writeError(w, http.StatusBadRequest, "source is required") return } if len(req.Failures) == 0 { writeError(w, http.StatusBadRequest, "failures is required") return } tx, err := h.deps.Failures.BeginTx(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "failure ingest failed") return } defer func() { _ = tx.Rollback() }() for _, item := range req.Failures { if strings.TrimSpace(item.ExternalID) == "" { writeError(w, http.StatusBadRequest, "failures.external_id is required") return } if item.ComponentID == "" { writeError(w, http.StatusBadRequest, "failures.component_id is required") return } if strings.TrimSpace(item.FailureType) == "" { writeError(w, http.StatusBadRequest, "failures.failure_type is required") return } if strings.TrimSpace(item.FailureTime) == "" { writeError(w, http.StatusBadRequest, "failures.failure_time is required") return } if h.deps.Components != nil { if _, err := h.deps.Components.Get(r.Context(), item.ComponentID); err != nil { if err == registry.ErrNotFound { writeError(w, http.StatusBadRequest, "component_id not found") return } writeError(w, http.StatusInternalServerError, "component lookup failed") return } } if item.AssetID != nil && h.deps.Assets != nil { if _, err := h.deps.Assets.Get(r.Context(), *item.AssetID); err != nil { if err == registry.ErrNotFound { writeError(w, http.StatusBadRequest, "asset_id not found") return } writeError(w, http.StatusInternalServerError, "asset lookup failed") return } } failureTime, err := time.Parse(time.RFC3339, item.FailureTime) if err != nil { writeError(w, http.StatusBadRequest, "failures.failure_time must be RFC3339") return } _, err = h.deps.Failures.Upsert(r.Context(), tx, domain.FailureEvent{ Source: strings.TrimSpace(req.Source), ExternalID: strings.TrimSpace(item.ExternalID), PartID: item.ComponentID, MachineID: item.AssetID, FailureType: strings.TrimSpace(item.FailureType), FailureTime: failureTime, Details: item.Details, Confidence: item.Confidence, }) if err != nil { writeError(w, http.StatusInternalServerError, "failure ingest failed") return } } if err := tx.Commit(); err != nil { writeError(w, http.StatusInternalServerError, "failure ingest failed") return } writeJSON(w, http.StatusOK, map[string]any{ "synced": len(req.Failures), }) } func (h failureHandlers) handleFailures(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } if h.deps.Failures == nil { writeError(w, http.StatusInternalServerError, "failures unavailable") return } limit := 200 if raw := r.URL.Query().Get("limit"); raw != "" { parsed, err := parseIntParam(raw) if err != nil || parsed <= 0 { writeError(w, http.StatusBadRequest, "limit must be a positive integer") return } limit = parsed } items, err := h.deps.Failures.ListAll(r.Context(), limit) if err != nil { writeError(w, http.StatusInternalServerError, "list failures failed") return } writeJSON(w, http.StatusOK, map[string]any{"items": items}) }