...
This commit is contained in:
		
							
								
								
									
										445
									
								
								pkg/heroagent/handlers/job_handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								pkg/heroagent/handlers/job_handlers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,445 @@ | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/herojobs" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| ) | ||||
|  | ||||
| // HeroJobsClientInterface defines the interface for the HeroJobs client | ||||
| type HeroJobsClientInterface interface { | ||||
| 	Connect() error | ||||
| 	Close() error | ||||
| 	SubmitJob(job *herojobs.Job) (*herojobs.Job, error) | ||||
| 	GetJob(jobID string) (*herojobs.Job, error) | ||||
| 	DeleteJob(jobID string) error | ||||
| 	ListJobs(circleID, topic string) ([]string, error) | ||||
| 	QueueSize(circleID, topic string) (int64, error) | ||||
| 	QueueEmpty(circleID, topic string) error | ||||
| 	QueueGet(circleID, topic string) (*herojobs.Job, error) | ||||
| 	CreateJob(circleID, topic, sessionKey, heroScript, rhaiScript string) (*herojobs.Job, error) | ||||
| } | ||||
|  | ||||
| // JobHandler handles job-related routes | ||||
| type JobHandler struct { | ||||
| 	client HeroJobsClientInterface | ||||
| 	logger *log.Logger | ||||
| } | ||||
|  | ||||
| // NewJobHandler creates a new JobHandler | ||||
| func NewJobHandler(socketPath string, logger *log.Logger) (*JobHandler, error) { | ||||
| 	client, err := herojobs.NewClient(socketPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create HeroJobs client: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return &JobHandler{ | ||||
| 		client: client, | ||||
| 		logger: logger, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RegisterRoutes registers job API routes | ||||
| func (h *JobHandler) RegisterRoutes(app *fiber.App) { | ||||
| 	// Register common routes to both API and admin groups | ||||
| 	jobRoutes := func(group fiber.Router) { | ||||
| 		group.Post("/submit", h.submitJob) | ||||
| 		group.Get("/get/:id", h.getJob) | ||||
| 		group.Delete("/delete/:id", h.deleteJob) | ||||
| 		group.Get("/list", h.listJobs) | ||||
| 		group.Get("/queue/size", h.queueSize) | ||||
| 		group.Post("/queue/empty", h.queueEmpty) | ||||
| 		group.Get("/queue/get", h.queueGet) | ||||
| 		group.Post("/create", h.createJob) | ||||
| 	} | ||||
|  | ||||
| 	// Apply common routes to API group | ||||
| 	apiJobs := app.Group("/api/jobs") | ||||
| 	jobRoutes(apiJobs) | ||||
|  | ||||
| 	// Apply common routes to admin group | ||||
| 	adminJobs := app.Group("/admin/jobs") | ||||
| 	jobRoutes(adminJobs) | ||||
| } | ||||
|  | ||||
| // @Summary Submit a job | ||||
| // @Description Submit a new job to the HeroJobs server | ||||
| // @Tags jobs | ||||
| // @Accept json | ||||
| // @Produce json | ||||
| // @Param job body herojobs.Job true "Job to submit" | ||||
| // @Success 200 {object} herojobs.Job | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/submit [post] | ||||
| // @Router /admin/jobs/submit [post] | ||||
| func (h *JobHandler) submitJob(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Parse job from request body | ||||
| 	var job herojobs.Job | ||||
| 	if err := c.BodyParser(&job); err != nil { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to parse job data: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Submit job | ||||
| 	submittedJob, err := h.client.SubmitJob(&job) | ||||
| 	if err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to submit job: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(submittedJob) | ||||
| } | ||||
|  | ||||
| // @Summary Get a job | ||||
| // @Description Get a job by ID | ||||
| // @Tags jobs | ||||
| // @Produce json | ||||
| // @Param id path string true "Job ID" | ||||
| // @Success 200 {object} herojobs.Job | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/get/{id} [get] | ||||
| // @Router /admin/jobs/get/{id} [get] | ||||
| func (h *JobHandler) getJob(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Get job ID from path parameter | ||||
| 	jobID := c.Params("id") | ||||
| 	if jobID == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Job ID is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Get job | ||||
| 	job, err := h.client.GetJob(jobID) | ||||
| 	if err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to get job: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(job) | ||||
| } | ||||
|  | ||||
| // @Summary Delete a job | ||||
| // @Description Delete a job by ID | ||||
| // @Tags jobs | ||||
| // @Produce json | ||||
| // @Param id path string true "Job ID" | ||||
| // @Success 200 {object} map[string]string | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/delete/{id} [delete] | ||||
| // @Router /admin/jobs/delete/{id} [delete] | ||||
| func (h *JobHandler) deleteJob(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Get job ID from path parameter | ||||
| 	jobID := c.Params("id") | ||||
| 	if jobID == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Job ID is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Delete job | ||||
| 	if err := h.client.DeleteJob(jobID); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to delete job: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"status":  "success", | ||||
| 		"message": fmt.Sprintf("Job %s deleted successfully", jobID), | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // @Summary List jobs | ||||
| // @Description List jobs by circle ID and topic | ||||
| // @Tags jobs | ||||
| // @Produce json | ||||
| // @Param circleid query string true "Circle ID" | ||||
| // @Param topic query string true "Topic" | ||||
| // @Success 200 {object} map[string][]string | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/list [get] | ||||
| // @Router /admin/jobs/list [get] | ||||
| func (h *JobHandler) listJobs(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Get parameters from query | ||||
| 	circleID := c.Query("circleid") | ||||
| 	if circleID == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Circle ID is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	topic := c.Query("topic") | ||||
| 	if topic == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Topic is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// List jobs | ||||
| 	jobs, err := h.client.ListJobs(circleID, topic) | ||||
| 	if err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to list jobs: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"status": "success", | ||||
| 		"jobs":   jobs, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // @Summary Get queue size | ||||
| // @Description Get the size of a job queue by circle ID and topic | ||||
| // @Tags jobs | ||||
| // @Produce json | ||||
| // @Param circleid query string true "Circle ID" | ||||
| // @Param topic query string true "Topic" | ||||
| // @Success 200 {object} map[string]int64 | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/queue/size [get] | ||||
| // @Router /admin/jobs/queue/size [get] | ||||
| func (h *JobHandler) queueSize(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Get parameters from query | ||||
| 	circleID := c.Query("circleid") | ||||
| 	if circleID == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Circle ID is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	topic := c.Query("topic") | ||||
| 	if topic == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Topic is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Get queue size | ||||
| 	size, err := h.client.QueueSize(circleID, topic) | ||||
| 	if err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to get queue size: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"status": "success", | ||||
| 		"size":   size, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // @Summary Empty queue | ||||
| // @Description Empty a job queue by circle ID and topic | ||||
| // @Tags jobs | ||||
| // @Accept json | ||||
| // @Produce json | ||||
| // @Param body body object true "Queue parameters" | ||||
| // @Success 200 {object} map[string]string | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/queue/empty [post] | ||||
| // @Router /admin/jobs/queue/empty [post] | ||||
| func (h *JobHandler) queueEmpty(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Parse parameters from request body | ||||
| 	var params struct { | ||||
| 		CircleID string `json:"circleid"` | ||||
| 		Topic    string `json:"topic"` | ||||
| 	} | ||||
| 	if err := c.BodyParser(¶ms); err != nil { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to parse parameters: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if params.CircleID == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Circle ID is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if params.Topic == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Topic is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Empty queue | ||||
| 	if err := h.client.QueueEmpty(params.CircleID, params.Topic); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to empty queue: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"status":  "success", | ||||
| 		"message": fmt.Sprintf("Queue for circle %s and topic %s emptied successfully", params.CircleID, params.Topic), | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // @Summary Get job from queue | ||||
| // @Description Get a job from a queue without removing it | ||||
| // @Tags jobs | ||||
| // @Produce json | ||||
| // @Param circleid query string true "Circle ID" | ||||
| // @Param topic query string true "Topic" | ||||
| // @Success 200 {object} herojobs.Job | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/queue/get [get] | ||||
| // @Router /admin/jobs/queue/get [get] | ||||
| func (h *JobHandler) queueGet(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Get parameters from query | ||||
| 	circleID := c.Query("circleid") | ||||
| 	if circleID == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Circle ID is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	topic := c.Query("topic") | ||||
| 	if topic == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Topic is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Get job from queue | ||||
| 	job, err := h.client.QueueGet(circleID, topic) | ||||
| 	if err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to get job from queue: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(job) | ||||
| } | ||||
|  | ||||
| // @Summary Create job | ||||
| // @Description Create a new job with the given parameters | ||||
| // @Tags jobs | ||||
| // @Accept json | ||||
| // @Produce json | ||||
| // @Param body body object true "Job parameters" | ||||
| // @Success 200 {object} herojobs.Job | ||||
| // @Failure 400 {object} map[string]string | ||||
| // @Failure 500 {object} map[string]string | ||||
| // @Router /api/jobs/create [post] | ||||
| // @Router /admin/jobs/create [post] | ||||
| func (h *JobHandler) createJob(c *fiber.Ctx) error { | ||||
| 	// Connect to the HeroJobs server | ||||
| 	if err := h.client.Connect(); err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to connect to HeroJobs server: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
| 	defer h.client.Close() | ||||
|  | ||||
| 	// Parse parameters from request body | ||||
| 	var params struct { | ||||
| 		CircleID   string `json:"circleid"` | ||||
| 		Topic      string `json:"topic"` | ||||
| 		SessionKey string `json:"sessionkey"` | ||||
| 		HeroScript string `json:"heroscript"` | ||||
| 		RhaiScript string `json:"rhaiscript"` | ||||
| 	} | ||||
| 	if err := c.BodyParser(¶ms); err != nil { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to parse parameters: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if params.CircleID == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Circle ID is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if params.Topic == "" { | ||||
| 		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Topic is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Create job | ||||
| 	job, err := h.client.CreateJob( | ||||
| 		params.CircleID, | ||||
| 		params.Topic, | ||||
| 		params.SessionKey, | ||||
| 		params.HeroScript, | ||||
| 		params.RhaiScript, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to create job: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(job) | ||||
| } | ||||
							
								
								
									
										638
									
								
								pkg/heroagent/handlers/job_handlers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										638
									
								
								pkg/heroagent/handlers/job_handlers_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,638 @@ | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/herojobs" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| ) | ||||
|  | ||||
| // MockHeroJobsClient is a mock implementation of the HeroJobs client | ||||
| type MockHeroJobsClient struct { | ||||
| 	mock.Mock | ||||
| } | ||||
|  | ||||
| // Connect mocks the Connect method | ||||
| func (m *MockHeroJobsClient) Connect() error { | ||||
| 	args := m.Called() | ||||
| 	return args.Error(0) | ||||
| } | ||||
|  | ||||
| // Close mocks the Close method | ||||
| func (m *MockHeroJobsClient) Close() error { | ||||
| 	args := m.Called() | ||||
| 	return args.Error(0) | ||||
| } | ||||
|  | ||||
| // SubmitJob mocks the SubmitJob method | ||||
| func (m *MockHeroJobsClient) SubmitJob(job *herojobs.Job) (*herojobs.Job, error) { | ||||
| 	args := m.Called(job) | ||||
| 	if args.Get(0) == nil { | ||||
| 		return nil, args.Error(1) | ||||
| 	} | ||||
| 	return args.Get(0).(*herojobs.Job), args.Error(1) | ||||
| } | ||||
|  | ||||
| // GetJob mocks the GetJob method | ||||
| func (m *MockHeroJobsClient) GetJob(jobID string) (*herojobs.Job, error) { | ||||
| 	args := m.Called(jobID) | ||||
| 	if args.Get(0) == nil { | ||||
| 		return nil, args.Error(1) | ||||
| 	} | ||||
| 	return args.Get(0).(*herojobs.Job), args.Error(1) | ||||
| } | ||||
|  | ||||
| // DeleteJob mocks the DeleteJob method | ||||
| func (m *MockHeroJobsClient) DeleteJob(jobID string) error { | ||||
| 	args := m.Called(jobID) | ||||
| 	return args.Error(0) | ||||
| } | ||||
|  | ||||
| // ListJobs mocks the ListJobs method | ||||
| func (m *MockHeroJobsClient) ListJobs(circleID, topic string) ([]string, error) { | ||||
| 	args := m.Called(circleID, topic) | ||||
| 	if args.Get(0) == nil { | ||||
| 		return nil, args.Error(1) | ||||
| 	} | ||||
| 	return args.Get(0).([]string), args.Error(1) | ||||
| } | ||||
|  | ||||
| // QueueSize mocks the QueueSize method | ||||
| func (m *MockHeroJobsClient) QueueSize(circleID, topic string) (int64, error) { | ||||
| 	args := m.Called(circleID, topic) | ||||
| 	return args.Get(0).(int64), args.Error(1) | ||||
| } | ||||
|  | ||||
| // QueueEmpty mocks the QueueEmpty method | ||||
| func (m *MockHeroJobsClient) QueueEmpty(circleID, topic string) error { | ||||
| 	args := m.Called(circleID, topic) | ||||
| 	return args.Error(0) | ||||
| } | ||||
|  | ||||
| // QueueGet mocks the QueueGet method | ||||
| func (m *MockHeroJobsClient) QueueGet(circleID, topic string) (*herojobs.Job, error) { | ||||
| 	args := m.Called(circleID, topic) | ||||
| 	if args.Get(0) == nil { | ||||
| 		return nil, args.Error(1) | ||||
| 	} | ||||
| 	return args.Get(0).(*herojobs.Job), args.Error(1) | ||||
| } | ||||
|  | ||||
| // CreateJob mocks the CreateJob method | ||||
| func (m *MockHeroJobsClient) CreateJob(circleID, topic, sessionKey, heroScript, rhaiScript string) (*herojobs.Job, error) { | ||||
| 	args := m.Called(circleID, topic, sessionKey, heroScript, rhaiScript) | ||||
| 	if args.Get(0) == nil { | ||||
| 		return nil, args.Error(1) | ||||
| 	} | ||||
| 	return args.Get(0).(*herojobs.Job), args.Error(1) | ||||
| } | ||||
|  | ||||
| // setupTest initializes a test environment with a mock client | ||||
| func setupTest() (*JobHandler, *MockHeroJobsClient, *fiber.App) { | ||||
| 	mockClient := new(MockHeroJobsClient) | ||||
| 	handler := &JobHandler{ | ||||
| 		client: mockClient, | ||||
| 	} | ||||
|  | ||||
| 	app := fiber.New() | ||||
|  | ||||
| 	// Register routes | ||||
| 	api := app.Group("/api") | ||||
| 	jobs := api.Group("/jobs") | ||||
| 	jobs.Post("/create", handler.createJob) | ||||
| 	jobs.Get("/queue/get", handler.queueGet) | ||||
| 	jobs.Post("/queue/empty", handler.queueEmpty) | ||||
| 	jobs.Post("/submit", handler.submitJob) | ||||
| 	jobs.Get("/get/:jobid", handler.getJob) | ||||
| 	jobs.Delete("/delete/:jobid", handler.deleteJob) | ||||
| 	jobs.Get("/list", handler.listJobs) | ||||
| 	jobs.Get("/queue/size", handler.queueSize) | ||||
|  | ||||
| 	return handler, mockClient, app | ||||
| } | ||||
|  | ||||
| // createTestRequest creates a test request with the given method, path, and body | ||||
| func createTestRequest(method, path string, body io.Reader) (*http.Request, error) { | ||||
| 	req := httptest.NewRequest(method, path, body) | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 	return req, nil | ||||
| } | ||||
|  | ||||
| // TestQueueEmpty tests the queueEmpty handler | ||||
| func TestQueueEmpty(t *testing.T) { | ||||
| 	// Test cases | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		circleID       string | ||||
| 		topic          string | ||||
| 		connectError   error | ||||
| 		emptyError     error | ||||
| 		expectedStatus int | ||||
| 		expectedBody   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Success", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   nil, | ||||
| 			emptyError:     nil, | ||||
| 			expectedStatus: fiber.StatusOK, | ||||
| 			expectedBody:   `{"status":"success","message":"Queue for circle test-circle and topic test-topic emptied successfully"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Connection Error", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   errors.New("connection error"), | ||||
| 			emptyError:     nil, | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to connect to HeroJobs server: connection error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Error", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   nil, | ||||
| 			emptyError:     errors.New("empty error"), | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to empty queue: empty error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Circle ID", | ||||
| 			circleID:       "", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   nil, | ||||
| 			emptyError:     nil, | ||||
| 			expectedStatus: fiber.StatusBadRequest, | ||||
| 			expectedBody:   `{"error":"Circle ID is required"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Topic", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "", | ||||
| 			connectError:   nil, | ||||
| 			emptyError:     nil, | ||||
| 			expectedStatus: fiber.StatusBadRequest, | ||||
| 			expectedBody:   `{"error":"Topic is required"}`, | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			// Create a new mock client for each test | ||||
| 			mockClient := new(MockHeroJobsClient) | ||||
| 			 | ||||
| 			// Setup mock expectations - Connect is always called in the handler | ||||
| 			mockClient.On("Connect").Return(tc.connectError) | ||||
| 			 | ||||
| 			// QueueEmpty and Close are only called if Connect succeeds and parameters are valid | ||||
| 			if tc.connectError == nil && tc.circleID != "" && tc.topic != "" { | ||||
| 				mockClient.On("QueueEmpty", tc.circleID, tc.topic).Return(tc.emptyError) | ||||
| 				mockClient.On("Close").Return(nil) | ||||
| 			} else { | ||||
| 				// Close is still called via defer even if we return early | ||||
| 				mockClient.On("Close").Return(nil).Maybe() | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new handler with the mock client | ||||
| 			handler := &JobHandler{ | ||||
| 				client: mockClient, | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new app for each test | ||||
| 			app := fiber.New() | ||||
| 			api := app.Group("/api") | ||||
| 			jobs := api.Group("/jobs") | ||||
| 			jobs.Post("/queue/empty", handler.queueEmpty) | ||||
| 			 | ||||
| 			// Create request body | ||||
| 			reqBody := map[string]string{ | ||||
| 				"circleid": tc.circleID, | ||||
| 				"topic":    tc.topic, | ||||
| 			} | ||||
| 			reqBodyBytes, err := json.Marshal(reqBody) | ||||
| 			assert.NoError(t, err) | ||||
| 			 | ||||
| 			// Create test request | ||||
| 			req, err := createTestRequest(http.MethodPost, "/api/jobs/queue/empty", bytes.NewReader(reqBodyBytes)) | ||||
| 			assert.NoError(t, err) | ||||
| 			req.Header.Set("Content-Type", "application/json") | ||||
| 			 | ||||
| 			// Perform the request | ||||
| 			resp, err := app.Test(req) | ||||
| 			assert.NoError(t, err) | ||||
| 			 | ||||
| 			// Check status code | ||||
| 			assert.Equal(t, tc.expectedStatus, resp.StatusCode) | ||||
| 			 | ||||
| 			// Check response body | ||||
| 			body, err := io.ReadAll(resp.Body) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.JSONEq(t, tc.expectedBody, string(body)) | ||||
| 			 | ||||
| 			// Verify that all expectations were met | ||||
| 			mockClient.AssertExpectations(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // TestQueueGet tests the queueGet handler | ||||
| func TestQueueGet(t *testing.T) { | ||||
| 	// Create a test job | ||||
| 	testJob := &herojobs.Job{ | ||||
| 		JobID:    "test-job-id", | ||||
| 		CircleID: "test-circle", | ||||
| 		Topic:    "test-topic", | ||||
| 	} | ||||
| 	 | ||||
| 	// Test cases | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		circleID       string | ||||
| 		topic          string | ||||
| 		connectError   error | ||||
| 		getError       error | ||||
| 		getResponse    *herojobs.Job | ||||
| 		expectedStatus int | ||||
| 		expectedBody   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Success", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   nil, | ||||
| 			getError:       nil, | ||||
| 			getResponse:    testJob, | ||||
| 			expectedStatus: fiber.StatusOK, | ||||
| 			// Include all fields in the response, even empty ones | ||||
| 			expectedBody:   `{"jobid":"test-job-id","circleid":"test-circle","topic":"test-topic","error":"","heroscript":"","result":"","rhaiscript":"","sessionkey":"","status":"","time_end":0,"time_scheduled":0,"time_start":0,"timeout":0}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Connection Error", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   errors.New("connection error"), | ||||
| 			getError:       nil, | ||||
| 			getResponse:    nil, | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to connect to HeroJobs server: connection error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Get Error", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   nil, | ||||
| 			getError:       errors.New("get error"), | ||||
| 			getResponse:    nil, | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to get job from queue: get error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Circle ID", | ||||
| 			circleID:       "", | ||||
| 			topic:          "test-topic", | ||||
| 			connectError:   nil, | ||||
| 			getError:       nil, | ||||
| 			getResponse:    nil, | ||||
| 			expectedStatus: fiber.StatusBadRequest, | ||||
| 			expectedBody:   `{"error":"Circle ID is required"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Topic", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "", | ||||
| 			connectError:   nil, | ||||
| 			getError:       nil, | ||||
| 			getResponse:    nil, | ||||
| 			expectedStatus: fiber.StatusBadRequest, | ||||
| 			expectedBody:   `{"error":"Topic is required"}`, | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			// Create a new mock client for each test | ||||
| 			mockClient := new(MockHeroJobsClient) | ||||
| 			 | ||||
| 			// Setup mock expectations - Connect is always called in the handler | ||||
| 			mockClient.On("Connect").Return(tc.connectError) | ||||
| 			 | ||||
| 			// QueueGet and Close are only called if Connect succeeds and parameters are valid | ||||
| 			if tc.connectError == nil && tc.circleID != "" && tc.topic != "" { | ||||
| 				mockClient.On("QueueGet", tc.circleID, tc.topic).Return(tc.getResponse, tc.getError) | ||||
| 				mockClient.On("Close").Return(nil) | ||||
| 			} else { | ||||
| 				// Close is still called via defer even if we return early | ||||
| 				mockClient.On("Close").Return(nil).Maybe() | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new handler with the mock client | ||||
| 			handler := &JobHandler{ | ||||
| 				client: mockClient, | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new app for each test | ||||
| 			app := fiber.New() | ||||
| 			api := app.Group("/api") | ||||
| 			jobs := api.Group("/jobs") | ||||
| 			jobs.Get("/queue/get", handler.queueGet) | ||||
| 			 | ||||
| 			// Create test request | ||||
| 			path := fmt.Sprintf("/api/jobs/queue/get?circleid=%s&topic=%s", tc.circleID, tc.topic) | ||||
| 			req, err := createTestRequest(http.MethodGet, path, nil) | ||||
| 			assert.NoError(t, err) | ||||
| 			 | ||||
| 			// Perform the request | ||||
| 			resp, err := app.Test(req) | ||||
| 			assert.NoError(t, err) | ||||
| 			 | ||||
| 			// Check status code | ||||
| 			assert.Equal(t, tc.expectedStatus, resp.StatusCode) | ||||
| 			 | ||||
| 			// Check response body | ||||
| 			body, err := io.ReadAll(resp.Body) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.JSONEq(t, tc.expectedBody, string(body)) | ||||
| 			 | ||||
| 			// Verify that all expectations were met | ||||
| 			mockClient.AssertExpectations(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // TestCreateJob tests the createJob handler | ||||
| func TestCreateJob(t *testing.T) { | ||||
| 	// Create a test job | ||||
| 	testJob := &herojobs.Job{ | ||||
| 		JobID:    "test-job-id", | ||||
| 		CircleID: "test-circle", | ||||
| 		Topic:    "test-topic", | ||||
| 	} | ||||
| 	 | ||||
| 	// Test cases | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		circleID       string | ||||
| 		topic          string | ||||
| 		sessionKey     string | ||||
| 		heroScript     string | ||||
| 		rhaiScript     string | ||||
| 		connectError   error | ||||
| 		createError    error | ||||
| 		createResponse *herojobs.Job | ||||
| 		expectedStatus int | ||||
| 		expectedBody   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Success", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			sessionKey:     "test-key", | ||||
| 			heroScript:     "test-hero-script", | ||||
| 			rhaiScript:     "test-rhai-script", | ||||
| 			connectError:   nil, | ||||
| 			createError:    nil, | ||||
| 			createResponse: testJob, | ||||
| 			expectedStatus: fiber.StatusOK, | ||||
| 			expectedBody:   `{"jobid":"test-job-id","circleid":"test-circle","topic":"test-topic","error":"","heroscript":"","result":"","rhaiscript":"","sessionkey":"","status":"","time_end":0,"time_scheduled":0,"time_start":0,"timeout":0}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Connection Error", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			sessionKey:     "test-key", | ||||
| 			heroScript:     "test-hero-script", | ||||
| 			rhaiScript:     "test-rhai-script", | ||||
| 			connectError:   errors.New("connection error"), | ||||
| 			createError:    nil, | ||||
| 			createResponse: nil, | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to connect to HeroJobs server: connection error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Create Error", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "test-topic", | ||||
| 			sessionKey:     "test-key", | ||||
| 			heroScript:     "test-hero-script", | ||||
| 			rhaiScript:     "test-rhai-script", | ||||
| 			connectError:   nil, | ||||
| 			createError:    errors.New("create error"), | ||||
| 			createResponse: nil, | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to create job: create error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Circle ID", | ||||
| 			circleID:       "", | ||||
| 			topic:          "test-topic", | ||||
| 			sessionKey:     "test-key", | ||||
| 			heroScript:     "test-hero-script", | ||||
| 			rhaiScript:     "test-rhai-script", | ||||
| 			connectError:   nil, | ||||
| 			createError:    nil, | ||||
| 			createResponse: nil, | ||||
| 			expectedStatus: fiber.StatusBadRequest, | ||||
| 			expectedBody:   `{"error":"Circle ID is required"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Topic", | ||||
| 			circleID:       "test-circle", | ||||
| 			topic:          "", | ||||
| 			sessionKey:     "test-key", | ||||
| 			heroScript:     "test-hero-script", | ||||
| 			rhaiScript:     "test-rhai-script", | ||||
| 			connectError:   nil, | ||||
| 			createError:    nil, | ||||
| 			createResponse: nil, | ||||
| 			expectedStatus: fiber.StatusBadRequest, | ||||
| 			expectedBody:   `{"error":"Topic is required"}`, | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			// Create a new mock client for each test | ||||
| 			mockClient := new(MockHeroJobsClient) | ||||
| 			 | ||||
| 			// Setup mock expectations - Connect is always called in the handler | ||||
| 			mockClient.On("Connect").Return(tc.connectError) | ||||
| 			 | ||||
| 			// CreateJob and Close are only called if Connect succeeds and parameters are valid | ||||
| 			if tc.connectError == nil && tc.circleID != "" && tc.topic != "" { | ||||
| 				mockClient.On("CreateJob", tc.circleID, tc.topic, tc.sessionKey, tc.heroScript, tc.rhaiScript).Return(tc.createResponse, tc.createError) | ||||
| 				mockClient.On("Close").Return(nil) | ||||
| 			} else { | ||||
| 				// Close is still called via defer even if we return early | ||||
| 				mockClient.On("Close").Return(nil).Maybe() | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new handler with the mock client | ||||
| 			handler := &JobHandler{ | ||||
| 				client: mockClient, | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new app for each test | ||||
| 			app := fiber.New() | ||||
| 			api := app.Group("/api") | ||||
| 			jobs := api.Group("/jobs") | ||||
| 			jobs.Post("/create", handler.createJob) | ||||
| 			 | ||||
| 			// Create request body | ||||
| 			reqBody := map[string]string{ | ||||
| 				"circleid":    tc.circleID, | ||||
| 				"topic":       tc.topic, | ||||
| 				"sessionkey":  tc.sessionKey, | ||||
| 				"heroscript":  tc.heroScript, | ||||
| 				"rhaiscript":  tc.rhaiScript, | ||||
| 			} | ||||
| 			reqBodyBytes, err := json.Marshal(reqBody) | ||||
| 			assert.NoError(t, err) | ||||
| 			 | ||||
| 			// Create test request | ||||
| 			req, err := createTestRequest(http.MethodPost, "/api/jobs/create", bytes.NewReader(reqBodyBytes)) | ||||
| 			assert.NoError(t, err) | ||||
| 			req.Header.Set("Content-Type", "application/json") | ||||
| 			 | ||||
| 			// Perform the request | ||||
| 			resp, err := app.Test(req) | ||||
| 			assert.NoError(t, err) | ||||
| 			 | ||||
| 			// Check status code | ||||
| 			assert.Equal(t, tc.expectedStatus, resp.StatusCode) | ||||
| 			 | ||||
| 			// Check response body | ||||
| 			body, err := io.ReadAll(resp.Body) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.JSONEq(t, tc.expectedBody, string(body)) | ||||
| 			 | ||||
| 			// Verify that all expectations were met | ||||
| 			mockClient.AssertExpectations(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // TestSubmitJob tests the submitJob handler | ||||
| func TestSubmitJob(t *testing.T) { | ||||
| 	// Create a test job | ||||
| 	testJob := &herojobs.Job{ | ||||
| 		JobID:    "test-job-id", | ||||
| 		CircleID: "test-circle", | ||||
| 		Topic:    "test-topic", | ||||
| 	} | ||||
| 	 | ||||
| 	// Test cases | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		job            *herojobs.Job | ||||
| 		connectError   error | ||||
| 		submitError    error | ||||
| 		submitResponse *herojobs.Job | ||||
| 		expectedStatus int | ||||
| 		expectedBody   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Success", | ||||
| 			job:            testJob, | ||||
| 			connectError:   nil, | ||||
| 			submitError:    nil, | ||||
| 			submitResponse: testJob, | ||||
| 			expectedStatus: fiber.StatusOK, | ||||
| 			expectedBody:   `{"jobid":"test-job-id","circleid":"test-circle","topic":"test-topic","error":"","heroscript":"","result":"","rhaiscript":"","sessionkey":"","status":"","time_end":0,"time_scheduled":0,"time_start":0,"timeout":0}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Connection Error", | ||||
| 			job:            testJob, | ||||
| 			connectError:   errors.New("connection error"), | ||||
| 			submitError:    nil, | ||||
| 			submitResponse: nil, | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to connect to HeroJobs server: connection error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Submit Error", | ||||
| 			job:            testJob, | ||||
| 			connectError:   nil, | ||||
| 			submitError:    errors.New("submit error"), | ||||
| 			submitResponse: nil, | ||||
| 			expectedStatus: fiber.StatusInternalServerError, | ||||
| 			expectedBody:   `{"error":"Failed to submit job: submit error"}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty Job", | ||||
| 			job:            nil, | ||||
| 			connectError:   nil, | ||||
| 			submitError:    nil, | ||||
| 			submitResponse: nil, | ||||
| 			expectedStatus: fiber.StatusBadRequest, | ||||
| 			expectedBody:   `{"error":"Failed to parse job data: unexpected end of JSON input"}`, | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			// Create a new mock client for each test | ||||
| 			mockClient := new(MockHeroJobsClient) | ||||
| 			 | ||||
| 			// Setup mock expectations - Connect is always called in the handler | ||||
| 			mockClient.On("Connect").Return(tc.connectError) | ||||
| 			 | ||||
| 			// SubmitJob and Close are only called if Connect succeeds and job is not nil | ||||
| 			if tc.connectError == nil && tc.job != nil { | ||||
| 				mockClient.On("SubmitJob", tc.job).Return(tc.submitResponse, tc.submitError) | ||||
| 				mockClient.On("Close").Return(nil) | ||||
| 			} else { | ||||
| 				// Close is still called via defer even if we return early | ||||
| 				mockClient.On("Close").Return(nil).Maybe() | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new handler with the mock client | ||||
| 			handler := &JobHandler{ | ||||
| 				client: mockClient, | ||||
| 			} | ||||
| 			 | ||||
| 			// Create a new app for each test | ||||
| 			app := fiber.New() | ||||
| 			api := app.Group("/api") | ||||
| 			jobs := api.Group("/jobs") | ||||
| 			jobs.Post("/submit", handler.submitJob) | ||||
| 			 | ||||
| 			// Create request body | ||||
| 			var reqBodyBytes []byte | ||||
| 			var err error | ||||
| 			if tc.job != nil { | ||||
| 				reqBodyBytes, err = json.Marshal(tc.job) | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 			 | ||||
| 			// Create test request | ||||
| 			req, err := createTestRequest(http.MethodPost, "/api/jobs/submit", bytes.NewReader(reqBodyBytes)) | ||||
| 			assert.NoError(t, err) | ||||
| 			req.Header.Set("Content-Type", "application/json") | ||||
| 			 | ||||
| 			// Perform the request | ||||
| 			resp, err := app.Test(req) | ||||
| 			assert.NoError(t, err) | ||||
| 			 | ||||
| 			// Check status code | ||||
| 			assert.Equal(t, tc.expectedStatus, resp.StatusCode) | ||||
| 			 | ||||
| 			// Check response body | ||||
| 			body, err := io.ReadAll(resp.Body) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.JSONEq(t, tc.expectedBody, string(body)) | ||||
| 			 | ||||
| 			// Verify that all expectations were met | ||||
| 			mockClient.AssertExpectations(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										4975
									
								
								pkg/heroagent/handlers/job_handlers_test.go.bak
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4975
									
								
								pkg/heroagent/handlers/job_handlers_test.go.bak
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										555
									
								
								pkg/heroagent/handlers/log_handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										555
									
								
								pkg/heroagent/handlers/log_handlers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,555 @@ | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/logger" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| ) | ||||
|  | ||||
| // LogHandler handles log-related routes | ||||
| type LogHandler struct { | ||||
| 	systemLogger  *logger.Logger | ||||
| 	serviceLogger *logger.Logger | ||||
| 	jobLogger     *logger.Logger | ||||
| 	processLogger *logger.Logger | ||||
| 	logBasePath   string | ||||
| } | ||||
|  | ||||
| // NewLogHandler creates a new LogHandler | ||||
| func NewLogHandler(logPath string) (*LogHandler, error) { | ||||
| 	// Create base directories for different log types | ||||
| 	systemLogPath := filepath.Join(logPath, "system") | ||||
| 	serviceLogPath := filepath.Join(logPath, "services") | ||||
| 	jobLogPath := filepath.Join(logPath, "jobs") | ||||
| 	processLogPath := filepath.Join(logPath, "processes") | ||||
|  | ||||
| 	// Create logger instances for each type | ||||
| 	systemLogger, err := logger.New(systemLogPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create system logger: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	serviceLogger, err := logger.New(serviceLogPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create service logger: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	jobLogger, err := logger.New(jobLogPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create job logger: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	processLogger, err := logger.New(processLogPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create process logger: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("Log handler created successfully with paths:\n  System: %s\n  Services: %s\n  Jobs: %s\n  Processes: %s\n", | ||||
| 		systemLogPath, serviceLogPath, jobLogPath, processLogPath) | ||||
|  | ||||
| 	return &LogHandler{ | ||||
| 		systemLogger:  systemLogger, | ||||
| 		serviceLogger: serviceLogger, | ||||
| 		jobLogger:     jobLogger, | ||||
| 		processLogger: processLogger, | ||||
| 		logBasePath:   logPath, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // LogType represents the type of logs to retrieve | ||||
| type LogType string | ||||
|  | ||||
| const ( | ||||
| 	LogTypeSystem   LogType = "system" | ||||
| 	LogTypeService  LogType = "service" | ||||
| 	LogTypeJob      LogType = "job" | ||||
| 	LogTypeProcess  LogType = "process" | ||||
| 	LogTypeAll      LogType = "all"     // Special type to retrieve logs from all sources | ||||
| ) | ||||
|  | ||||
| // GetLogs renders the logs page with logs content | ||||
| func (h *LogHandler) GetLogs(c *fiber.Ctx) error { | ||||
| 	// Check which logger to use based on the log type parameter | ||||
| 	logTypeParam := c.Query("log_type", string(LogTypeSystem)) | ||||
| 	 | ||||
| 	// Parse query parameters | ||||
| 	category := c.Query("category", "") | ||||
| 	logItemType := parseLogType(c.Query("type", "")) | ||||
| 	maxItems := c.QueryInt("max_items", 100) | ||||
| 	page := c.QueryInt("page", 1) | ||||
| 	itemsPerPage := 20 // Default items per page | ||||
| 	 | ||||
| 	// Parse time range | ||||
| 	fromTime := parseTimeParam(c.Query("from", "")) | ||||
| 	toTime := parseTimeParam(c.Query("to", "")) | ||||
| 	 | ||||
| 	// Create search arguments | ||||
| 	searchArgs := logger.SearchArgs{ | ||||
| 		Category:      category, | ||||
| 		LogType:       logItemType, | ||||
| 		MaxItems:      maxItems, | ||||
| 	} | ||||
| 	 | ||||
| 	if !fromTime.IsZero() { | ||||
| 		searchArgs.TimestampFrom = &fromTime | ||||
| 	} | ||||
| 	 | ||||
| 	if !toTime.IsZero() { | ||||
| 		searchArgs.TimestampTo = &toTime | ||||
| 	} | ||||
|  | ||||
| 	// Variables for logs and error | ||||
| 	var logs []logger.LogItem | ||||
| 	var err error | ||||
| 	var logTypeTitle string | ||||
| 	 | ||||
| 	// Check if we want to merge logs from all sources | ||||
| 	if LogType(logTypeParam) == LogTypeAll { | ||||
| 		// Get merged logs from all loggers | ||||
| 		logs, err = h.getMergedLogs(searchArgs) | ||||
| 		logTypeTitle = "All Logs" | ||||
| 	} else { | ||||
| 		// Select the appropriate logger based on the log type | ||||
| 		var selectedLogger *logger.Logger | ||||
| 		 | ||||
| 		switch LogType(logTypeParam) { | ||||
| 		case LogTypeService: | ||||
| 			selectedLogger = h.serviceLogger | ||||
| 			logTypeTitle = "Service Logs" | ||||
| 		case LogTypeJob: | ||||
| 			selectedLogger = h.jobLogger | ||||
| 			logTypeTitle = "Job Logs" | ||||
| 		case LogTypeProcess: | ||||
| 			selectedLogger = h.processLogger | ||||
| 			logTypeTitle = "Process Logs" | ||||
| 		default: | ||||
| 			selectedLogger = h.systemLogger | ||||
| 			logTypeTitle = "System Logs" | ||||
| 		} | ||||
| 		 | ||||
| 		// Check if the selected logger is properly initialized | ||||
| 		if selectedLogger == nil { | ||||
| 			return c.Render("admin/system/logs", fiber.Map{ | ||||
| 				"title": logTypeTitle, | ||||
| 				"error": "Logger not initialized", | ||||
| 				"logTypes": []LogType{LogTypeAll, LogTypeSystem, LogTypeService, LogTypeJob, LogTypeProcess}, | ||||
| 				"selectedLogType": logTypeParam, | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		// Search for logs using the selected logger | ||||
| 		logs, err = selectedLogger.Search(searchArgs) | ||||
| 	} | ||||
|  | ||||
| 	// Handle search error | ||||
| 	if err != nil { | ||||
| 		return c.Render("admin/system/logs", fiber.Map{ | ||||
| 			"title": logTypeTitle, | ||||
| 			"error": err.Error(), | ||||
| 			"logTypes": []LogType{LogTypeAll, LogTypeSystem, LogTypeService, LogTypeJob, LogTypeProcess}, | ||||
| 			"selectedLogType": logTypeParam, | ||||
| 		}) | ||||
| 	} | ||||
| 	 | ||||
| 	 | ||||
| 	// Calculate total pages | ||||
| 	totalLogs := len(logs) | ||||
| 	totalPages := (totalLogs + itemsPerPage - 1) / itemsPerPage | ||||
| 	 | ||||
| 	// Apply pagination | ||||
| 	startIndex := (page - 1) * itemsPerPage | ||||
| 	endIndex := startIndex + itemsPerPage | ||||
| 	if endIndex > totalLogs { | ||||
| 		endIndex = totalLogs | ||||
| 	} | ||||
| 	 | ||||
| 	// Slice logs for current page | ||||
| 	pagedLogs := logs | ||||
| 	if startIndex < totalLogs { | ||||
| 		pagedLogs = logs[startIndex:endIndex] | ||||
| 	} else { | ||||
| 		pagedLogs = []logger.LogItem{} | ||||
| 	} | ||||
| 	 | ||||
| 	// Convert logs to a format suitable for the UI | ||||
| 	formattedLogs := make([]fiber.Map, 0, len(pagedLogs)) | ||||
| 	for _, log := range pagedLogs { | ||||
| 		logTypeStr := "INFO" | ||||
| 		logTypeClass := "log-info" | ||||
| 		if log.LogType == logger.LogTypeError { | ||||
| 			logTypeStr = "ERROR" | ||||
| 			logTypeClass = "log-error" | ||||
| 		} | ||||
| 		 | ||||
| 		formattedLogs = append(formattedLogs, fiber.Map{ | ||||
| 			"timestamp": log.Timestamp.Format("2006-01-02T15:04:05"), | ||||
| 			"category":  log.Category, | ||||
| 			"message":   log.Message, | ||||
| 			"type":      logTypeStr, | ||||
| 			"typeClass": logTypeClass, | ||||
| 		}) | ||||
| 	} | ||||
| 	 | ||||
| 	return c.Render("admin/system/logs", fiber.Map{ | ||||
| 		"title": logTypeTitle, | ||||
| 		"logTypes": []LogType{LogTypeAll, LogTypeSystem, LogTypeService, LogTypeJob, LogTypeProcess}, | ||||
| 		"selectedLogType": logTypeParam, | ||||
| 		"logs": formattedLogs, | ||||
| 		"total": totalLogs, | ||||
| 		"showing": len(formattedLogs), | ||||
| 		"page": page, | ||||
| 		"totalPages": totalPages, | ||||
| 		"categoryParam": category, | ||||
| 		"typeParam": c.Query("type", ""), | ||||
| 		"fromParam": c.Query("from", ""), | ||||
| 		"toParam": c.Query("to", ""), | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetLogsAPI returns logs in JSON format for API consumption | ||||
| func (h *LogHandler) GetLogsAPI(c *fiber.Ctx) error { | ||||
| 	// Check which logger to use based on the log type parameter | ||||
| 	logTypeParam := c.Query("log_type", string(LogTypeSystem)) | ||||
| 	 | ||||
| 	// Parse query parameters | ||||
| 	category := c.Query("category", "") | ||||
| 	logItemType := parseLogType(c.Query("type", "")) | ||||
| 	maxItems := c.QueryInt("max_items", 100) | ||||
| 	 | ||||
| 	// Parse time range | ||||
| 	fromTime := parseTimeParam(c.Query("from", "")) | ||||
| 	toTime := parseTimeParam(c.Query("to", "")) | ||||
| 	 | ||||
| 	// Create search arguments | ||||
| 	searchArgs := logger.SearchArgs{ | ||||
| 		Category:      category, | ||||
| 		LogType:       logItemType, | ||||
| 		MaxItems:      maxItems, | ||||
| 	} | ||||
| 	 | ||||
| 	if !fromTime.IsZero() { | ||||
| 		searchArgs.TimestampFrom = &fromTime | ||||
| 	} | ||||
| 	 | ||||
| 	if !toTime.IsZero() { | ||||
| 		searchArgs.TimestampTo = &toTime | ||||
| 	} | ||||
|  | ||||
| 	// Variables for logs and error | ||||
| 	var logs []logger.LogItem | ||||
| 	var err error | ||||
| 	 | ||||
| 	// Check if we want to merge logs from all sources | ||||
| 	if LogType(logTypeParam) == LogTypeAll { | ||||
| 		// Get merged logs from all loggers | ||||
| 		logs, err = h.getMergedLogs(searchArgs) | ||||
| 	} else { | ||||
| 		// Select the appropriate logger based on the log type | ||||
| 		var selectedLogger *logger.Logger | ||||
| 		 | ||||
| 		switch LogType(logTypeParam) { | ||||
| 		case LogTypeService: | ||||
| 			selectedLogger = h.serviceLogger | ||||
| 		case LogTypeJob: | ||||
| 			selectedLogger = h.jobLogger | ||||
| 		case LogTypeProcess: | ||||
| 			selectedLogger = h.processLogger | ||||
| 		default: | ||||
| 			selectedLogger = h.systemLogger | ||||
| 		} | ||||
| 		 | ||||
| 		// Check if the selected logger is properly initialized | ||||
| 		if selectedLogger == nil { | ||||
| 			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 				"error": "Logger not initialized", | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		// Search for logs using the selected logger | ||||
| 		logs, err = selectedLogger.Search(searchArgs) | ||||
| 	} | ||||
|  | ||||
| 	// Handle search error | ||||
| 	if err != nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": err.Error(), | ||||
| 		}) | ||||
| 	} | ||||
| 	 | ||||
| 	// Convert logs to a format suitable for the UI | ||||
| 	response := make([]fiber.Map, 0, len(logs)) | ||||
| 	for _, log := range logs { | ||||
| 		logTypeStr := "INFO" | ||||
| 		if log.LogType == logger.LogTypeError { | ||||
| 			logTypeStr = "ERROR" | ||||
| 		} | ||||
| 		 | ||||
| 		response = append(response, fiber.Map{ | ||||
| 			"timestamp": log.Timestamp.Format(time.RFC3339), | ||||
| 			"category":  log.Category, | ||||
| 			"message":   log.Message, | ||||
| 			"type":      logTypeStr, | ||||
| 		}) | ||||
| 	} | ||||
| 	 | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"logs": response, | ||||
| 		"total": len(logs), | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetLogsFragment returns logs in HTML format for Unpoly partial updates | ||||
| func (h *LogHandler) GetLogsFragment(c *fiber.Ctx) error { | ||||
| 	// This is a fragment template for Unpoly updates | ||||
|  | ||||
| 	// Check which logger to use based on the log type parameter | ||||
| 	logTypeParam := c.Query("log_type", string(LogTypeSystem)) | ||||
| 	 | ||||
| 	// Parse query parameters | ||||
| 	category := c.Query("category", "") | ||||
| 	logItemType := parseLogType(c.Query("type", "")) | ||||
| 	maxItems := c.QueryInt("max_items", 100) | ||||
| 	page := c.QueryInt("page", 1) | ||||
| 	itemsPerPage := 20 // Default items per page | ||||
| 	 | ||||
| 	// Parse time range | ||||
| 	fromTime := parseTimeParam(c.Query("from", "")) | ||||
| 	toTime := parseTimeParam(c.Query("to", "")) | ||||
| 	 | ||||
| 	// Create search arguments | ||||
| 	searchArgs := logger.SearchArgs{ | ||||
| 		Category:      category, | ||||
| 		LogType:       logItemType, | ||||
| 		MaxItems:      maxItems, | ||||
| 	} | ||||
| 	 | ||||
| 	if !fromTime.IsZero() { | ||||
| 		searchArgs.TimestampFrom = &fromTime | ||||
| 	} | ||||
| 	 | ||||
| 	if !toTime.IsZero() { | ||||
| 		searchArgs.TimestampTo = &toTime | ||||
| 	} | ||||
|  | ||||
| 	// Variables for logs and error | ||||
| 	var logs []logger.LogItem | ||||
| 	var err error | ||||
| 	var logTypeTitle string | ||||
| 	 | ||||
| 	// Check if we want to merge logs from all sources | ||||
| 	if LogType(logTypeParam) == LogTypeAll { | ||||
| 		// Get merged logs from all loggers | ||||
| 		logs, err = h.getMergedLogs(searchArgs) | ||||
| 		logTypeTitle = "All Logs" | ||||
| 	} else { | ||||
| 		// Select the appropriate logger based on the log type | ||||
| 		var selectedLogger *logger.Logger | ||||
| 		 | ||||
| 		switch LogType(logTypeParam) { | ||||
| 		case LogTypeService: | ||||
| 			selectedLogger = h.serviceLogger | ||||
| 			logTypeTitle = "Service Logs" | ||||
| 		case LogTypeJob: | ||||
| 			selectedLogger = h.jobLogger | ||||
| 			logTypeTitle = "Job Logs" | ||||
| 		case LogTypeProcess: | ||||
| 			selectedLogger = h.processLogger | ||||
| 			logTypeTitle = "Process Logs" | ||||
| 		default: | ||||
| 			selectedLogger = h.systemLogger | ||||
| 			logTypeTitle = "System Logs" | ||||
| 		} | ||||
| 		 | ||||
| 		// Check if the selected logger is properly initialized | ||||
| 		if selectedLogger == nil { | ||||
| 			return c.Render("admin/system/logs_fragment", fiber.Map{ | ||||
| 				"title": logTypeTitle, | ||||
| 				"error": "Logger not initialized", | ||||
| 				"logTypes": []LogType{LogTypeAll, LogTypeSystem, LogTypeService, LogTypeJob, LogTypeProcess}, | ||||
| 				"selectedLogType": logTypeParam, | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		// Search for logs using the selected logger | ||||
| 		logs, err = selectedLogger.Search(searchArgs) | ||||
| 	} | ||||
|  | ||||
| 	// Handle search error | ||||
| 	if err != nil { | ||||
| 		return c.Render("admin/system/logs_fragment", fiber.Map{ | ||||
| 			"title": logTypeTitle, | ||||
| 			"error": err.Error(), | ||||
| 			"logTypes": []LogType{LogTypeAll, LogTypeSystem, LogTypeService, LogTypeJob, LogTypeProcess}, | ||||
| 			"selectedLogType": logTypeParam, | ||||
| 		}) | ||||
| 	} | ||||
| 	 | ||||
| 	// Calculate total pages | ||||
| 	totalLogs := len(logs) | ||||
| 	totalPages := (totalLogs + itemsPerPage - 1) / itemsPerPage | ||||
| 	 | ||||
| 	// Apply pagination | ||||
| 	startIndex := (page - 1) * itemsPerPage | ||||
| 	endIndex := startIndex + itemsPerPage | ||||
| 	if endIndex > totalLogs { | ||||
| 		endIndex = totalLogs | ||||
| 	} | ||||
| 	 | ||||
| 	// Slice logs for current page | ||||
| 	pagedLogs := logs | ||||
| 	if startIndex < totalLogs { | ||||
| 		pagedLogs = logs[startIndex:endIndex] | ||||
| 	} else { | ||||
| 		pagedLogs = []logger.LogItem{} | ||||
| 	} | ||||
| 	 | ||||
| 	// Convert logs to a format suitable for the UI | ||||
| 	formattedLogs := make([]fiber.Map, 0, len(pagedLogs)) | ||||
| 	for _, log := range pagedLogs { | ||||
| 		logTypeStr := "INFO" | ||||
| 		logTypeClass := "log-info" | ||||
| 		if log.LogType == logger.LogTypeError { | ||||
| 			logTypeStr = "ERROR" | ||||
| 			logTypeClass = "log-error" | ||||
| 		} | ||||
| 		 | ||||
| 		formattedLogs = append(formattedLogs, fiber.Map{ | ||||
| 			"timestamp": log.Timestamp.Format("2006-01-02T15:04:05"), | ||||
| 			"category":  log.Category, | ||||
| 			"message":   log.Message, | ||||
| 			"type":      logTypeStr, | ||||
| 			"typeClass": logTypeClass, | ||||
| 		}) | ||||
| 	} | ||||
| 	 | ||||
| 	// Set layout to empty to disable the layout for fragment responses | ||||
| 	return c.Render("admin/system/logs_fragment", fiber.Map{ | ||||
| 		"title": logTypeTitle, | ||||
| 		"logTypes": []LogType{LogTypeAll, LogTypeSystem, LogTypeService, LogTypeJob, LogTypeProcess}, | ||||
| 		"selectedLogType": logTypeParam, | ||||
| 		"logs": formattedLogs, | ||||
| 		"total": totalLogs, | ||||
| 		"showing": len(formattedLogs), | ||||
| 		"page": page, | ||||
| 		"totalPages": totalPages, | ||||
| 		"layout": "", // Disable layout for partial template | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // Helper functions | ||||
|  | ||||
| // parseLogType converts a string log type to the appropriate LogType enum | ||||
| func parseLogType(logTypeStr string) logger.LogType { | ||||
| 	switch logTypeStr { | ||||
| 	case "error": | ||||
| 		return logger.LogTypeError | ||||
| 	default: | ||||
| 		return logger.LogTypeStdout | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // parseTimeParam parses a time string in ISO format | ||||
| func parseTimeParam(timeStr string) time.Time { | ||||
| 	if timeStr == "" { | ||||
| 		return time.Time{} | ||||
| 	} | ||||
| 	 | ||||
| 	t, err := time.Parse(time.RFC3339, timeStr) | ||||
| 	if err != nil { | ||||
| 		return time.Time{} | ||||
| 	} | ||||
| 	 | ||||
| 	return t | ||||
| } | ||||
|  | ||||
| // getMergedLogs retrieves and merges logs from all available loggers | ||||
| func (h *LogHandler) getMergedLogs(args logger.SearchArgs) ([]logger.LogItem, error) { | ||||
| 	// Create a slice to hold all logs | ||||
| 	allLogs := make([]logger.LogItem, 0) | ||||
| 	 | ||||
| 	// Create a map to track errors | ||||
| 	errors := make(map[string]error) | ||||
| 	 | ||||
| 	// Get logs from system logger if available | ||||
| 	if h.systemLogger != nil { | ||||
| 		systemLogs, err := h.systemLogger.Search(args) | ||||
| 		if err != nil { | ||||
| 			errors["system"] = err | ||||
| 		} else { | ||||
| 			// Add source information to each log item | ||||
| 			for i := range systemLogs { | ||||
| 				systemLogs[i].Category = fmt.Sprintf("system:%s", systemLogs[i].Category) | ||||
| 			} | ||||
| 			allLogs = append(allLogs, systemLogs...) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	// Get logs from service logger if available | ||||
| 	if h.serviceLogger != nil { | ||||
| 		serviceLogs, err := h.serviceLogger.Search(args) | ||||
| 		if err != nil { | ||||
| 			errors["service"] = err | ||||
| 		} else { | ||||
| 			// Add source information to each log item | ||||
| 			for i := range serviceLogs { | ||||
| 				serviceLogs[i].Category = fmt.Sprintf("service:%s", serviceLogs[i].Category) | ||||
| 			} | ||||
| 			allLogs = append(allLogs, serviceLogs...) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	// Get logs from job logger if available | ||||
| 	if h.jobLogger != nil { | ||||
| 		jobLogs, err := h.jobLogger.Search(args) | ||||
| 		if err != nil { | ||||
| 			errors["job"] = err | ||||
| 		} else { | ||||
| 			// Add source information to each log item | ||||
| 			for i := range jobLogs { | ||||
| 				jobLogs[i].Category = fmt.Sprintf("job:%s", jobLogs[i].Category) | ||||
| 			} | ||||
| 			allLogs = append(allLogs, jobLogs...) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	// Get logs from process logger if available | ||||
| 	if h.processLogger != nil { | ||||
| 		processLogs, err := h.processLogger.Search(args) | ||||
| 		if err != nil { | ||||
| 			errors["process"] = err | ||||
| 		} else { | ||||
| 			// Add source information to each log item | ||||
| 			for i := range processLogs { | ||||
| 				processLogs[i].Category = fmt.Sprintf("process:%s", processLogs[i].Category) | ||||
| 			} | ||||
| 			allLogs = append(allLogs, processLogs...) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	// Check if we have any logs | ||||
| 	if len(allLogs) == 0 && len(errors) > 0 { | ||||
| 		// Combine error messages | ||||
| 		errorMsgs := make([]string, 0, len(errors)) | ||||
| 		for source, err := range errors { | ||||
| 			errorMsgs = append(errorMsgs, fmt.Sprintf("%s: %s", source, err.Error())) | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("failed to retrieve logs: %s", strings.Join(errorMsgs, "; ")) | ||||
| 	} | ||||
| 	 | ||||
| 	// Sort logs by timestamp (newest first) | ||||
| 	sort.Slice(allLogs, func(i, j int) bool { | ||||
| 		return allLogs[i].Timestamp.After(allLogs[j].Timestamp) | ||||
| 	}) | ||||
| 	 | ||||
| 	// Apply max items limit if specified | ||||
| 	if args.MaxItems > 0 && len(allLogs) > args.MaxItems { | ||||
| 		allLogs = allLogs[:args.MaxItems] | ||||
| 	} | ||||
| 	 | ||||
| 	return allLogs, nil | ||||
| } | ||||
							
								
								
									
										205
									
								
								pkg/heroagent/handlers/process_handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								pkg/heroagent/handlers/process_handlers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/system/stats" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| ) | ||||
|  | ||||
| // ProcessHandler handles process-related routes | ||||
| type ProcessHandler struct { | ||||
| 	statsManager *stats.StatsManager | ||||
| } | ||||
|  | ||||
| // NewProcessHandler creates a new ProcessHandler | ||||
| func NewProcessHandler(statsManager *stats.StatsManager) *ProcessHandler { | ||||
| 	return &ProcessHandler{ | ||||
| 		statsManager: statsManager, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetProcessStatsJSON returns process stats in JSON format for API consumption | ||||
| func (h *ProcessHandler) GetProcessStatsJSON(c *fiber.Ctx) error { | ||||
| 	// Check if StatsManager is properly initialized | ||||
| 	if h.statsManager == nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": "System error: Stats manager not initialized", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Get process data from the StatsManager | ||||
| 	processData, err := h.statsManager.GetProcessStatsFresh(100) // Limit to 100 processes | ||||
| 	if err != nil { | ||||
| 		// Try getting cached data as fallback | ||||
| 		processData, err = h.statsManager.GetProcessStats(100) | ||||
| 		if err != nil { | ||||
| 			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 				"error": "Failed to get process data: " + err.Error(), | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Convert to fiber.Map for JSON response | ||||
| 	response := fiber.Map{ | ||||
| 		"total": processData.Total, | ||||
| 		"filtered": processData.Filtered, | ||||
| 		"timestamp": time.Now().Unix(), | ||||
| 	} | ||||
|  | ||||
| 	// Convert processes to a slice of maps | ||||
| 	processes := make([]fiber.Map, len(processData.Processes)) | ||||
| 	for i, proc := range processData.Processes { | ||||
| 		processes[i] = fiber.Map{ | ||||
| 			"pid":             proc.PID, | ||||
| 			"name":            proc.Name, | ||||
| 			"status":          proc.Status, | ||||
| 			"cpu_percent":     proc.CPUPercent, | ||||
| 			"memory_mb":       proc.MemoryMB, | ||||
| 			"create_time_str": proc.CreateTime, | ||||
| 			"is_current":      proc.IsCurrent, | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	response["processes"] = processes | ||||
|  | ||||
| 	// Return JSON response | ||||
| 	return c.JSON(response) | ||||
| } | ||||
|  | ||||
| // GetProcesses renders the processes page with initial process data | ||||
| func (h *ProcessHandler) GetProcesses(c *fiber.Ctx) error { | ||||
| 	// Check if StatsManager is properly initialized | ||||
| 	if h.statsManager == nil { | ||||
| 		return c.Render("admin/system/processes", fiber.Map{ | ||||
| 			"processes": []fiber.Map{}, | ||||
| 			"error":     "System error: Stats manager not initialized", | ||||
| 			"warning":   "The process manager is not properly initialized.", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Force cache refresh for process stats | ||||
| 	h.statsManager.ForceUpdate("process") | ||||
|  | ||||
| 	// Get process data from the StatsManager | ||||
| 	processData, err := h.statsManager.GetProcessStatsFresh(0) // Get all processes with fresh data | ||||
| 	if err != nil { | ||||
| 		// Try getting cached data as fallback | ||||
| 		processData, err = h.statsManager.GetProcessStats(0) | ||||
| 		if err != nil { | ||||
| 			// If there's an error, still render the page but with empty data | ||||
| 			return c.Render("admin/system/processes", fiber.Map{ | ||||
| 				"processes": []fiber.Map{}, | ||||
| 				"error":     "Failed to load process data: " + err.Error(), | ||||
| 				"warning":   "System attempted both fresh and cached data retrieval but failed.", | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Convert to []fiber.Map for template rendering | ||||
| 	processStats := make([]fiber.Map, len(processData.Processes)) | ||||
| 	for i, proc := range processData.Processes { | ||||
| 		processStats[i] = fiber.Map{ | ||||
| 			"pid":             proc.PID, | ||||
| 			"name":            proc.Name, | ||||
| 			"status":          proc.Status, | ||||
| 			"cpu_percent":     proc.CPUPercent, | ||||
| 			"memory_mb":       proc.MemoryMB, | ||||
| 			"create_time_str": proc.CreateTime, | ||||
| 			"is_current":      proc.IsCurrent, | ||||
| 			"cpu_percent_str": fmt.Sprintf("%.1f%%", proc.CPUPercent), | ||||
| 			"memory_mb_str":   fmt.Sprintf("%.1f MB", proc.MemoryMB), | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Render the full page with initial process data | ||||
| 	return c.Render("admin/system/processes", fiber.Map{ | ||||
| 		"processes": processStats, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetProcessesData returns the HTML fragment for processes data | ||||
| func (h *ProcessHandler) GetProcessesData(c *fiber.Ctx) error { | ||||
| 	// Check if this is a manual refresh request (with X-Requested-With header set) | ||||
| 	isManualRefresh := c.Get("X-Requested-With") == "XMLHttpRequest" | ||||
|  | ||||
| 	// Check if StatsManager is properly initialized | ||||
| 	if h.statsManager == nil { | ||||
| 		return c.Render("admin/system/processes_data", fiber.Map{ | ||||
| 			"error":   "System error: Stats manager not initialized", | ||||
| 			"layout":  "", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// For manual refresh, always get fresh data by forcing cache invalidation | ||||
| 	var processData *stats.ProcessStats | ||||
| 	var err error | ||||
|  | ||||
| 	// Force cache refresh for process stats on manual refresh | ||||
| 	if isManualRefresh { | ||||
| 		h.statsManager.ForceUpdate("process") | ||||
| 	} | ||||
|  | ||||
| 	if isManualRefresh { | ||||
| 		// Force bypass cache for manual refresh by using fresh data | ||||
| 		processData, err = h.statsManager.GetProcessStatsFresh(0) | ||||
| 	} else { | ||||
| 		// Use cached data for auto-polling | ||||
| 		processData, err = h.statsManager.GetProcessStats(0) | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		// Try alternative method if the primary method fails | ||||
| 		if isManualRefresh { | ||||
| 			processData, err = h.statsManager.GetProcessStats(0) | ||||
| 		} else { | ||||
| 			processData, err = h.statsManager.GetProcessStatsFresh(0) | ||||
| 		} | ||||
| 		 | ||||
| 		if err != nil { | ||||
| 			// Handle AJAX requests differently from regular requests | ||||
| 			isAjax := c.Get("X-Requested-With") == "XMLHttpRequest" | ||||
| 			if isAjax { | ||||
| 				return c.Status(fiber.StatusInternalServerError).SendString("Failed to get process data: " + err.Error()) | ||||
| 			} | ||||
| 			// For regular requests, render the error within the fragment | ||||
| 			return c.Render("admin/system/processes_data", fiber.Map{ | ||||
| 				"error":   "Failed to get process data: " + err.Error(), | ||||
| 				"layout":  "", | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Convert to []fiber.Map for template rendering | ||||
| 	processStats := make([]fiber.Map, len(processData.Processes)) | ||||
| 	for i, proc := range processData.Processes { | ||||
| 		processStats[i] = fiber.Map{ | ||||
| 			"pid":             proc.PID, | ||||
| 			"name":            proc.Name, | ||||
| 			"status":          proc.Status, | ||||
| 			"cpu_percent":     proc.CPUPercent, | ||||
| 			"memory_mb":       proc.MemoryMB, | ||||
| 			"create_time_str": proc.CreateTime, | ||||
| 			"is_current":      proc.IsCurrent, | ||||
| 			"cpu_percent_str": fmt.Sprintf("%.1f%%", proc.CPUPercent), | ||||
| 			"memory_mb_str":   fmt.Sprintf("%.1f MB", proc.MemoryMB), | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Create a boolean to indicate if we have processes | ||||
| 	hasProcesses := len(processStats) > 0 | ||||
|  | ||||
| 	// Create template data with fiber.Map | ||||
| 	templateData := fiber.Map{ | ||||
| 		"hasProcesses": hasProcesses, | ||||
| 		"processCount": len(processStats), | ||||
| 		"processStats": processStats, | ||||
| 		"layout":       "", // Disable layout for partial template | ||||
| 	} | ||||
| 	 | ||||
| 	// Return only the table HTML content directly to be injected into the processes-table-content div | ||||
| 	return c.Render("admin/system/processes_data", templateData) | ||||
| } | ||||
|  | ||||
|  | ||||
							
								
								
									
										266
									
								
								pkg/heroagent/handlers/service_handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								pkg/heroagent/handlers/service_handlers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,266 @@ | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/processmanager/interfaces" | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/processmanager/interfaces/openrpc" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| ) | ||||
|  | ||||
| // ServiceHandler handles service-related routes | ||||
| type ServiceHandler struct { | ||||
| 	client *openrpc.Client | ||||
| } | ||||
|  | ||||
| // NewServiceHandler creates a new ServiceHandler | ||||
| func NewServiceHandler(socketPath, secret string) *ServiceHandler { | ||||
| 	fmt.Printf("DEBUG: Creating new ServiceHandler with socket path: %s and secret: %s\n", socketPath, secret) | ||||
| 	return &ServiceHandler{ | ||||
| 		client: openrpc.NewClient(socketPath, secret), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetServices renders the services page | ||||
| func (h *ServiceHandler) GetServices(c *fiber.Ctx) error { | ||||
| 	return c.Render("admin/services", fiber.Map{ | ||||
| 		"title":   "Services", | ||||
| 		"error":   c.Query("error", ""), | ||||
| 		"warning": c.Query("warning", ""), | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetServicesFragment returns the services table fragment for Unpoly updates | ||||
| func (h *ServiceHandler) GetServicesFragment(c *fiber.Ctx) error { | ||||
| 	processes, err := h.getProcessList() | ||||
| 	if err != nil { | ||||
| 		return c.Render("admin/services_fragment", fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to fetch services: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.Render("admin/services_fragment", fiber.Map{ | ||||
| 		"processes": processes, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // StartService handles the request to start a new service | ||||
| func (h *ServiceHandler) StartService(c *fiber.Ctx) error { | ||||
| 	name := c.FormValue("name") | ||||
| 	command := c.FormValue("command") | ||||
|  | ||||
| 	if name == "" || command == "" { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": "Service name and command are required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Default to enabling logs | ||||
| 	logEnabled := true | ||||
|  | ||||
| 	// Start the process with no deadline, no cron, and no job ID | ||||
| 	fmt.Printf("DEBUG: StartService called for '%s' using client: %p\n", name, h.client) | ||||
| 	result, err := h.client.StartProcess(name, command, logEnabled, 0, "", "") | ||||
| 	if err != nil { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to start service: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if !result.Success { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": result.Message, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"success": true, | ||||
| 		"message": result.Message, | ||||
| 		"pid":     result.PID, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // StopService handles the request to stop a service | ||||
| func (h *ServiceHandler) StopService(c *fiber.Ctx) error { | ||||
| 	name := c.FormValue("name") | ||||
|  | ||||
| 	if name == "" { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": "Service name is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	result, err := h.client.StopProcess(name) | ||||
| 	if err != nil { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to stop service: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if !result.Success { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": result.Message, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"success": true, | ||||
| 		"message": result.Message, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // RestartService handles the request to restart a service | ||||
| func (h *ServiceHandler) RestartService(c *fiber.Ctx) error { | ||||
| 	name := c.FormValue("name") | ||||
|  | ||||
| 	if name == "" { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": "Service name is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	result, err := h.client.RestartProcess(name) | ||||
| 	if err != nil { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to restart service: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if !result.Success { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": result.Message, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"success": true, | ||||
| 		"message": result.Message, | ||||
| 		"pid":     result.PID, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // DeleteService handles the request to delete a service | ||||
| func (h *ServiceHandler) DeleteService(c *fiber.Ctx) error { | ||||
| 	name := c.FormValue("name") | ||||
|  | ||||
| 	if name == "" { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": "Service name is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	result, err := h.client.DeleteProcess(name) | ||||
| 	if err != nil { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to delete service: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if !result.Success { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": result.Message, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"success": true, | ||||
| 		"message": result.Message, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetServiceLogs handles the request to get logs for a service | ||||
| func (h *ServiceHandler) GetServiceLogs(c *fiber.Ctx) error { | ||||
| 	name := c.Query("name") | ||||
| 	lines := c.QueryInt("lines", 100) | ||||
|  | ||||
| 	fmt.Printf("DEBUG: GetServiceLogs called for service '%s' using client: %p\n", name, h.client) | ||||
|  | ||||
| 	if name == "" { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": "Service name is required", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Debug: List all processes before getting logs | ||||
| 	processes, listErr := h.getProcessList() | ||||
| 	if listErr == nil { | ||||
| 		fmt.Println("DEBUG: Current processes in service handler:") | ||||
| 		for _, proc := range processes { | ||||
| 			fmt.Printf("DEBUG: - '%v' (PID: %v, Status: %v)\n", proc["Name"], proc["ID"], proc["Status"]) | ||||
| 		} | ||||
| 	} else { | ||||
| 		fmt.Printf("DEBUG: Error listing processes: %v\n", listErr) | ||||
| 	} | ||||
|  | ||||
| 	result, err := h.client.GetProcessLogs(name, lines) | ||||
| 	if err != nil { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": fmt.Sprintf("Failed to get service logs: %v", err), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if !result.Success { | ||||
| 		return c.JSON(fiber.Map{ | ||||
| 			"error": result.Message, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"success": true, | ||||
| 		"logs":    result.Logs, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // Helper function to get the list of processes and format them for the UI | ||||
| func (h *ServiceHandler) getProcessList() ([]fiber.Map, error) { | ||||
| 	// Get the list of processes | ||||
| 	result, err := h.client.ListProcesses("json") | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to list processes: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Convert the result to a slice of ProcessStatus | ||||
| 	processList, ok := result.([]interfaces.ProcessStatus) | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("unexpected result type from ListProcesses") | ||||
| 	} | ||||
|  | ||||
| 	// Format the processes for the UI | ||||
| 	formattedProcesses := make([]fiber.Map, 0, len(processList)) | ||||
| 	for _, proc := range processList { | ||||
| 		// Calculate uptime | ||||
| 		uptime := "N/A" | ||||
| 		if proc.Status == "running" { | ||||
| 			duration := time.Since(proc.StartTime) | ||||
| 			if duration.Hours() >= 24 { | ||||
| 				days := int(duration.Hours() / 24) | ||||
| 				hours := int(duration.Hours()) % 24 | ||||
| 				uptime = fmt.Sprintf("%dd %dh", days, hours) | ||||
| 			} else if duration.Hours() >= 1 { | ||||
| 				hours := int(duration.Hours()) | ||||
| 				minutes := int(duration.Minutes()) % 60 | ||||
| 				uptime = fmt.Sprintf("%dh %dm", hours, minutes) | ||||
| 			} else { | ||||
| 				minutes := int(duration.Minutes()) | ||||
| 				seconds := int(duration.Seconds()) % 60 | ||||
| 				uptime = fmt.Sprintf("%dm %ds", minutes, seconds) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Format CPU and memory usage | ||||
| 		cpuUsage := fmt.Sprintf("%.1f%%", proc.CPUPercent) | ||||
| 		memoryUsage := fmt.Sprintf("%.1f MB", proc.MemoryMB) | ||||
|  | ||||
| 		formattedProcesses = append(formattedProcesses, fiber.Map{ | ||||
| 			"Name":   proc.Name, | ||||
| 			"Status": string(proc.Status), | ||||
| 			"ID":     proc.PID, | ||||
| 			"CPU":    cpuUsage, | ||||
| 			"Memory": memoryUsage, | ||||
| 			"Uptime": uptime, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return formattedProcesses, nil | ||||
| } | ||||
							
								
								
									
										375
									
								
								pkg/heroagent/handlers/system_handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								pkg/heroagent/handlers/system_handlers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,375 @@ | ||||
| package handlers | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/system/stats" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/shirou/gopsutil/v3/host" | ||||
| ) | ||||
|  | ||||
| // UptimeProvider defines an interface for getting system uptime | ||||
| type UptimeProvider interface { | ||||
| 	GetUptime() string | ||||
| } | ||||
|  | ||||
| // SystemHandler handles system-related page routes | ||||
| type SystemHandler struct { | ||||
| 	uptimeProvider UptimeProvider | ||||
| 	statsManager   *stats.StatsManager | ||||
| } | ||||
|  | ||||
| // NewSystemHandler creates a new SystemHandler | ||||
| func NewSystemHandler(uptimeProvider UptimeProvider, statsManager *stats.StatsManager) *SystemHandler { | ||||
| 	// If statsManager is nil, create a new one with default settings | ||||
| 	if statsManager == nil { | ||||
| 		var err error | ||||
| 		statsManager, err = stats.NewStatsManagerWithDefaults() | ||||
| 		if err != nil { | ||||
| 			// Log the error but continue with nil statsManager | ||||
| 			fmt.Printf("Error creating StatsManager: %v\n", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &SystemHandler{ | ||||
| 		uptimeProvider: uptimeProvider, | ||||
| 		statsManager:   statsManager, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetSystemInfo renders the system info page | ||||
| func (h *SystemHandler) GetSystemInfo(c *fiber.Ctx) error { | ||||
| 	// Initialize default values | ||||
| 	cpuInfo := "Unknown" | ||||
| 	memoryInfo := "Unknown" | ||||
| 	diskInfo := "Unknown" | ||||
| 	networkInfo := "Unknown" | ||||
| 	osInfo := "Unknown" | ||||
| 	uptimeInfo := "Unknown" | ||||
|  | ||||
| 	// Get hardware stats from the StatsManager | ||||
| 	var hardwareStats map[string]interface{} | ||||
| 	if h.statsManager != nil { | ||||
| 		hardwareStats = h.statsManager.GetHardwareStats() | ||||
| 	} else { | ||||
| 		// Fallback to direct function call if StatsManager is not available | ||||
| 		hardwareStats = stats.GetHardwareStats() | ||||
| 	} | ||||
|  | ||||
| 	// Extract the formatted strings - safely handle different return types | ||||
| 	if cpuVal, ok := hardwareStats["cpu"]; ok { | ||||
| 		switch v := cpuVal.(type) { | ||||
| 		case string: | ||||
| 			cpuInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			if model, ok := v["model"].(string); ok { | ||||
| 				usage := 0.0 | ||||
| 				if usagePercent, ok := v["usage_percent"].(float64); ok { | ||||
| 					usage = usagePercent | ||||
| 				} | ||||
| 				cpuInfo = fmt.Sprintf("%s (Usage: %.1f%%)", model, usage) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if memVal, ok := hardwareStats["memory"]; ok { | ||||
| 		switch v := memVal.(type) { | ||||
| 		case string: | ||||
| 			memoryInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			total, used := 0.0, 0.0 | ||||
| 			if totalGB, ok := v["total_gb"].(float64); ok { | ||||
| 				total = totalGB | ||||
| 			} | ||||
| 			if usedGB, ok := v["used_gb"].(float64); ok { | ||||
| 				used = usedGB | ||||
| 			} | ||||
| 			usedPercent := 0.0 | ||||
| 			if percent, ok := v["used_percent"].(float64); ok { | ||||
| 				usedPercent = percent | ||||
| 			} | ||||
| 			memoryInfo = fmt.Sprintf("%.1f GB / %.1f GB (%.1f%% used)", used, total, usedPercent) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if diskVal, ok := hardwareStats["disk"]; ok { | ||||
| 		switch v := diskVal.(type) { | ||||
| 		case string: | ||||
| 			diskInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			total, used := 0.0, 0.0 | ||||
| 			if totalGB, ok := v["total_gb"].(float64); ok { | ||||
| 				total = totalGB | ||||
| 			} | ||||
| 			if usedGB, ok := v["used_gb"].(float64); ok { | ||||
| 				used = usedGB | ||||
| 			} | ||||
| 			usedPercent := 0.0 | ||||
| 			if percent, ok := v["used_percent"].(float64); ok { | ||||
| 				usedPercent = percent | ||||
| 			} | ||||
| 			diskInfo = fmt.Sprintf("%.1f GB / %.1f GB (%.1f%% used)", used, total, usedPercent) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if netVal, ok := hardwareStats["network"]; ok { | ||||
| 		switch v := netVal.(type) { | ||||
| 		case string: | ||||
| 			networkInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			var interfaces []string | ||||
| 			if ifaces, ok := v["interfaces"].([]interface{}); ok { | ||||
| 				for _, iface := range ifaces { | ||||
| 					if ifaceMap, ok := iface.(map[string]interface{}); ok { | ||||
| 						name := ifaceMap["name"].(string) | ||||
| 						ip := ifaceMap["ip"].(string) | ||||
| 						interfaces = append(interfaces, fmt.Sprintf("%s: %s", name, ip)) | ||||
| 					} | ||||
| 				} | ||||
| 				networkInfo = strings.Join(interfaces, ", ") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Get OS info | ||||
| 	hostInfo, err := host.Info() | ||||
| 	if err == nil { | ||||
| 		osInfo = fmt.Sprintf("%s %s (%s)", hostInfo.Platform, hostInfo.PlatformVersion, hostInfo.KernelVersion) | ||||
| 	} | ||||
|  | ||||
| 	// Get uptime | ||||
| 	if h.uptimeProvider != nil { | ||||
| 		uptimeInfo = h.uptimeProvider.GetUptime() | ||||
| 	} | ||||
|  | ||||
| 	// Render the template with the system info | ||||
| 	return c.Render("admin/system/info", fiber.Map{ | ||||
| 		"title":       "System Information", | ||||
| 		"cpuInfo":     cpuInfo, | ||||
| 		"memoryInfo":  memoryInfo, | ||||
| 		"diskInfo":    diskInfo, | ||||
| 		"networkInfo": networkInfo, | ||||
| 		"osInfo":      osInfo, | ||||
| 		"uptimeInfo":  uptimeInfo, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetHardwareStats returns only the hardware stats for Unpoly polling | ||||
| func (h *SystemHandler) GetHardwareStats(c *fiber.Ctx) error { | ||||
| 	// Initialize default values | ||||
| 	cpuInfo := "Unknown" | ||||
| 	memoryInfo := "Unknown" | ||||
| 	diskInfo := "Unknown" | ||||
| 	networkInfo := "Unknown" | ||||
|  | ||||
| 	// Get hardware stats from the StatsManager | ||||
| 	var hardwareStats map[string]interface{} | ||||
| 	if h.statsManager != nil { | ||||
| 		hardwareStats = h.statsManager.GetHardwareStats() | ||||
| 	} else { | ||||
| 		// Fallback to direct function call if StatsManager is not available | ||||
| 		hardwareStats = stats.GetHardwareStats() | ||||
| 	} | ||||
|  | ||||
| 	// Extract the formatted strings - safely handle different return types | ||||
| 	if cpuVal, ok := hardwareStats["cpu"]; ok { | ||||
| 		switch v := cpuVal.(type) { | ||||
| 		case string: | ||||
| 			cpuInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			if model, ok := v["model"].(string); ok { | ||||
| 				cpuInfo = model | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if memVal, ok := hardwareStats["memory"]; ok { | ||||
| 		switch v := memVal.(type) { | ||||
| 		case string: | ||||
| 			memoryInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			total, used := 0.0, 0.0 | ||||
| 			if totalGB, ok := v["total_gb"].(float64); ok { | ||||
| 				total = totalGB | ||||
| 			} | ||||
| 			if usedGB, ok := v["used_gb"].(float64); ok { | ||||
| 				used = usedGB | ||||
| 			} | ||||
| 			memoryInfo = fmt.Sprintf("%.1f GB / %.1f GB", used, total) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if diskVal, ok := hardwareStats["disk"]; ok { | ||||
| 		switch v := diskVal.(type) { | ||||
| 		case string: | ||||
| 			diskInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			total, used := 0.0, 0.0 | ||||
| 			if totalGB, ok := v["total_gb"].(float64); ok { | ||||
| 				total = totalGB | ||||
| 			} | ||||
| 			if usedGB, ok := v["used_gb"].(float64); ok { | ||||
| 				used = usedGB | ||||
| 			} | ||||
| 			diskInfo = fmt.Sprintf("%.1f GB / %.1f GB", used, total) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if netVal, ok := hardwareStats["network"]; ok { | ||||
| 		switch v := netVal.(type) { | ||||
| 		case string: | ||||
| 			networkInfo = v | ||||
| 		case map[string]interface{}: | ||||
| 			// Format the map into a string | ||||
| 			var interfaces []string | ||||
| 			if ifaces, ok := v["interfaces"].([]interface{}); ok { | ||||
| 				for _, iface := range ifaces { | ||||
| 					if ifaceMap, ok := iface.(map[string]interface{}); ok { | ||||
| 						name := ifaceMap["name"].(string) | ||||
| 						ip := ifaceMap["ip"].(string) | ||||
| 						interfaces = append(interfaces, fmt.Sprintf("%s: %s", name, ip)) | ||||
| 					} | ||||
| 				} | ||||
| 				networkInfo = strings.Join(interfaces, ", ") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Format for display | ||||
| 	cpuUsage := "0.0%" | ||||
| 	memUsage := "0.0%" | ||||
| 	diskUsage := "0.0%" | ||||
|  | ||||
| 	// Safely extract usage percentages | ||||
| 	if cpuVal, ok := hardwareStats["cpu"].(map[string]interface{}); ok { | ||||
| 		if usagePercent, ok := cpuVal["usage_percent"].(float64); ok { | ||||
| 			cpuUsage = fmt.Sprintf("%.1f%%", usagePercent) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if memVal, ok := hardwareStats["memory"].(map[string]interface{}); ok { | ||||
| 		if usedPercent, ok := memVal["used_percent"].(float64); ok { | ||||
| 			memUsage = fmt.Sprintf("%.1f%%", usedPercent) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if diskVal, ok := hardwareStats["disk"].(map[string]interface{}); ok { | ||||
| 		if usedPercent, ok := diskVal["used_percent"].(float64); ok { | ||||
| 			diskUsage = fmt.Sprintf("%.1f%%", usedPercent) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Render only the hardware stats fragment | ||||
| 	return c.Render("admin/system/hardware_stats_fragment", fiber.Map{ | ||||
| 		"cpuInfo":     cpuInfo, | ||||
| 		"memoryInfo":  memoryInfo, | ||||
| 		"diskInfo":    diskInfo, | ||||
| 		"networkInfo": networkInfo, | ||||
| 		"cpuUsage":    cpuUsage, | ||||
| 		"memUsage":    memUsage, | ||||
| 		"diskUsage":   diskUsage, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetHardwareStatsAPI returns hardware stats in JSON format | ||||
| func (h *SystemHandler) GetHardwareStatsAPI(c *fiber.Ctx) error { | ||||
| 	// Get hardware stats from the StatsManager | ||||
| 	var hardwareStats map[string]interface{} | ||||
| 	if h.statsManager != nil { | ||||
| 		hardwareStats = h.statsManager.GetHardwareStats() | ||||
| 	} else { | ||||
| 		// Fallback to direct function call if StatsManager is not available | ||||
| 		hardwareStats = stats.GetHardwareStats() | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(hardwareStats) | ||||
| } | ||||
|  | ||||
| // GetProcessStatsAPI returns process stats in JSON format for API consumption | ||||
| func (h *SystemHandler) GetProcessStatsAPI(c *fiber.Ctx) error { | ||||
| 	// Check if StatsManager is properly initialized | ||||
| 	if h.statsManager == nil { | ||||
| 		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": "System error: Stats manager not initialized", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Get process data from the StatsManager | ||||
| 	processData, err := h.statsManager.GetProcessStatsFresh(100) // Limit to 100 processes | ||||
| 	if err != nil { | ||||
| 		// Try getting cached data as fallback | ||||
| 		processData, err = h.statsManager.GetProcessStats(100) | ||||
| 		if err != nil { | ||||
| 			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 				"error": "Failed to get process data: " + err.Error(), | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Convert to fiber.Map for JSON response | ||||
| 	response := fiber.Map{ | ||||
| 		"total":     processData.Total, | ||||
| 		"filtered":  processData.Filtered, | ||||
| 		"timestamp": time.Now().Unix(), | ||||
| 	} | ||||
|  | ||||
| 	// Convert processes to a slice of maps | ||||
| 	processes := make([]fiber.Map, len(processData.Processes)) | ||||
| 	for i, proc := range processData.Processes { | ||||
| 		processes[i] = fiber.Map{ | ||||
| 			"pid":             proc.PID, | ||||
| 			"name":            proc.Name, | ||||
| 			"status":          proc.Status, | ||||
| 			"cpu_percent":     proc.CPUPercent, | ||||
| 			"memory_mb":       proc.MemoryMB, | ||||
| 			"create_time_str": proc.CreateTime, | ||||
| 			"is_current":      proc.IsCurrent, | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	response["processes"] = processes | ||||
|  | ||||
| 	// Return JSON response | ||||
| 	return c.JSON(response) | ||||
| } | ||||
|  | ||||
| // GetSystemLogs renders the system logs page | ||||
| func (h *SystemHandler) GetSystemLogs(c *fiber.Ctx) error { | ||||
| 	return c.Render("admin/system/logs", fiber.Map{ | ||||
| 		"title": "System Logs", | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetSystemLogsTest renders the test logs page | ||||
| func (h *SystemHandler) GetSystemLogsTest(c *fiber.Ctx) error { | ||||
| 	return c.Render("admin/system/logs_test", fiber.Map{ | ||||
| 		"title": "Test Logs", | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetSystemSettings renders the system settings page | ||||
| func (h *SystemHandler) GetSystemSettings(c *fiber.Ctx) error { | ||||
| 	// Get the current time | ||||
| 	currentTime := time.Now().Format("2006-01-02 15:04:05") | ||||
|  | ||||
| 	// Render the template with the system settings | ||||
| 	return c.Render("admin/system/settings", fiber.Map{ | ||||
| 		"title":       "System Settings", | ||||
| 		"currentTime": currentTime, | ||||
| 		"settings": map[string]interface{}{ | ||||
| 			"autoUpdate":      true, | ||||
| 			"logLevel":        "info", | ||||
| 			"maxLogSize":      "100MB", | ||||
| 			"backupFrequency": "Daily", | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user