Files
core/internal/api/analytics.go

156 lines
4.1 KiB
Go

package api
import (
"math"
"net/http"
"time"
"reanimator/internal/repository/analytics"
)
type AnalyticsDependencies struct {
Analytics *analytics.Repository
}
type analyticsHandlers struct {
deps AnalyticsDependencies
}
func RegisterAnalyticsRoutes(mux *http.ServeMux, deps AnalyticsDependencies) {
h := analyticsHandlers{deps: deps}
mux.HandleFunc("/analytics/lot-metrics", h.handleLotMetrics)
mux.HandleFunc("/analytics/firmware-risk", h.handleFirmwareRisk)
mux.HandleFunc("/analytics/spare-forecast", h.handleSpareForecast)
}
func (h analyticsHandlers) handleLotMetrics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if h.deps.Analytics == nil {
writeError(w, http.StatusInternalServerError, "analytics unavailable")
return
}
start, end, ok := parseWindow(w, r)
if !ok {
return
}
items, err := h.deps.Analytics.ListLotMetrics(r.Context(), start, end)
if err != nil {
writeError(w, http.StatusInternalServerError, "lot metrics failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"start": start.Format(time.RFC3339),
"end": end.Format(time.RFC3339),
"items": items,
})
}
func (h analyticsHandlers) handleFirmwareRisk(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if h.deps.Analytics == nil {
writeError(w, http.StatusInternalServerError, "analytics unavailable")
return
}
start, end, ok := parseWindow(w, r)
if !ok {
return
}
items, err := h.deps.Analytics.ListFirmwareRisk(r.Context(), start, end)
if err != nil {
writeError(w, http.StatusInternalServerError, "firmware risk failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"start": start.Format(time.RFC3339),
"end": end.Format(time.RFC3339),
"items": items,
})
}
func (h analyticsHandlers) handleSpareForecast(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if h.deps.Analytics == nil {
writeError(w, http.StatusInternalServerError, "analytics unavailable")
return
}
start, end, ok := parseWindow(w, r)
if !ok {
return
}
horizonDays := 30
if value := r.URL.Query().Get("horizon_days"); value != "" {
parsed, err := parseIntParam(value)
if err != nil || parsed <= 0 {
writeError(w, http.StatusBadRequest, "horizon_days must be a positive integer")
return
}
horizonDays = parsed
}
multiplier := 1.0
if value := r.URL.Query().Get("multiplier"); value != "" {
parsed, err := parseFloatParam(value)
if err != nil || parsed < 0 {
writeError(w, http.StatusBadRequest, "multiplier must be >= 0")
return
}
multiplier = parsed
}
if math.IsNaN(multiplier) || math.IsInf(multiplier, 0) {
writeError(w, http.StatusBadRequest, "multiplier must be finite")
return
}
items, err := h.deps.Analytics.ForecastSpare(r.Context(), start, end, horizonDays, multiplier)
if err != nil {
writeError(w, http.StatusInternalServerError, "spare forecast failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"start": start.Format(time.RFC3339),
"end": end.Format(time.RFC3339),
"horizon_days": horizonDays,
"multiplier": multiplier,
"items": items,
})
}
func parseWindow(w http.ResponseWriter, r *http.Request) (time.Time, time.Time, bool) {
startValue := r.URL.Query().Get("start")
endValue := r.URL.Query().Get("end")
if startValue == "" || endValue == "" {
writeError(w, http.StatusBadRequest, "start and end are required")
return time.Time{}, time.Time{}, false
}
start, err := parseTimeParam(startValue)
if err != nil {
writeError(w, http.StatusBadRequest, "start must be RFC3339")
return time.Time{}, time.Time{}, false
}
end, err := parseTimeParam(endValue)
if err != nil {
writeError(w, http.StatusBadRequest, "end must be RFC3339")
return time.Time{}, time.Time{}, false
}
if !start.Before(end) {
writeError(w, http.StatusBadRequest, "start must be before end")
return time.Time{}, time.Time{}, false
}
return start, end, true
}