diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 3d08249..2486b44 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -98,7 +98,9 @@ func main() { scriptSvc := service.NewScriptServiceWithInterpreters(h.Repo, scriptRepo) scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdTracker) jobsHandlers := handlers.NewJobsHandlers(cmdTracker, scriptSvc, - os.Getenv("WHEREAMI") /* our address for redirects */) + os.Getenv("WHEREAMI"), /* our address for redirects */ + jobRepo, + ) // Initialize script management service and handlers scriptManageSvc := service.NewScriptService(h.Repo) @@ -206,6 +208,8 @@ func main() { { jobsGroup.POST("", jobsHandlers.AddJob) jobsGroup.POST("/:id/wait", jobsHandlers.WaitJob) + jobsGroup.GET("/metrics", jobsHandlers.GetJobMetrics) + jobsGroup.POST("/check_cmd", jobsHandlers.CheckCmd) } // Agent registration diff --git a/backend/internal/handlers/jobs.go b/backend/internal/handlers/jobs.go index 1b371ff..8a30c33 100644 --- a/backend/internal/handlers/jobs.go +++ b/backend/internal/handlers/jobs.go @@ -6,9 +6,11 @@ import ( "net/http" "os/exec" "strconv" + "time" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" + "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service" "github.com/gin-gonic/gin" ) @@ -17,10 +19,11 @@ type JobsHandlers struct { tracker *commander.ConnTracker svc *service.ScriptService whereami string + jobRepo *repository.JobRepository } -func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string) JobsHandlers { - return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami} +func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string, jobRepo *repository.JobRepository) JobsHandlers { + return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami, jobRepo: jobRepo} } // AddJobIn is the request body for creating a job. @@ -197,3 +200,46 @@ type CheckCmdIn struct { type CheckCmdOut struct { Exists bool `json:"exists"` } + +// JobMetricsOut is the response body for the job metrics endpoint. +type JobMetricsOut struct { + Total int `json:"total"` + Success int `json:"success"` + Failed int `json:"failed"` + Pending int `json:"pending"` + Period string `json:"period"` +} + +// GetJobMetrics returns job success metrics over a parameterized period. +// @Summary Get job metrics +// @Description Returns total, successful, failed, and pending job counts over the given period +// @Tags jobs +// @Produce json +// @Param period query string false "Time period (e.g. 1h, 24h, 7d)" default(24h) +// @Success 200 {object} JobMetricsOut +// @Failure 400 {object} map[string]string +// @Security Bearer +// @Router /jobs/metrics [get] +func (h *JobsHandlers) GetJobMetrics(c *gin.Context) { + periodStr := c.DefaultQuery("period", "24h") + period, err := time.ParseDuration(periodStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid period, use Go duration format (e.g. 1h, 24h, 7d)"}) + return + } + + since := time.Now().Add(-period) + metrics, err := h.jobRepo.GetJobMetrics(c.Request.Context(), since) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, JobMetricsOut{ + Total: metrics.Total, + Success: metrics.Success, + Failed: metrics.Failed, + Pending: metrics.Pending, + Period: periodStr, + }) +} diff --git a/backend/internal/repository/job_repository.go b/backend/internal/repository/job_repository.go index acef903..20ac2ea 100644 --- a/backend/internal/repository/job_repository.go +++ b/backend/internal/repository/job_repository.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "time" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" @@ -103,3 +104,29 @@ func (r *JobRepository) GetJobByID(ctx context.Context, jid int64) (models.Job, job.Stdin = stdinVal return job, nil } + +type JobMetrics struct { + Total int + Success int + Failed int + Pending int +} + +// GetJobMetrics returns job success metrics for jobs updated since the given time. +// A successful job has status == 0, failed has status != 0, pending has status == 0 with empty stdout/stderr. +func (r *JobRepository) GetJobMetrics(ctx context.Context, since time.Time) (JobMetrics, error) { + var m JobMetrics + err := r.DB.QueryRowContext(ctx, + `SELECT + COUNT(*), + SUM(CASE WHEN status = 0 AND (stdout != '' OR stderr != '') THEN 1 ELSE 0 END), + SUM(CASE WHEN status != 0 THEN 1 ELSE 0 END), + SUM(CASE WHEN status = 0 AND stdout = '' AND stderr = '' THEN 1 ELSE 0 END) + FROM jobs WHERE updated_at >= ?`, + since, + ).Scan(&m.Total, &m.Success, &m.Failed, &m.Pending) + if err != nil { + return JobMetrics{}, err + } + return m, nil +}