Changeset 0.27.2 (#169)
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/taskutil"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,15 @@ func (h *TaskHandler) ListAll(c *gin.Context) {
|
||||
// Create creates a new task.
|
||||
// POST /api/v1/tasks
|
||||
func (h *TaskHandler) Create(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// v0.27.2: Check global task configuration
|
||||
taskCfg := taskutil.LoadTaskConfig(ctx, h.stores.GlobalConfig)
|
||||
if !taskCfg.Enabled {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "tasks are disabled by the administrator"})
|
||||
return
|
||||
}
|
||||
|
||||
var t models.Task
|
||||
if err := c.ShouldBindJSON(&t); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -59,6 +69,14 @@ func (h *TaskHandler) Create(c *gin.Context) {
|
||||
|
||||
t.OwnerID = c.GetString("user_id")
|
||||
|
||||
// v0.27.2: Personal task check — non-admin users need tasks.allow_personal
|
||||
if t.Scope == "personal" || t.Scope == "" {
|
||||
if c.GetString("role") != "admin" && !taskCfg.AllowPersonal {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "personal tasks are disabled — contact your administrator"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if t.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
@@ -77,6 +95,12 @@ func (h *TaskHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// v0.27.2: Validate cron expression before persisting
|
||||
if err := taskutil.ValidateCron(t.Schedule); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid schedule: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Defaults
|
||||
if t.Scope == "" {
|
||||
t.Scope = "personal"
|
||||
@@ -90,29 +114,22 @@ func (h *TaskHandler) Create(c *gin.Context) {
|
||||
if t.OutputMode == "" {
|
||||
t.OutputMode = "channel"
|
||||
}
|
||||
if t.MaxTokens == 0 {
|
||||
t.MaxTokens = 4096
|
||||
}
|
||||
if t.MaxToolCalls == 0 {
|
||||
t.MaxToolCalls = 10
|
||||
}
|
||||
if t.MaxWallClock == 0 {
|
||||
t.MaxWallClock = 300
|
||||
}
|
||||
|
||||
// v0.27.2: Apply global default budgets for zero-value fields
|
||||
taskCfg.ApplyDefaults(&t)
|
||||
|
||||
// Compute initial next_run_at
|
||||
// For "once" tasks, next_run_at = now (runs on next poll)
|
||||
// For cron tasks, compute from schedule
|
||||
if t.Schedule == "once" {
|
||||
now := time.Now().UTC()
|
||||
t.NextRunAt = &now
|
||||
} else {
|
||||
// v0.27.2: Full cron parsing via robfig/cron/v3
|
||||
t.NextRunAt = taskutil.NextRunFromSchedule(t.Schedule, t.Timezone)
|
||||
}
|
||||
// TODO: compute next_run_at from cron expression (requires robfig/cron/v3)
|
||||
// For now, "once" tasks run immediately; cron tasks need next_run_at set by scheduler
|
||||
|
||||
t.IsActive = true
|
||||
|
||||
if err := h.stores.Tasks.Create(c.Request.Context(), &t); err != nil {
|
||||
if err := h.stores.Tasks.Create(ctx, &t); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task: " + err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -160,12 +177,29 @@ func (h *TaskHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// v0.27.2: Validate new schedule if provided
|
||||
if patch.Schedule != nil {
|
||||
if err := taskutil.ValidateCron(*patch.Schedule); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid schedule: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.stores.Tasks.Update(ctx, id, patch); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update task"})
|
||||
return
|
||||
}
|
||||
|
||||
updated, _ := h.stores.Tasks.GetByID(ctx, id)
|
||||
|
||||
// Recompute next_run_at if schedule or timezone changed
|
||||
if patch.Schedule != nil || patch.Timezone != nil {
|
||||
tz := updated.Timezone
|
||||
next := taskutil.NextRunFromSchedule(updated.Schedule, tz)
|
||||
_ = h.stores.Tasks.SetNextRun(ctx, id, next)
|
||||
updated.NextRunAt = next
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, updated)
|
||||
}
|
||||
|
||||
@@ -213,6 +247,13 @@ func (h *TaskHandler) RunNow(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// v0.27.2: Check tasks enabled
|
||||
taskCfg := taskutil.LoadTaskConfig(ctx, h.stores.GlobalConfig)
|
||||
if !taskCfg.Enabled {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "tasks are disabled by the administrator"})
|
||||
return
|
||||
}
|
||||
|
||||
t, err := h.stores.Tasks.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
@@ -238,3 +279,33 @@ func (h *TaskHandler) RunNow(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"scheduled": true, "next_run_at": now})
|
||||
}
|
||||
|
||||
// KillRun cancels the active run of a task.
|
||||
// POST /api/v1/tasks/:id/kill
|
||||
func (h *TaskHandler) KillRun(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
t, err := h.stores.Tasks.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
if c.GetString("role") != "admin" && t.OwnerID != c.GetString("user_id") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
active, _ := h.stores.Tasks.GetActiveRun(ctx, id)
|
||||
if active == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no active run to cancel"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.stores.Tasks.UpdateRun(ctx, active.ID, "cancelled", active.TokensUsed, active.ToolCalls, active.WallClock, "killed by user"); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to cancel run"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"killed": true, "run_id": active.ID})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user