175 lines
4.7 KiB
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})
|
|
}
|