package handler
import (
"fmt"
"net/http"
"github.com/siherrmann/queuerManager/view/screens"
"github.com/labstack/echo/v5"
)
func (m *ManagerHandler) AddJobView(c *echo.Context) error {
tasks, err := m.taskDB.SelectAllTasks(0, 100)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve tasks")
}
c.Response().Header().Add("HX-Push-Url", "/")
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.AddJob(tasks))
}
// AddJobConfigView renders a task-specific screen with parameter inputs
func (m *ManagerHandler) AddJobConfigView(c *echo.Context) error {
taskKey := c.Param("taskKey")
task, err := m.taskDB.SelectTaskByKey(taskKey)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, "Missing or non-existent task name")
}
files, err := m.Filesystem.ListFiles()
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Error listing files: %v", err))
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/task/%s", task.Key))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.AddJobConfig(task, files))
}
package handler
import (
"net/http"
"github.com/labstack/echo/v5"
)
// GetConnections retrieves all active connections
func (m *ManagerHandler) GetConnections(c *echo.Context) error {
connections, err := m.Queuer.GetConnections()
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve connections")
}
return c.JSON(http.StatusOK, connections)
}
package handler
import (
"fmt"
"log"
"log/slog"
"net/http"
"github.com/siherrmann/queuerManager/view/components"
"github.com/gorilla/csrf"
"github.com/labstack/echo/v5"
)
func HandleErrorView(err error, c *echo.Context) {
code := http.StatusInternalServerError
var message interface{}
message = err.Error()
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = he.Message
}
c.Logger().Error(fmt.Sprintf("failed with code %d", code), slog.String("error", err.Error()))
err = renderPopup(c, components.PopupError("Error", fmt.Sprint(message)))
if err != nil {
c.Logger().Error("failed to render error popup", slog.String("error", err.Error()))
}
}
func HandleCSRFErrorView(w http.ResponseWriter, r *http.Request) {
err := csrf.FailureReason(r)
log.Printf("CSRF error: %v", err)
err = renderPopupHTTP(w, components.PopupError("Error", "Invalid CSRF token, please reload the page."))
if err != nil {
log.Printf("Failed to render CSRF error popup: %v", err)
}
}
package handler
import (
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/siherrmann/queuerManager/upload"
"github.com/siherrmann/queuerManager/view/screens"
"github.com/labstack/echo/v5"
)
func (m *ManagerHandler) UploadFiles(c *echo.Context) error {
// Parse multipart form with 32MB max memory
err := c.Request().ParseMultipartForm(32 << 20)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Failed to parse multipart form: %v", err))
}
form := c.Request().MultipartForm
defer form.RemoveAll() // Clean up temporary files
files := form.File["files"]
if len(files) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No files found in the request")
}
var uploadedFiles []string
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to open file %s: %v", fileHeader.Filename, err))
}
defer file.Close()
// Generate safe filename (you might want to add UUID or timestamp for uniqueness)
filename := filepath.Base(fileHeader.Filename)
err = m.Filesystem.Write(filename, file, fileHeader.Size)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to save file %s: %v", filename, err))
}
uploadedFiles = append(uploadedFiles, filename)
}
// TODO add loader on trigger
c.Response().Header().Add("HX-Trigger-After-Settle", "reloadFiles")
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("%v file(s) uploaded successfully", len(uploadedFiles)))
}
func (m *ManagerHandler) DeleteFile(c *echo.Context) error {
filename := c.Param("filename")
err := m.Filesystem.Remove(filename)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to delete file %s: %v", filename, err))
}
c.Response().Header().Add("HX-Trigger-After-Settle", "reloadFiles")
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("File %s deleted successfully", filename))
}
// DeleteFiles deletes multiple files
func (m *ManagerHandler) DeleteFiles(c *echo.Context) error {
names := c.QueryParams()["name"]
if len(names) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No file names provided")
}
var deletedFiles []string
var errors []string
for _, name := range names {
err := m.Filesystem.Remove(name)
if err != nil {
errors = append(errors, fmt.Sprintf("%s: %v", name, err))
} else {
deletedFiles = append(deletedFiles, name)
}
}
c.Response().Header().Add("HX-Trigger-After-Settle", "getFiles")
if len(errors) > 0 {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Deleted %d file(s), but %d failed: %v", len(deletedFiles), len(errors), errors))
}
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("%d file(s) deleted successfully", len(deletedFiles)))
}
// FileView renders the file detail view
func (m *ManagerHandler) FileView(c *echo.Context) error {
filename := c.QueryParam("name")
if filename == "" {
return renderPopupOrJson(c, http.StatusBadRequest, "File name is required")
}
files, err := m.Filesystem.ListFiles()
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to list files: %v", err))
}
var foundFile *upload.File
for _, file := range files {
if file.Name == filename {
foundFile = &file
break
}
}
if foundFile == nil {
return renderPopupOrJson(c, http.StatusNotFound, "File not found")
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/file?name=%s", filename))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.File(*foundFile))
}
// FilesView renders the files list view
func (m *ManagerHandler) FilesView(c *echo.Context) error {
search := c.QueryParam("search")
files, err := m.Filesystem.ListFiles()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": fmt.Sprintf("Failed to list files: %v", err),
})
}
if search != "" {
var filteredFiles []upload.File
for _, file := range files {
if strings.Contains(file.Name, search) {
filteredFiles = append(filteredFiles, file)
}
}
files = filteredFiles
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/files?search=%s", search))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.Files(files, search))
}
// AddFilePopupView renders the add file popup
func (m *ManagerHandler) AddFilePopupView(c *echo.Context) error {
return renderPopup(c, screens.AddFilePopup())
}
// DeleteFilePopupView renders the delete file popup
func (m *ManagerHandler) DeleteFilePopupView(c *echo.Context) error {
names := c.QueryParams()["name"]
if len(names) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No file names provided")
}
return renderPopup(c, screens.DeleteFilePopup(names))
}
package handler
import (
"fmt"
"log"
"net/http"
"strconv"
"github.com/siherrmann/queuerManager/view/screens"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/siherrmann/queuer/model"
)
// =======API Handlers=======
// AddJob handles the addition of a new job
func (m *ManagerHandler) AddJob(c *echo.Context) error {
taskKey := c.Param("taskKey")
task, err := m.taskDB.SelectTaskByKey(taskKey)
if err != nil {
return c.String(http.StatusNotFound, "Task not found")
}
// Validate regular parameters
parameters := map[string]any{}
validations := task.InputParameters
validations = append(validations, task.InputParametersKeyed...)
err = m.validator.UnmapOrUnmarshalValidateAndUpdateWithValidation(c.Request(), ¶meters, validations)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Validation error: %v", err))
}
// Validate keyed parameters (extract from form values with "keyed_" prefix)
parametersList := []any{}
parametersKeyed := map[string]any{}
for _, v := range task.InputParameters {
if val, ok := parameters[v.Key]; ok {
parametersList = append(parametersList, val)
}
}
for _, v := range task.InputParametersKeyed {
if val, ok := parameters[v.Key]; ok {
parametersKeyed[v.Key] = val
}
}
// Add job with keyed parameters map and spread parameter list
jobAdded, err := m.Queuer.AddJob(taskKey, parametersKeyed, parametersList...)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to add job: %v", err))
}
c.Response().Header().Add("HX-Redirect", fmt.Sprintf("/job?rid=%s", jobAdded.RID.String()))
return renderPopupOrJson(c, http.StatusOK, jobAdded)
}
// GetJob retrieves a specific job by RID
func (m *ManagerHandler) GetJob(c *echo.Context) error {
ridStr := c.Param("rid")
rid, err := uuid.Parse(ridStr)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, "Invalid job RID format")
}
job, err := m.Queuer.GetJob(rid)
if err != nil {
job, err = m.Queuer.GetJobEnded(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusNotFound, "Job not found")
}
}
return renderPopupOrJson(c, http.StatusOK, job)
}
// GetJobs retrieves a paginated list of jobs
func (m *ManagerHandler) GetJobs(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 10
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return renderPopupOrJson(c, http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
jobs, err := m.Queuer.GetJobs(lastId, limit)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, "Failed to retrieve jobs")
}
return renderPopupOrJson(c, http.StatusOK, jobs)
}
// CancelJob cancels a specific job by RID
func (m *ManagerHandler) CancelJob(c *echo.Context) error {
ridStr := c.Param("rid")
rid, err := uuid.Parse(ridStr)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, "Invalid job RID format")
}
cancelledJob, err := m.Queuer.CancelJob(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, "Failed to cancel job")
}
c.Response().Header().Add("HX-Redirect", "/jobArchive")
return renderPopupOrJson(c, http.StatusOK, cancelledJob)
}
// CancelJobs cancels multiple jobs by their RIDs
func (m *ManagerHandler) CancelJobs(c *echo.Context) error {
form, err := c.FormValues()
if _, ok := form["rid"]; !ok || err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, "Failed to parse form with job RIDs")
}
ridStrs := form["rid"]
if len(ridStrs) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No job RIDs provided")
}
var rids []uuid.UUID
for _, ridStr := range ridStrs {
rid, err := uuid.Parse(ridStr)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid job RID format: %s", ridStr))
}
rids = append(rids, rid)
}
var cancelledJobs []*model.Job
for _, rid := range rids {
cancelledJob, err := m.Queuer.CancelJob(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, "Failed to cancel jobs")
}
cancelledJobs = append(cancelledJobs, cancelledJob)
}
c.Response().Header().Add("HX-Redirect", "/jobArchive")
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("%v jobs cancelled successfully", len(cancelledJobs)))
}
// DeleteJob deletes a specific job by RID
func (m *ManagerHandler) DeleteJob(c *echo.Context) error {
ridStr := c.Param("rid")
rid, err := uuid.Parse(ridStr)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid rid: %v", err))
}
err = m.Queuer.DeleteJob(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to delete job: %v", err))
}
// TODO add loader on trigger
c.Response().Header().Add("HX-Trigger-After-Settle", "reloadJobArchive")
return renderPopupOrJson(c, http.StatusOK, "Job deleted successfully")
}
// =======View Handlers=======
// JobView renders the job detail view
func (m *ManagerHandler) JobView(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if len(ridStrings) == 0 || !ok {
return renderPopupOrJson(c, http.StatusBadRequest, "Missing job RID")
}
rid, err := uuid.Parse(ridStrings[0])
if err != nil {
return c.String(http.StatusBadRequest, "Invalid job RID format")
}
job, err := m.Queuer.GetJob(rid)
if err != nil {
job, err = m.Queuer.GetJobEnded(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusNotFound, "Job not found")
}
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/job?rid=%s", rid.String()))
c.Response().Header().Add("HX-Retarget", "#body")
status := http.StatusOK
if job.Status == model.JobStatusFailed || job.Status == model.JobStatusCancelled || job.Status == model.JobStatusSucceeded {
status = 286 // Custom status code to end htmx polling
}
return render(c, screens.Job(job), status)
}
// JobsView renders the jobs view
func (m *ManagerHandler) JobsView(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
search := c.QueryParam("search")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return c.String(http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 100
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return c.String(http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
var jobs []*model.Job
var err error
if search != "" {
log.Printf("searching for: %v", search)
jobs, err = m.Queuer.GetJobsBySearch(search, lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to search jobs")
}
log.Printf("found jobs: %v", jobs)
} else {
jobs, err = m.Queuer.GetJobs(lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve jobs")
}
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/jobs?search=%s&limit=%d&lastId=%d", search, limit, lastId))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.Jobs(jobs, search))
}
package handler
import (
"fmt"
"net/http"
"strconv"
"github.com/siherrmann/queuerManager/view/screens"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/siherrmann/queuer/model"
)
// GetJobArchive retrieves a specific archived job by RID
func (m *ManagerHandler) GetJobArchive(c *echo.Context) error {
ridStr := c.Param("rid")
rid, err := uuid.Parse(ridStr)
if err != nil {
return c.String(http.StatusBadRequest, "Invalid job archive RID format")
}
job, err := m.Queuer.GetJobEnded(rid)
if err != nil {
return c.String(http.StatusNotFound, "Archived job not found")
}
return c.JSON(http.StatusOK, job)
}
// GetJobsArchive retrieves a paginated list of archived jobs
func (m *ManagerHandler) GetJobsArchive(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return c.String(http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 10
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return c.String(http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
jobArchives, err := m.Queuer.GetJobsEnded(lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve archived jobs")
}
return c.JSON(http.StatusOK, jobArchives)
}
// ======View Handlers======
// JobArchiveView renders the job archive view
func (m *ManagerHandler) JobArchiveView(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
search := c.QueryParam("search")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return c.String(http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 100
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return c.String(http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
var archivedJobs []*model.Job
var err error
if search != "" {
archivedJobs, err = m.Queuer.GetJobsEndedBySearch(search, lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to search archived jobs")
}
} else {
archivedJobs, err = m.Queuer.GetJobsEnded(lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve archived jobs")
}
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/jobArchive?search=%s&limit=%d&lastId=%d", search, limit, lastId))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.JobArchive(archivedJobs, search))
}
// ReaddJobFromArchiveView readds a job from the archive back to the queue
func (m *ManagerHandler) ReaddJobFromArchiveView(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if !ok || len(ridStrings) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No job RID provided")
}
if len(ridStrings) > 1 {
return renderPopupOrJson(c, http.StatusBadRequest, "Please select exactly one job")
}
rid, err := uuid.Parse(ridStrings[0])
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid job RID: %v", err))
}
readdedJob, err := m.Queuer.ReaddJobFromArchive(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to re-add job: %v", err))
}
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("Job %s re-added to queue", readdedJob.RID.String()))
}
package handler
import (
"net/http"
"github.com/siherrmann/queuer"
"github.com/siherrmann/queuerManager/database"
"github.com/siherrmann/queuerManager/upload"
"github.com/labstack/echo/v5"
"github.com/siherrmann/validator"
)
type ManagerHandler struct {
Queuer *queuer.Queuer
Filesystem upload.Filesystem
validator *validator.Validator
taskDB *database.TaskDBHandler
}
func NewManagerHandler(filesystem upload.Filesystem, taskDB *database.TaskDBHandler, queuerInstance *queuer.Queuer) *ManagerHandler {
return &ManagerHandler{
Queuer: queuerInstance,
Filesystem: filesystem,
validator: validator.NewValidator(),
taskDB: taskDB,
}
}
// Health check handler
func (m *ManagerHandler) HealthCheck(c *echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"status": "healthy",
"service": "queuer-manager",
})
}
package handler
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/siherrmann/queuerManager/model"
"github.com/siherrmann/queuerManager/view/screens"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
vm "github.com/siherrmann/validator/model"
)
// =======API Handlers=======
// AddTask handles the addition of a new task
func (m *ManagerHandler) AddTask(c *echo.Context) error {
var requestData struct {
Key string `json:"key" form:"key"`
Name string `json:"name" form:"name"`
Description string `json:"description" form:"description"`
Validations string `json:"validations" form:"validations"`
ValidationsKeyed string `json:"validations_keyed" form:"validations_keyed"`
OutputParameters string `json:"output_parameters" form:"output_parameters"`
}
if err := c.Bind(&requestData); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
}
if requestData.Key == "" {
return renderPopupOrJson(c, http.StatusBadRequest, "Task key is required")
}
if requestData.Name == "" {
return renderPopupOrJson(c, http.StatusBadRequest, "Task name is required")
}
// Parse validations JSON
var validations []vm.Validation
if requestData.Validations != "" {
if err := json.Unmarshal([]byte(requestData.Validations), &validations); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid validations JSON: %v", err))
}
}
// Parse validations_keyed JSON
var validationsKeyed []vm.Validation
if requestData.ValidationsKeyed != "" {
if err := json.Unmarshal([]byte(requestData.ValidationsKeyed), &validationsKeyed); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid validations_keyed JSON: %v", err))
}
}
// Parse output_parameters JSON
var outputParameters []vm.Validation
if requestData.OutputParameters != "" {
if err := json.Unmarshal([]byte(requestData.OutputParameters), &outputParameters); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid output_parameters JSON: %v", err))
}
}
task := &model.Task{
Key: requestData.Key,
Name: requestData.Name,
Description: requestData.Description,
InputParameters: validations,
InputParametersKeyed: validationsKeyed,
OutputParameters: outputParameters,
}
insertedTask, err := m.taskDB.InsertTask(task)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to add task: %v", err))
}
c.Response().Header().Add("HX-Redirect", "/tasks")
return renderPopupOrJson(c, http.StatusCreated, "Task added successfully", insertedTask)
}
// UpdateTask handles updating an existing task
func (m *ManagerHandler) UpdateTask(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if len(ridStrings) == 0 || !ok {
return renderPopupOrJson(c, http.StatusBadRequest, "Missing task RID")
}
rid, err := uuid.Parse(ridStrings[0])
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid task RID: %v", err))
}
var requestData struct {
Key string `json:"key" form:"key"`
Name string `json:"name" form:"name"`
Description string `json:"description" form:"description"`
Validations string `json:"validations" form:"validations"`
ValidationsKeyed string `json:"validations_keyed" form:"validations_keyed"`
OutputParameters string `json:"output_parameters" form:"output_parameters"`
}
if err := c.Bind(&requestData); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
}
if requestData.Key == "" {
return renderPopupOrJson(c, http.StatusBadRequest, "Task key is required")
}
if requestData.Name == "" {
return renderPopupOrJson(c, http.StatusBadRequest, "Task name is required")
}
// Parse validations JSON
var validations []vm.Validation
if requestData.Validations != "" {
if err := json.Unmarshal([]byte(requestData.Validations), &validations); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid validations JSON: %v", err))
}
}
// Parse validations_keyed JSON
var validationsKeyed []vm.Validation
if requestData.ValidationsKeyed != "" {
if err := json.Unmarshal([]byte(requestData.ValidationsKeyed), &validationsKeyed); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid validations_keyed JSON: %v", err))
}
}
// Parse output_parameters JSON
var outputParameters []vm.Validation
if requestData.OutputParameters != "" {
if err := json.Unmarshal([]byte(requestData.OutputParameters), &outputParameters); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid output_parameters JSON: %v", err))
}
}
task := &model.Task{
RID: rid,
Key: requestData.Key,
Name: requestData.Name,
Description: requestData.Description,
InputParameters: validations,
InputParametersKeyed: validationsKeyed,
OutputParameters: outputParameters,
}
updatedTask, err := m.taskDB.UpdateTask(task)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to update task: %v", err))
}
c.Response().Header().Add("HX-Redirect", "/tasks")
return renderPopupOrJson(c, http.StatusOK, "Task updated successfully", updatedTask)
}
// DeleteTasks deletes multiple tasks by RIDs
func (m *ManagerHandler) DeleteTasks(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if len(ridStrings) == 0 || !ok {
return renderPopupOrJson(c, http.StatusBadRequest, "Missing task RID")
}
// Delete each task
deletedCount := 0
var errors []string
for _, ridStr := range ridStrings {
rid, err := uuid.Parse(ridStr)
if err != nil {
errors = append(errors, fmt.Sprintf("Invalid RID %s: %v", ridStr, err))
continue
}
err = m.taskDB.DeleteTask(rid)
if err != nil {
errors = append(errors, fmt.Sprintf("Failed to delete task %s: %v", ridStr, err))
continue
}
deletedCount++
}
// Trigger table refresh
c.Response().Header().Add("HX-Trigger", "getTasks")
if len(errors) > 0 {
return renderPopupOrJson(c, http.StatusPartialContent, fmt.Sprintf("Deleted %d tasks. Errors: %v", deletedCount, errors))
}
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("Successfully deleted %d task(s)", deletedCount))
}
// GetTask retrieves a specific task by RID
func (m *ManagerHandler) GetTask(c *echo.Context) error {
ridStr := c.Param("rid")
rid, err := uuid.Parse(ridStr)
if err != nil {
return c.String(http.StatusBadRequest, "Invalid task RID format")
}
task, err := m.taskDB.SelectTask(rid)
if err != nil {
return c.String(http.StatusNotFound, "Task not found")
}
return c.JSON(http.StatusOK, task)
}
// GetTaskByName retrieves a specific task by name
func (m *ManagerHandler) GetTaskByName(c *echo.Context) error {
name := c.Param("name")
if name == "" {
return c.String(http.StatusBadRequest, "Task name is required")
}
task, err := m.taskDB.SelectTaskByKey(name)
if err != nil {
return c.String(http.StatusNotFound, "Task not found")
}
return c.JSON(http.StatusOK, task)
}
// GetTasks retrieves a paginated list of tasks
func (m *ManagerHandler) GetTasks(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return c.String(http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 10
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return c.String(http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
tasks, err := m.taskDB.SelectAllTasks(lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve tasks")
}
return c.JSON(http.StatusOK, tasks)
}
// =======View Handlers=======
// TaskView renders the task detail view
func (m *ManagerHandler) TaskView(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if len(ridStrings) == 0 || !ok {
return renderPopupOrJson(c, http.StatusBadRequest, "Missing task RID")
}
rid, err := uuid.Parse(ridStrings[0])
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid task RID: %v", err))
}
task, err := m.taskDB.SelectTask(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusNotFound, "Task not found")
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/task?rid=%v", rid))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.Task(task))
}
// TasksView renders the tasks list view
func (m *ManagerHandler) TasksView(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
search := c.QueryParam("search")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return c.String(http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 100
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return c.String(http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
var tasks []*model.Task
var err error
if search != "" {
tasks, err = m.taskDB.SelectAllTasksBySearch(search, lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to search tasks")
}
} else {
tasks, err = m.taskDB.SelectAllTasks(lastId, limit)
if err != nil {
log.Printf("Error retrieving tasks: %v", err)
return c.String(http.StatusInternalServerError, "Failed to retrieve tasks")
}
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/tasks?search=%s&limit=%d&lastId=%d", search, limit, lastId))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.Tasks(tasks, search))
}
// =======Popup Handlers=======
// AddTaskPopupView renders the add task popup
func (m *ManagerHandler) AddTaskPopupView(c *echo.Context) error {
return renderPopup(c, screens.AddTaskPopup())
}
// UpdateTaskPopupView renders the update task popup
func (m *ManagerHandler) UpdateTaskPopupView(c *echo.Context) error {
ridStr := c.QueryParam("rid")
rid, err := uuid.Parse(ridStr)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid task RID: %v", err))
}
task, err := m.taskDB.SelectTask(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusNotFound, "Task not found")
}
return renderPopup(c, screens.UpdateTaskPopup(task))
}
// DeleteTaskPopupView renders the delete task confirmation popup
func (m *ManagerHandler) DeleteTaskPopupView(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if len(ridStrings) == 0 || !ok {
return renderPopupOrJson(c, http.StatusBadRequest, "Missing task RIDs")
}
log.Printf("rids: %v", ridStrings[0])
return renderPopup(c, screens.DeleteTaskPopup(ridStrings))
}
// ImportTaskPopupView renders the import task popup
func (m *ManagerHandler) ImportTaskPopupView(c *echo.Context) error {
return renderPopup(c, screens.ImportTaskPopup())
}
// ExportTask exports selected tasks as JSON array file
func (m *ManagerHandler) ExportTask(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if len(ridStrings) == 0 || !ok {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Missing task RIDs"})
}
var exportTasks []map[string]interface{}
for _, ridStr := range ridStrings {
rid, err := uuid.Parse(ridStr)
if err != nil {
log.Printf("Invalid task RID: %s, skipping", ridStr)
continue
}
task, err := m.taskDB.SelectTask(rid)
if err != nil {
log.Printf("Task not found: %s, skipping", ridStr)
continue
}
// Create a clean export without ID and timestamps
exportTask := map[string]interface{}{
"key": task.Key,
"name": task.Name,
"description": task.Description,
"input_parameters": task.InputParameters,
"input_parameters_keyed": task.InputParametersKeyed,
"output_parameters": task.OutputParameters,
}
exportTasks = append(exportTasks, exportTask)
}
if len(exportTasks) == 0 {
return c.JSON(http.StatusNotFound, map[string]string{"error": "No valid tasks found to export"})
}
jsonData, err := json.MarshalIndent(exportTasks, "", " ")
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to marshal tasks"})
}
filename := "tasks_export.json"
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
c.Response().Header().Set("Content-Type", "application/json")
return c.Blob(http.StatusOK, "application/json", jsonData)
}
// ImportTask imports tasks from JSON array file
func (m *ManagerHandler) ImportTask(c *echo.Context) error {
file, err := c.FormFile("task_file")
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, "No file uploaded")
}
src, err := file.Open()
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, "Failed to open file")
}
defer src.Close()
var tasksData []struct {
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description"`
InputParameters []vm.Validation `json:"input_parameters"`
InputParametersKeyed []vm.Validation `json:"input_parameters_keyed"`
OutputParameters []vm.Validation `json:"output_parameters"`
}
if err := json.NewDecoder(src).Decode(&tasksData); err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid JSON format: %v", err))
}
if len(tasksData) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No tasks found in JSON file")
}
importedCount := 0
var errors []string
for _, taskData := range tasksData {
if taskData.Key == "" {
errors = append(errors, "Skipped task with empty key")
continue
}
task := &model.Task{
Key: taskData.Key,
Name: taskData.Name,
Description: taskData.Description,
InputParameters: taskData.InputParameters,
InputParametersKeyed: taskData.InputParametersKeyed,
OutputParameters: taskData.OutputParameters,
}
_, err := m.taskDB.InsertTask(task)
if err != nil {
errors = append(errors, fmt.Sprintf("Failed to import task '%s': %v", taskData.Key, err))
continue
}
importedCount++
}
c.Response().Header().Add("HX-Redirect", "/tasks")
if len(errors) > 0 {
errorMsg := fmt.Sprintf("Imported %d tasks with errors: %v", importedCount, errors)
return renderPopupOrJson(c, http.StatusPartialContent, errorMsg)
}
return renderPopupOrJson(c, http.StatusCreated, fmt.Sprintf("Successfully imported %d tasks", importedCount))
}
package handler
import (
"context"
"fmt"
"net/http"
"github.com/siherrmann/queuerManager/view/components"
"github.com/a-h/templ"
"github.com/labstack/echo/v5"
)
func render(ctx *echo.Context, t templ.Component, status ...int) error {
buf := templ.GetBuffer()
defer templ.ReleaseBuffer(buf)
if err := t.Render(ctx.Request().Context(), buf); err != nil {
return err
}
if len(status) > 0 {
return ctx.HTML(status[0], buf.String())
}
return ctx.HTML(http.StatusOK, buf.String())
}
func renderPopup(c *echo.Context, component templ.Component) error {
c.Response().Header().Add("HX-Retarget", "#body")
c.Response().Header().Add("HX-Reswap", "beforeend")
return render(c, component)
}
func renderHTTP(writer http.ResponseWriter, t templ.Component) error {
buf := templ.GetBuffer()
defer templ.ReleaseBuffer(buf)
if err := t.Render(context.Background(), buf); err != nil {
return err
}
writer.WriteHeader(http.StatusOK)
fmt.Fprint(writer, buf.String())
return nil
}
func renderPopupHTTP(writer http.ResponseWriter, component templ.Component) error {
writer.Header().Add("HX-Retarget", "#body")
writer.Header().Add("HX-Reswap", "beforeend")
return renderHTTP(writer, component)
}
func renderPopupOrJson(c *echo.Context, status int, value ...any) error {
// No value to render
if len(value) == 0 {
return c.NoContent(status)
}
// If HTMX request, render popup
if c.Request().Header.Get("HX-Request") != "" {
messageStr := ""
if messageTemp, ok := value[0].(string); ok {
messageStr = messageTemp
} else {
messageStr = fmt.Sprintf("%v", value[0])
}
if status >= 200 && status < 300 {
return renderPopup(c, components.PopupSuccess("Info", messageStr))
} else {
return renderPopup(c, components.PopupError("Error", messageStr))
}
}
// Otherwise, return JSON
// If single value that is a string, wrap in message object
if len(value) == 1 {
if message, ok := value[0].(string); ok {
return c.JSON(status, map[string]any{"message": message})
}
// Single non-string value, return it directly
return c.JSON(status, value[0])
}
// Multiple values, return as array
return c.JSON(status, value)
}
package handler
import (
"fmt"
"net/http"
"strconv"
"github.com/siherrmann/queuerManager/view/screens"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/siherrmann/queuer/model"
)
// GetWorker retrieves a specific worker by RID
func (m *ManagerHandler) GetWorker(c *echo.Context) error {
ridStr := c.Param("rid")
rid, err := uuid.Parse(ridStr)
if err != nil {
return c.String(http.StatusBadRequest, "Invalid worker RID format")
}
worker, err := m.Queuer.GetWorker(rid)
if err != nil {
return c.String(http.StatusNotFound, "Worker not found")
}
return c.JSON(http.StatusOK, worker)
}
// GetWorkers retrieves a paginated list of workers
func (m *ManagerHandler) GetWorkers(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return c.String(http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 100
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return c.String(http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
workers, err := m.Queuer.GetWorkers(lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve workers")
}
return c.JSON(http.StatusOK, workers)
}
// =======View Handlers=======
// WorkerView renders the worker detail page
func (m *ManagerHandler) WorkerView(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if len(ridStrings) == 0 || !ok {
return renderPopupOrJson(c, http.StatusBadRequest, "Missing worker RID")
}
rid, err := uuid.Parse(ridStrings[0])
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid worker RID: %v", err))
}
worker, err := m.Queuer.GetWorker(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusNotFound, "Worker not found")
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/worker?rid=%s", rid))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.Worker(worker))
}
// WorkersView renders the workers list page
func (m *ManagerHandler) WorkersView(c *echo.Context) error {
lastIdStr := c.QueryParam("lastId")
limitStr := c.QueryParam("limit")
search := c.QueryParam("search")
// Parse lastId with default
lastId := 0
if lastIdStr != "" {
parsedLastId, err := strconv.Atoi(lastIdStr)
if err != nil || parsedLastId < 0 {
return c.String(http.StatusBadRequest, "Invalid lastId format")
}
lastId = parsedLastId
}
// Parse limit with default
limit := 1000
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 || parsedLimit > 100 {
return c.String(http.StatusBadRequest, "Invalid limit (must be 1-100)")
}
limit = parsedLimit
}
var workers []*model.Worker
var err error
if search != "" {
workers, err = m.Queuer.GetWorkersBySearch(search, lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to search workers")
}
} else {
workers, err = m.Queuer.GetWorkers(lastId, limit)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to retrieve workers")
}
}
c.Response().Header().Add("HX-Push-Url", fmt.Sprintf("/workers?search=%s&limit=%d&lastId=%d", search, limit, lastId))
c.Response().Header().Add("HX-Retarget", "#body")
return render(c, screens.Workers(workers, search))
}
// StopWorkersView handles stopping workers
func (m *ManagerHandler) StopWorkersView(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if !ok || len(ridStrings) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No worker RIDs provided")
}
var rids []uuid.UUID
for _, ridStr := range ridStrings {
rid, err := uuid.Parse(ridStr)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid worker RID: %s", ridStr))
}
rids = append(rids, rid)
}
// Stop each worker
for _, rid := range rids {
err := m.Queuer.StopWorker(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to stop worker %s: %v", rid, err))
}
}
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("Successfully requested stop for %d worker(s)", len(rids)))
}
// StopWorkersGracefullyView handles gracefully stopping workers
func (m *ManagerHandler) StopWorkersGracefullyView(c *echo.Context) error {
ridStrings, ok := c.QueryParams()["rid"]
if !ok || len(ridStrings) == 0 {
return renderPopupOrJson(c, http.StatusBadRequest, "No worker RIDs provided")
}
var rids []uuid.UUID
for _, ridStr := range ridStrings {
rid, err := uuid.Parse(ridStr)
if err != nil {
return renderPopupOrJson(c, http.StatusBadRequest, fmt.Sprintf("Invalid worker RID: %s", ridStr))
}
rids = append(rids, rid)
}
// Gracefully stop each worker
for _, rid := range rids {
err := m.Queuer.StopWorkerGracefully(rid)
if err != nil {
return renderPopupOrJson(c, http.StatusInternalServerError, fmt.Sprintf("Failed to gracefully stop worker %s: %v", rid, err))
}
}
return renderPopupOrJson(c, http.StatusOK, fmt.Sprintf("Successfully requested graceful stop for %d worker(s)", len(rids)))
}