698 lines
19 KiB
Go
698 lines
19 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"strconv"
|
|
|
|
"director/api/pubsub"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *sql.DB
|
|
ps *pubsub.PubSub
|
|
}
|
|
|
|
func New(db *sql.DB, ps *pubsub.PubSub) *Handler {
|
|
return &Handler{db: db, ps: ps}
|
|
}
|
|
|
|
func (h *Handler) broadcast(c *gin.Context, event string, data any) {
|
|
h.ps.Publish(c.Request.Context(), event, data)
|
|
}
|
|
|
|
// --- Plans ---
|
|
|
|
func (h *Handler) ListPlans(c *gin.Context) {
|
|
rows, err := h.db.Query(`
|
|
SELECT id, title, description, team, priority, status,
|
|
created_at, updated_at, approved_at, started_at, completed_at
|
|
FROM plans ORDER BY
|
|
CASE priority
|
|
WHEN 'critical' THEN 0
|
|
WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3
|
|
WHEN '4' THEN 4 WHEN '5' THEN 5 WHEN 'backlog' THEN 6
|
|
END,
|
|
created_at DESC
|
|
`)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
plans := []map[string]any{}
|
|
for rows.Next() {
|
|
var id int
|
|
var title, desc, team, priority, status string
|
|
var createdAt, updatedAt string
|
|
var approvedAt, startedAt, completedAt sql.NullString
|
|
rows.Scan(&id, &title, &desc, &team, &priority, &status, &createdAt, &updatedAt, &approvedAt, &startedAt, &completedAt)
|
|
p := map[string]any{
|
|
"id": id, "title": title, "description": desc, "team": team,
|
|
"priority": priority, "status": status,
|
|
"created_at": createdAt, "updated_at": updatedAt,
|
|
}
|
|
if approvedAt.Valid {
|
|
p["approved_at"] = approvedAt.String
|
|
}
|
|
if startedAt.Valid {
|
|
p["started_at"] = startedAt.String
|
|
}
|
|
if completedAt.Valid {
|
|
p["completed_at"] = completedAt.String
|
|
}
|
|
plans = append(plans, p)
|
|
}
|
|
c.JSON(200, plans)
|
|
}
|
|
|
|
func (h *Handler) CreatePlan(c *gin.Context) {
|
|
var input struct {
|
|
Title string `json:"title" binding:"required"`
|
|
Description string `json:"description" binding:"required"`
|
|
Team string `json:"team" binding:"required"`
|
|
Priority string `json:"priority"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if input.Priority == "" {
|
|
input.Priority = "3"
|
|
}
|
|
|
|
var id int
|
|
err := h.db.QueryRow(`
|
|
INSERT INTO plans (title, description, team, priority)
|
|
VALUES ($1, $2, $3, $4) RETURNING id
|
|
`, input.Title, input.Description, input.Team, input.Priority).Scan(&id)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
plan := map[string]any{
|
|
"id": id, "title": input.Title, "description": input.Description,
|
|
"team": input.Team, "priority": input.Priority, "status": "draft",
|
|
}
|
|
h.broadcast(c, "plan:created", plan)
|
|
c.JSON(201, plan)
|
|
}
|
|
|
|
func (h *Handler) GetPlan(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var title, desc, team, priority, status, createdAt, updatedAt string
|
|
var approvedAt, startedAt, completedAt sql.NullString
|
|
err := h.db.QueryRow(`
|
|
SELECT title, description, team, priority, status,
|
|
created_at, updated_at, approved_at, started_at, completed_at
|
|
FROM plans WHERE id = $1
|
|
`, id).Scan(&title, &desc, &team, &priority, &status, &createdAt, &updatedAt, &approvedAt, &startedAt, &completedAt)
|
|
if err == sql.ErrNoRows {
|
|
c.JSON(404, gin.H{"error": "plan not found"})
|
|
return
|
|
}
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
out := map[string]any{
|
|
"id": id, "title": title, "description": desc, "team": team,
|
|
"priority": priority, "status": status,
|
|
"created_at": createdAt, "updated_at": updatedAt,
|
|
}
|
|
if approvedAt.Valid {
|
|
out["approved_at"] = approvedAt.String
|
|
}
|
|
if startedAt.Valid {
|
|
out["started_at"] = startedAt.String
|
|
}
|
|
if completedAt.Valid {
|
|
out["completed_at"] = completedAt.String
|
|
}
|
|
c.JSON(200, out)
|
|
}
|
|
|
|
func (h *Handler) UpdatePlan(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var input map[string]any
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Build dynamic update
|
|
allowed := map[string]bool{"title": true, "description": true, "priority": true, "status": true}
|
|
sets := ""
|
|
args := []any{}
|
|
i := 1
|
|
for k, v := range input {
|
|
if !allowed[k] {
|
|
continue
|
|
}
|
|
if sets != "" {
|
|
sets += ", "
|
|
}
|
|
sets += k + " = $" + strconv.Itoa(i)
|
|
args = append(args, v)
|
|
i++
|
|
}
|
|
if sets == "" {
|
|
c.JSON(400, gin.H{"error": "no valid fields"})
|
|
return
|
|
}
|
|
args = append(args, id)
|
|
_, err := h.db.Exec("UPDATE plans SET "+sets+" WHERE id = $"+strconv.Itoa(i), args...)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
input["id"] = id
|
|
h.broadcast(c, "plan:updated", input)
|
|
c.JSON(200, input)
|
|
}
|
|
|
|
// --- Tasks ---
|
|
|
|
func (h *Handler) ListTasks(c *gin.Context) {
|
|
planID, _ := strconv.Atoi(c.Param("id"))
|
|
rows, err := h.db.Query(`
|
|
SELECT id, title, description, task_type, status, assigned_agent, progress_note, sort_order,
|
|
created_at, updated_at, started_at, completed_at
|
|
FROM tasks WHERE plan_id = $1 ORDER BY sort_order, id
|
|
`, planID)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
tasks := []map[string]any{}
|
|
for rows.Next() {
|
|
var id, sortOrder int
|
|
var title, taskType, status, createdAt, updatedAt string
|
|
var desc, agent, note *string
|
|
var startedAt, completedAt sql.NullString
|
|
rows.Scan(&id, &title, &desc, &taskType, &status, &agent, ¬e, &sortOrder,
|
|
&createdAt, &updatedAt, &startedAt, &completedAt)
|
|
t := map[string]any{
|
|
"id": id, "plan_id": planID, "title": title, "task_type": taskType,
|
|
"status": status, "sort_order": sortOrder,
|
|
"created_at": createdAt, "updated_at": updatedAt,
|
|
}
|
|
if desc != nil {
|
|
t["description"] = *desc
|
|
}
|
|
if agent != nil {
|
|
t["assigned_agent"] = *agent
|
|
}
|
|
if note != nil {
|
|
t["progress_note"] = *note
|
|
}
|
|
if startedAt.Valid {
|
|
t["started_at"] = startedAt.String
|
|
}
|
|
if completedAt.Valid {
|
|
t["completed_at"] = completedAt.String
|
|
}
|
|
tasks = append(tasks, t)
|
|
}
|
|
c.JSON(200, tasks)
|
|
}
|
|
|
|
func (h *Handler) CreateTask(c *gin.Context) {
|
|
planID, _ := strconv.Atoi(c.Param("id"))
|
|
var input struct {
|
|
Title string `json:"title" binding:"required"`
|
|
Description string `json:"description"`
|
|
TaskType string `json:"task_type"`
|
|
AssignedAgent string `json:"assigned_agent"`
|
|
SortOrder int `json:"sort_order"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if input.TaskType == "" {
|
|
input.TaskType = "core"
|
|
}
|
|
|
|
var id int
|
|
err := h.db.QueryRow(`
|
|
INSERT INTO tasks (plan_id, title, description, task_type, assigned_agent, sort_order)
|
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id
|
|
`, planID, input.Title, input.Description, input.TaskType, input.AssignedAgent, input.SortOrder).Scan(&id)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
task := map[string]any{
|
|
"id": id, "plan_id": planID, "title": input.Title,
|
|
"task_type": input.TaskType, "status": "queued",
|
|
}
|
|
h.broadcast(c, "task:created", task)
|
|
c.JSON(201, task)
|
|
}
|
|
|
|
func (h *Handler) UpdateTask(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var input map[string]any
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
allowed := map[string]bool{"status": true, "assigned_agent": true, "progress_note": true, "blockers": true}
|
|
sets := ""
|
|
args := []any{}
|
|
i := 1
|
|
for k, v := range input {
|
|
if !allowed[k] {
|
|
continue
|
|
}
|
|
if sets != "" {
|
|
sets += ", "
|
|
}
|
|
sets += k + " = $" + strconv.Itoa(i)
|
|
args = append(args, v)
|
|
i++
|
|
}
|
|
if sets == "" {
|
|
c.JSON(400, gin.H{"error": "no valid fields"})
|
|
return
|
|
}
|
|
args = append(args, id)
|
|
_, err := h.db.Exec("UPDATE tasks SET "+sets+" WHERE id = $"+strconv.Itoa(i), args...)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
input["id"] = id
|
|
h.broadcast(c, "task:updated", input)
|
|
c.JSON(200, input)
|
|
}
|
|
|
|
// --- Agents ---
|
|
|
|
func (h *Handler) ListAgents(c *gin.Context) {
|
|
rows, err := h.db.Query(`
|
|
SELECT id, name, team, role, model_provider, model_id, gpu_default, status, current_task_id, last_seen_at, specialization
|
|
FROM agents ORDER BY team, role
|
|
`)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
agents := []map[string]any{}
|
|
for rows.Next() {
|
|
var id, name, team, role, provider, model, status string
|
|
var gpu, lastSeen, specialization *string
|
|
var taskID *int
|
|
rows.Scan(&id, &name, &team, &role, &provider, &model, &gpu, &status, &taskID, &lastSeen, &specialization)
|
|
a := map[string]any{
|
|
"id": id, "name": name, "team": team, "role": role,
|
|
"model_provider": provider, "model_id": model, "status": status,
|
|
}
|
|
if gpu != nil {
|
|
a["gpu_default"] = *gpu
|
|
}
|
|
if taskID != nil {
|
|
a["current_task_id"] = *taskID
|
|
}
|
|
if lastSeen != nil {
|
|
a["last_seen_at"] = *lastSeen
|
|
}
|
|
if specialization != nil {
|
|
a["specialization"] = *specialization
|
|
}
|
|
agents = append(agents, a)
|
|
}
|
|
c.JSON(200, agents)
|
|
}
|
|
|
|
func (h *Handler) UpdateAgent(c *gin.Context) {
|
|
id := c.Param("id")
|
|
var input map[string]any
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
allowed := map[string]bool{"status": true, "current_task_id": true}
|
|
sets := ""
|
|
args := []any{}
|
|
i := 1
|
|
for k, v := range input {
|
|
if !allowed[k] {
|
|
continue
|
|
}
|
|
if sets != "" {
|
|
sets += ", "
|
|
}
|
|
sets += k + " = $" + strconv.Itoa(i)
|
|
args = append(args, v)
|
|
i++
|
|
}
|
|
if sets == "" {
|
|
c.JSON(400, gin.H{"error": "no valid fields"})
|
|
return
|
|
}
|
|
args = append(args, id)
|
|
_, err := h.db.Exec("UPDATE agents SET "+sets+", last_seen_at = now() WHERE id = $"+strconv.Itoa(i), args...)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
input["id"] = id
|
|
h.broadcast(c, "agent:updated", input)
|
|
c.JSON(200, input)
|
|
}
|
|
|
|
// --- Director Queue ---
|
|
|
|
func (h *Handler) ListQueue(c *gin.Context) {
|
|
status := c.DefaultQuery("status", "ready_for_approval")
|
|
rows, err := h.db.Query(`
|
|
SELECT dq.id, dq.plan_id, dq.task_id, dq.status, dq.verification_notes,
|
|
dq.director_notes, dq.submitted_at, dq.reviewed_at,
|
|
COALESCE(p.title, ''), COALESCE(t.title, '')
|
|
FROM director_queue dq
|
|
LEFT JOIN plans p ON dq.plan_id = p.id
|
|
LEFT JOIN tasks t ON dq.task_id = t.id
|
|
WHERE dq.status = $1
|
|
ORDER BY dq.submitted_at
|
|
`, status)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []map[string]any{}
|
|
for rows.Next() {
|
|
var id int
|
|
var planID, taskID *int
|
|
var qStatus, planTitle, taskTitle string
|
|
var verNotes, dirNotes *string
|
|
var submittedAt string
|
|
var reviewedAt *string
|
|
rows.Scan(&id, &planID, &taskID, &qStatus, &verNotes, &dirNotes, &submittedAt, &reviewedAt, &planTitle, &taskTitle)
|
|
item := map[string]any{
|
|
"id": id, "status": qStatus, "submitted_at": submittedAt,
|
|
"plan_title": planTitle, "task_title": taskTitle,
|
|
}
|
|
if planID != nil {
|
|
item["plan_id"] = *planID
|
|
}
|
|
if taskID != nil {
|
|
item["task_id"] = *taskID
|
|
}
|
|
if verNotes != nil {
|
|
item["verification_notes"] = *verNotes
|
|
}
|
|
if dirNotes != nil {
|
|
item["director_notes"] = *dirNotes
|
|
}
|
|
if reviewedAt != nil {
|
|
item["reviewed_at"] = *reviewedAt
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
c.JSON(200, items)
|
|
}
|
|
|
|
func (h *Handler) UpdateQueueItem(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var input struct {
|
|
Status string `json:"status" binding:"required"`
|
|
DirectorNotes string `json:"director_notes"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
_, err := h.db.Exec(`
|
|
UPDATE director_queue SET status = $1, director_notes = $2, reviewed_at = now() WHERE id = $3
|
|
`, input.Status, input.DirectorNotes, id)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
result := map[string]any{"id": id, "status": input.Status}
|
|
h.broadcast(c, "queue:updated", result)
|
|
c.JSON(200, result)
|
|
}
|
|
|
|
// --- Pending Questions ---
|
|
|
|
func (h *Handler) CreateQuestion(c *gin.Context) {
|
|
var input struct {
|
|
PlanID *int `json:"plan_id"`
|
|
TaskID *int `json:"task_id"`
|
|
FromRole string `json:"from_role" binding:"required"`
|
|
FromID string `json:"from_id" binding:"required"`
|
|
Question string `json:"question" binding:"required"`
|
|
Context string `json:"context"`
|
|
Priority string `json:"priority"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if input.Priority == "" {
|
|
input.Priority = "3"
|
|
}
|
|
|
|
var id int
|
|
err := h.db.QueryRow(`
|
|
INSERT INTO pending_questions (plan_id, task_id, from_role, from_id, question, context, priority)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id
|
|
`, input.PlanID, input.TaskID, input.FromRole, input.FromID, input.Question, input.Context, input.Priority).Scan(&id)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
q := map[string]any{
|
|
"id": id, "from_role": input.FromRole, "from_id": input.FromID,
|
|
"question": input.Question, "priority": input.Priority, "status": "awaiting_director",
|
|
}
|
|
if input.PlanID != nil {
|
|
q["plan_id"] = *input.PlanID
|
|
}
|
|
if input.TaskID != nil {
|
|
q["task_id"] = *input.TaskID
|
|
}
|
|
if input.Context != "" {
|
|
q["context"] = input.Context
|
|
}
|
|
h.broadcast(c, "question:created", q)
|
|
c.JSON(201, q)
|
|
}
|
|
|
|
func (h *Handler) ListQuestions(c *gin.Context) {
|
|
statusFilter := c.DefaultQuery("status", "awaiting_director")
|
|
rows, err := h.db.Query(`
|
|
SELECT pq.id, pq.plan_id, pq.task_id, pq.from_role, pq.from_id, pq.question, pq.context,
|
|
pq.priority, pq.status, pq.answer, pq.created_at, pq.answered_at,
|
|
COALESCE(p.title,''), COALESCE(t.title,'')
|
|
FROM pending_questions pq
|
|
LEFT JOIN plans p ON pq.plan_id = p.id
|
|
LEFT JOIN tasks t ON pq.task_id = t.id
|
|
WHERE pq.status = $1
|
|
ORDER BY
|
|
CASE pq.priority
|
|
WHEN 'critical' THEN 0
|
|
WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3
|
|
WHEN '4' THEN 4 WHEN '5' THEN 5
|
|
END,
|
|
pq.created_at
|
|
`, statusFilter)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
questions := []map[string]any{}
|
|
for rows.Next() {
|
|
var id int
|
|
var planID, taskID *int
|
|
var fromRole, fromID, question, priority, status, createdAt string
|
|
var ctx, answer, answeredAt *string
|
|
var planTitle, taskTitle string
|
|
rows.Scan(&id, &planID, &taskID, &fromRole, &fromID, &question, &ctx, &priority, &status,
|
|
&answer, &createdAt, &answeredAt, &planTitle, &taskTitle)
|
|
q := map[string]any{
|
|
"id": id, "from_role": fromRole, "from_id": fromID,
|
|
"question": question, "priority": priority, "status": status,
|
|
"created_at": createdAt,
|
|
"plan_title": planTitle, "task_title": taskTitle,
|
|
}
|
|
if planID != nil {
|
|
q["plan_id"] = *planID
|
|
}
|
|
if taskID != nil {
|
|
q["task_id"] = *taskID
|
|
}
|
|
if ctx != nil {
|
|
q["context"] = *ctx
|
|
}
|
|
if answer != nil {
|
|
q["answer"] = *answer
|
|
}
|
|
if answeredAt != nil {
|
|
q["answered_at"] = *answeredAt
|
|
}
|
|
questions = append(questions, q)
|
|
}
|
|
c.JSON(200, questions)
|
|
}
|
|
|
|
func (h *Handler) AnswerQuestion(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var input struct {
|
|
Answer string `json:"answer" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
_, err := h.db.Exec(`
|
|
UPDATE pending_questions SET answer = $1, status = 'answered', answered_at = now() WHERE id = $2
|
|
`, input.Answer, id)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
result := map[string]any{"id": id, "status": "answered"}
|
|
h.broadcast(c, "question:answered", result)
|
|
c.JSON(200, result)
|
|
}
|
|
|
|
// --- Messages ---
|
|
|
|
func (h *Handler) ListMessages(c *gin.Context) {
|
|
planID, _ := strconv.Atoi(c.Param("id"))
|
|
rows, err := h.db.Query(`
|
|
SELECT id, task_id, reply_to_id, from_role, from_id, message, message_type, created_at
|
|
FROM messages WHERE plan_id = $1 ORDER BY created_at
|
|
`, planID)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
messages := []map[string]any{}
|
|
for rows.Next() {
|
|
var id int
|
|
var taskID, replyToID *int
|
|
var fromRole, fromID, message, msgType, createdAt string
|
|
rows.Scan(&id, &taskID, &replyToID, &fromRole, &fromID, &message, &msgType, &createdAt)
|
|
m := map[string]any{
|
|
"id": id, "plan_id": planID, "from_role": fromRole, "from_id": fromID,
|
|
"message": message, "message_type": msgType, "created_at": createdAt,
|
|
}
|
|
if taskID != nil {
|
|
m["task_id"] = *taskID
|
|
}
|
|
if replyToID != nil {
|
|
m["reply_to_id"] = *replyToID
|
|
}
|
|
messages = append(messages, m)
|
|
}
|
|
c.JSON(200, messages)
|
|
}
|
|
|
|
func (h *Handler) CreateMessage(c *gin.Context) {
|
|
planID, _ := strconv.Atoi(c.Param("id"))
|
|
var input struct {
|
|
TaskID *int `json:"task_id"`
|
|
ReplyToID *int `json:"reply_to_id"`
|
|
FromRole string `json:"from_role" binding:"required"`
|
|
FromID string `json:"from_id" binding:"required"`
|
|
Message string `json:"message" binding:"required"`
|
|
MessageType string `json:"message_type"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if input.MessageType == "" {
|
|
input.MessageType = "comment"
|
|
}
|
|
|
|
var id int
|
|
err := h.db.QueryRow(`
|
|
INSERT INTO messages (plan_id, task_id, reply_to_id, from_role, from_id, message, message_type)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id
|
|
`, planID, input.TaskID, input.ReplyToID, input.FromRole, input.FromID, input.Message, input.MessageType).Scan(&id)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
msg := map[string]any{
|
|
"id": id, "plan_id": planID, "from_role": input.FromRole,
|
|
"from_id": input.FromID, "message": input.Message,
|
|
"message_type": input.MessageType,
|
|
}
|
|
if input.TaskID != nil {
|
|
msg["task_id"] = *input.TaskID
|
|
}
|
|
if input.ReplyToID != nil {
|
|
msg["reply_to_id"] = *input.ReplyToID
|
|
}
|
|
h.broadcast(c, "message:created", msg)
|
|
c.JSON(201, msg)
|
|
}
|
|
|
|
func (h *Handler) UpdateMessage(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var input struct {
|
|
Message string `json:"message" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
// Only allow editing director-authored messages — never mutate agent or lead history
|
|
var fromRole string
|
|
err := h.db.QueryRow(`SELECT from_role FROM messages WHERE id = $1`, id).Scan(&fromRole)
|
|
if err == sql.ErrNoRows {
|
|
c.JSON(404, gin.H{"error": "message not found"})
|
|
return
|
|
}
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if fromRole != "director" {
|
|
c.JSON(403, gin.H{"error": "only director-authored messages can be edited"})
|
|
return
|
|
}
|
|
_, err = h.db.Exec(`UPDATE messages SET message = $1 WHERE id = $2`, input.Message, id)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
result := map[string]any{"id": id, "message": input.Message}
|
|
h.broadcast(c, "message:updated", result)
|
|
c.JSON(200, result)
|
|
}
|
|
|
|
func intParam(c *gin.Context, name string) int {
|
|
v, _ := strconv.Atoi(c.Param(name))
|
|
return v
|
|
}
|
|
|
|
func sendError(c *gin.Context, code int, err error) {
|
|
c.JSON(code, gin.H{"error": err.Error()})
|
|
}
|