package handlers import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/robfig/cron/v3" "armature/models" "armature/store" "armature/triggers" ) // ScheduleHandler provides CRUD for user-created scheduled tasks. type ScheduleHandler struct { stores store.Stores engine *triggers.Engine } // NewScheduleHandler creates a ScheduleHandler. func NewScheduleHandler(stores store.Stores, engine *triggers.Engine) *ScheduleHandler { return &ScheduleHandler{stores: stores, engine: engine} } // ── User Endpoints ─────────────────────────── // ListMySchedules returns schedules created by the current user. // GET /api/v1/schedules func (h *ScheduleHandler) ListMySchedules(c *gin.Context) { userID := c.GetString("user_id") tasks, err := h.stores.ScheduledTasks.ListByCreator(c.Request.Context(), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"schedules": tasks}) } // CreateSchedule creates a new scheduled task. // POST /api/v1/schedules func (h *ScheduleHandler) CreateSchedule(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` Description string `json:"description"` CronExpr string `json:"cron_expr" binding:"required"` Script string `json:"script"` TemplateID string `json:"template_id"` TemplateParams any `json:"template_params"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate cron expression parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) schedule, err := parser.Parse(req.CronExpr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid cron expression: " + err.Error()}) return } nextFire := schedule.Next(time.Now()) userID := c.GetString("user_id") task := &models.ScheduledTask{ Name: req.Name, Description: req.Description, CreatorID: userID, RunAs: models.RunAsCreator, CronExpr: req.CronExpr, NextFireAt: &nextFire, Enabled: true, Script: req.Script, TemplateID: req.TemplateID, } if err := h.stores.ScheduledTasks.Create(c.Request.Context(), task); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Register with engine h.engine.RegisterSchedule(task) c.JSON(http.StatusCreated, task) } // GetSchedule returns a single scheduled task. // GET /api/v1/schedules/:id func (h *ScheduleHandler) GetSchedule(c *gin.Context) { task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if task == nil { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } // Check ownership (non-admin can only see own schedules) userID := c.GetString("user_id") if task.CreatorID != userID { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } c.JSON(http.StatusOK, task) } // UpdateSchedule updates a scheduled task. // PUT /api/v1/schedules/:id func (h *ScheduleHandler) UpdateSchedule(c *gin.Context) { task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id")) if err != nil || task == nil { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } userID := c.GetString("user_id") if task.CreatorID != userID { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } var req struct { Name *string `json:"name"` Description *string `json:"description"` CronExpr *string `json:"cron_expr"` Script *string `json:"script"` Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Name != nil { task.Name = *req.Name } if req.Description != nil { task.Description = *req.Description } if req.Script != nil { task.Script = *req.Script } if req.Enabled != nil { task.Enabled = *req.Enabled } if req.CronExpr != nil { parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) schedule, err := parser.Parse(*req.CronExpr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid cron expression: " + err.Error()}) return } task.CronExpr = *req.CronExpr nextFire := schedule.Next(time.Now()) task.NextFireAt = &nextFire } if err := h.stores.ScheduledTasks.Update(c.Request.Context(), task); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Re-register with engine h.engine.UnregisterSchedule(task.ID) if task.Enabled { h.engine.RegisterSchedule(task) } c.JSON(http.StatusOK, task) } // DeleteSchedule deletes a scheduled task. // DELETE /api/v1/schedules/:id func (h *ScheduleHandler) DeleteSchedule(c *gin.Context) { task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id")) if err != nil || task == nil { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } userID := c.GetString("user_id") if task.CreatorID != userID { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } h.engine.UnregisterSchedule(task.ID) if err := h.stores.ScheduledTasks.Delete(c.Request.Context(), task.ID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // RunSchedule manually triggers a scheduled task (test run). // POST /api/v1/schedules/:id/run func (h *ScheduleHandler) RunSchedule(c *gin.Context) { task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id")) if err != nil || task == nil { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } userID := c.GetString("user_id") if task.CreatorID != userID { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } // Fire immediately in a goroutine go h.engine.ManualRun(task) c.JSON(http.StatusAccepted, gin.H{"ok": true, "message": "task queued for execution"}) } // ListScheduleLogs returns execution logs for a scheduled task. // GET /api/v1/schedules/:id/logs func (h *ScheduleHandler) ListScheduleLogs(c *gin.Context) { task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id")) if err != nil || task == nil { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } userID := c.GetString("user_id") if task.CreatorID != userID { c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"}) return } limit := parseIntParam(c, "limit", 50) logs, err := h.stores.Triggers.ListLogs(c.Request.Context(), task.ID, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"logs": logs}) } // ── Admin Endpoints ────────────────────────── // AdminListSchedules returns all scheduled tasks (admin view). // GET /admin/schedules func (h *ScheduleHandler) AdminListSchedules(c *gin.Context) { opts := store.ScheduledTaskListOptions{ ListOptions: store.ListOptions{ Limit: parseIntParam(c, "limit", 50), Offset: parseIntParam(c, "offset", 0), }, CreatorID: c.Query("creator_id"), } if e := c.Query("enabled"); e != "" { b := e == "true" opts.Enabled = &b } tasks, total, err := h.stores.ScheduledTasks.List(c.Request.Context(), opts) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"schedules": tasks, "total": total}) } // AdminEnableSchedule enables a scheduled task. // PUT /admin/schedules/:id/enable func (h *ScheduleHandler) AdminEnableSchedule(c *gin.Context) { id := c.Param("id") if err := h.stores.ScheduledTasks.SetEnabled(c.Request.Context(), id, true); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } task, _ := h.stores.ScheduledTasks.GetByID(c.Request.Context(), id) if task != nil { h.engine.RegisterSchedule(task) } c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminDisableSchedule disables a scheduled task. // PUT /admin/schedules/:id/disable func (h *ScheduleHandler) AdminDisableSchedule(c *gin.Context) { id := c.Param("id") h.engine.UnregisterSchedule(id) if err := h.stores.ScheduledTasks.SetEnabled(c.Request.Context(), id, false); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminDeleteSchedule deletes a scheduled task. // DELETE /admin/schedules/:id func (h *ScheduleHandler) AdminDeleteSchedule(c *gin.Context) { id := c.Param("id") h.engine.UnregisterSchedule(id) if err := h.stores.ScheduledTasks.Delete(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) }