package postgres import ( "context" "fmt" "git.gobha.me/xcaliber/chat-switchboard/models" ) type TaskStore struct{} func NewTaskStore() *TaskStore { return &TaskStore{} } func (s *TaskStore) Create(ctx context.Context, t *models.Task) error { toolGrants := jsonOrNull(t.ToolGrants) return DB.QueryRowContext(ctx, ` INSERT INTO tasks (owner_id, team_id, name, description, scope, task_type, persona_id, model_id, system_prompt, user_prompt, workflow_id, tool_grants, schedule, timezone, is_active, max_tokens, max_tool_calls, max_wall_clock, output_mode, output_channel_id, webhook_url, provider_config_id, notify_on_complete, notify_on_failure, next_run_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25) RETURNING id, created_at, updated_at`, t.OwnerID, t.TeamID, t.Name, t.Description, t.Scope, t.TaskType, t.PersonaID, t.ModelID, t.SystemPrompt, t.UserPrompt, t.WorkflowID, toolGrants, t.Schedule, t.Timezone, t.IsActive, t.MaxTokens, t.MaxToolCalls, t.MaxWallClock, t.OutputMode, t.OutputChannelID, t.WebhookURL, t.ProviderConfigID, t.NotifyOnComplete, t.NotifyOnFailure, t.NextRunAt, ).Scan(&t.ID, &t.CreatedAt, &t.UpdatedAt) } func (s *TaskStore) GetByID(ctx context.Context, id string) (*models.Task, error) { t := &models.Task{} err := DB.QueryRowContext(ctx, ` SELECT id, owner_id, team_id, name, description, scope, task_type, persona_id, model_id, system_prompt, user_prompt, workflow_id, tool_grants, schedule, timezone, is_active, max_tokens, max_tool_calls, max_wall_clock, output_mode, output_channel_id, webhook_url, provider_config_id, notify_on_complete, notify_on_failure, last_run_at, next_run_at, run_count, created_at, updated_at FROM tasks WHERE id = $1`, id, ).Scan(&t.ID, &t.OwnerID, &t.TeamID, &t.Name, &t.Description, &t.Scope, &t.TaskType, &t.PersonaID, &t.ModelID, &t.SystemPrompt, &t.UserPrompt, &t.WorkflowID, &t.ToolGrants, &t.Schedule, &t.Timezone, &t.IsActive, &t.MaxTokens, &t.MaxToolCalls, &t.MaxWallClock, &t.OutputMode, &t.OutputChannelID, &t.WebhookURL, &t.ProviderConfigID, &t.NotifyOnComplete, &t.NotifyOnFailure, &t.LastRunAt, &t.NextRunAt, &t.RunCount, &t.CreatedAt, &t.UpdatedAt) if err != nil { return nil, err } return t, nil } func (s *TaskStore) Update(ctx context.Context, id string, p models.TaskPatch) error { // Dynamic PATCH: only update non-nil fields q := "UPDATE tasks SET updated_at = NOW()" args := []interface{}{} i := 1 if p.Name != nil { q += comma(i, "name"); args = append(args, *p.Name); i++ } if p.Description != nil { q += comma(i, "description"); args = append(args, *p.Description); i++ } if p.PersonaID != nil { q += comma(i, "persona_id"); args = append(args, *p.PersonaID); i++ } if p.ModelID != nil { q += comma(i, "model_id"); args = append(args, *p.ModelID); i++ } if p.SystemPrompt != nil { q += comma(i, "system_prompt"); args = append(args, *p.SystemPrompt); i++ } if p.UserPrompt != nil { q += comma(i, "user_prompt"); args = append(args, *p.UserPrompt); i++ } if p.Schedule != nil { q += comma(i, "schedule"); args = append(args, *p.Schedule); i++ } if p.Timezone != nil { q += comma(i, "timezone"); args = append(args, *p.Timezone); i++ } if p.IsActive != nil { q += comma(i, "is_active"); args = append(args, *p.IsActive); i++ } if p.MaxTokens != nil { q += comma(i, "max_tokens"); args = append(args, *p.MaxTokens); i++ } if p.MaxToolCalls != nil { q += comma(i, "max_tool_calls"); args = append(args, *p.MaxToolCalls); i++ } if p.MaxWallClock != nil { q += comma(i, "max_wall_clock"); args = append(args, *p.MaxWallClock); i++ } if p.OutputMode != nil { q += comma(i, "output_mode"); args = append(args, *p.OutputMode); i++ } if p.WebhookURL != nil { q += comma(i, "webhook_url"); args = append(args, *p.WebhookURL); i++ } if p.NotifyOnComplete != nil { q += comma(i, "notify_on_complete"); args = append(args, *p.NotifyOnComplete); i++ } if p.NotifyOnFailure != nil { q += comma(i, "notify_on_failure"); args = append(args, *p.NotifyOnFailure); i++ } q += pgWhere(i, "id") args = append(args, id) _, err := DB.ExecContext(ctx, q, args...) return err } func (s *TaskStore) Delete(ctx context.Context, id string) error { _, err := DB.ExecContext(ctx, `DELETE FROM tasks WHERE id = $1`, id) return err } func (s *TaskStore) ListByOwner(ctx context.Context, ownerID string) ([]models.Task, error) { return s.list(ctx, `WHERE owner_id = $1 ORDER BY created_at DESC`, ownerID) } func (s *TaskStore) ListByTeam(ctx context.Context, teamID string) ([]models.Task, error) { return s.list(ctx, `WHERE team_id = $1 ORDER BY created_at DESC`, teamID) } func (s *TaskStore) ListAll(ctx context.Context) ([]models.Task, error) { return s.list(ctx, `ORDER BY created_at DESC`) } func (s *TaskStore) ListDue(ctx context.Context, limit int) ([]models.Task, error) { return s.list(ctx, `WHERE is_active = true AND next_run_at <= NOW() ORDER BY next_run_at ASC LIMIT $1`, limit) } func (s *TaskStore) SetNextRun(ctx context.Context, id string, nextRun interface{}) error { _, err := DB.ExecContext(ctx, `UPDATE tasks SET next_run_at = $1, last_run_at = NOW(), updated_at = NOW() WHERE id = $2`, nextRun, id) return err } func (s *TaskStore) IncrementRunCount(ctx context.Context, id string) error { _, err := DB.ExecContext(ctx, `UPDATE tasks SET run_count = run_count + 1, updated_at = NOW() WHERE id = $1`, id) return err } // ── Run History ───────────────────────────── func (s *TaskStore) CreateRun(ctx context.Context, r *models.TaskRun) error { return DB.QueryRowContext(ctx, ` INSERT INTO task_runs (task_id, channel_id, status) VALUES ($1, $2, $3) RETURNING id, started_at`, r.TaskID, r.ChannelID, r.Status, ).Scan(&r.ID, &r.StartedAt) } func (s *TaskStore) UpdateRun(ctx context.Context, id, status string, tokensUsed, toolCalls, wallClock int, errMsg string) error { _, err := DB.ExecContext(ctx, ` UPDATE task_runs SET status = $1, tokens_used = $2, tool_calls = $3, wall_clock = $4, error = $5, completed_at = NOW() WHERE id = $6`, status, tokensUsed, toolCalls, wallClock, errMsg, id) return err } func (s *TaskStore) GetActiveRun(ctx context.Context, taskID string) (*models.TaskRun, error) { r := &models.TaskRun{} err := DB.QueryRowContext(ctx, ` SELECT id, task_id, channel_id, status, started_at FROM task_runs WHERE task_id = $1 AND status = 'running' LIMIT 1`, taskID, ).Scan(&r.ID, &r.TaskID, &r.ChannelID, &r.Status, &r.StartedAt) if err != nil { return nil, err } return r, nil } func (s *TaskStore) ListRuns(ctx context.Context, taskID string, limit int) ([]models.TaskRun, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, task_id, channel_id, status, started_at, completed_at, tokens_used, tool_calls, wall_clock, COALESCE(error, '') FROM task_runs WHERE task_id = $1 ORDER BY started_at DESC LIMIT $2`, taskID, limit) if err != nil { return nil, err } defer rows.Close() var runs []models.TaskRun for rows.Next() { var r models.TaskRun if err := rows.Scan(&r.ID, &r.TaskID, &r.ChannelID, &r.Status, &r.StartedAt, &r.CompletedAt, &r.TokensUsed, &r.ToolCalls, &r.WallClock, &r.Error); err != nil { continue } runs = append(runs, r) } return runs, nil } // ── Helpers ───────────────────────────────── func (s *TaskStore) list(ctx context.Context, where string, args ...interface{}) ([]models.Task, error) { q := `SELECT id, owner_id, team_id, name, description, scope, task_type, persona_id, model_id, system_prompt, user_prompt, workflow_id, schedule, timezone, is_active, max_tokens, max_tool_calls, max_wall_clock, output_mode, notify_on_complete, notify_on_failure, last_run_at, next_run_at, run_count, created_at, updated_at FROM tasks ` + where rows, err := DB.QueryContext(ctx, q, args...) if err != nil { return nil, err } defer rows.Close() var tasks []models.Task for rows.Next() { var t models.Task if err := rows.Scan(&t.ID, &t.OwnerID, &t.TeamID, &t.Name, &t.Description, &t.Scope, &t.TaskType, &t.PersonaID, &t.ModelID, &t.SystemPrompt, &t.UserPrompt, &t.WorkflowID, &t.Schedule, &t.Timezone, &t.IsActive, &t.MaxTokens, &t.MaxToolCalls, &t.MaxWallClock, &t.OutputMode, &t.NotifyOnComplete, &t.NotifyOnFailure, &t.LastRunAt, &t.NextRunAt, &t.RunCount, &t.CreatedAt, &t.UpdatedAt); err != nil { continue } tasks = append(tasks, t) } return tasks, nil } // comma builds ", column = $N" func comma(i int, col string) string { return ", " + col + " = " + pgArg(i) } // pgArg returns "$N" func pgArg(i int) string { return fmt.Sprintf("$%d", i) } // pgWhere returns " WHERE col = $N" func pgWhere(i int, col string) string { return " WHERE " + col + " = " + pgArg(i) }