156 lines
4.1 KiB
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
|
|
}
|