Files
core/internal/api/failures.go

175 lines
4.7 KiB
Go

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})
}