This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/schedules.go
Jeffrey Smith f0dd43144e rebrand: Switchboard Core → Armature
- Rename Go module switchboard-core → armature (155+ files)
- Rename Docker image → gobha/armature
- Rename K8s resources, secrets, deployments
- Rename Prometheus metrics switchboard_* → armature_*
- Rename env vars SWITCHBOARD_ADMIN_* → ARMATURE_ADMIN_*
- Rename DB names switchboard_core* → armature*
- Update all frontend branding, notification templates, docs
- Update CI scripts, e2e tests, Keycloak realm, nginx conf
- Rename scripts/switchboard-ca.sh → scripts/armature-ca.sh
- Rename k8s/switchboard.yaml → k8s/armature.yaml
- Rename chart alerting/dashboard files
- Fix: DockerHub push uses env: binding for secret injection
- Helm chart updated (name, labels, template functions, dashboard, alerting)
- Replace favicon/icon assets with Armature brand

No functional changes. Pure mechanical rename + CI fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:39:58 +00:00

310 lines
9.0 KiB
Go

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})
}