diff --git a/internal/httpapi/dashboard.go b/internal/httpapi/dashboard.go new file mode 100644 index 0000000..88313db --- /dev/null +++ b/internal/httpapi/dashboard.go @@ -0,0 +1,85 @@ +package httpapi + +import ( + "net/http" + "time" + + "ai-service/internal/model" +) + +type dashboardResponse struct { + At time.Time `json:"at"` + Summary dashboardSummary `json:"summary"` + Stats *model.Stats `json:"stats"` + Providers providersStatusResponse `json:"providers"` + Infra infraStatusResponse `json:"infra"` + Jobs []*model.Job `json:"jobs"` +} + +type dashboardSummary struct { + Pending int64 `json:"pending"` + Running int64 `json:"running"` + Done int64 `json:"done"` + Failed int64 `json:"failed"` + Cancelled int64 `json:"cancelled"` + Total int64 `json:"total"` +} + +func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { + now := time.Now().UTC() + ctx, cancel := contextWithTimeout(r, 12*time.Second) + defer cancel() + + stats, err := s.store.Stats(ctx) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + jobs, err := s.store.ListJobs(ctx, model.JobFilter{ + Statuses: []string{model.StatusFailed, model.StatusRunning}, + Limit: 40, + }) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + + resp := dashboardResponse{ + At: now, + Summary: summarizeQueues(stats), + Stats: stats, + Providers: providersStatusResponse{ + At: now, + Providers: []providerStatus{ + s.checkLLM(ctx), + s.checkWhisperX(ctx), + }, + }, + Infra: loadInfraSnapshot(r, s.cfg), + Jobs: jobs, + } + writeJSON(w, http.StatusOK, resp) +} + +func summarizeQueues(stats *model.Stats) dashboardSummary { + var out dashboardSummary + if stats == nil { + return out + } + for _, row := range stats.Queues { + switch row.Status { + case model.StatusPending: + out.Pending += row.Total + case model.StatusRunning: + out.Running += row.Total + case model.StatusDone: + out.Done += row.Total + case model.StatusFailed: + out.Failed += row.Total + case model.StatusCancelled: + out.Cancelled += row.Total + } + out.Total += row.Total + } + return out +} diff --git a/internal/httpapi/infra.go b/internal/httpapi/infra.go index 4fd92e5..432e72f 100644 --- a/internal/httpapi/infra.go +++ b/internal/httpapi/infra.go @@ -8,6 +8,8 @@ import ( "net/http" "strings" "time" + + "ai-service/internal/config" ) type infraStatusResponse struct { @@ -17,15 +19,18 @@ type infraStatusResponse struct { } func (s *Server) handleInfraStatus(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, loadInfraSnapshot(r, s.cfg)) +} + +func loadInfraSnapshot(r *http.Request, cfg config.Config) infraStatusResponse { resp := infraStatusResponse{At: time.Now().UTC()} - baseURL := strings.TrimRight(strings.TrimSpace(s.cfg.AIStatsSidecarURL), "/") + baseURL := strings.TrimRight(strings.TrimSpace(cfg.AIStatsSidecarURL), "/") if baseURL == "" { resp.SidecarError = "AI stats sidecar is not configured" - writeJSON(w, http.StatusOK, resp) - return + return resp } - timeout := s.cfg.AIStatsTimeout + timeout := cfg.AIStatsTimeout if timeout <= 0 { timeout = 8 * time.Second } @@ -37,7 +42,7 @@ func (s *Server) handleInfraStatus(w http.ResponseWriter, r *http.Request) { } else { resp.Sidecar = sidecar } - writeJSON(w, http.StatusOK, resp) + return resp } func fetchAIStatsSidecar(ctx context.Context, baseURL string, timeout time.Duration) (map[string]any, error) { diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 7a9168b..f363667 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -69,6 +69,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handleProviderStatus(w, r) case r.Method == http.MethodGet && path == "/api/v1/infra/status": s.handleInfraStatus(w, r) + case r.Method == http.MethodGet && path == "/api/v1/dashboard": + s.handleDashboard(w, r) default: writeError(w, http.StatusNotFound, "not found") }