From 6753bfa3974952003f666995b875b615f6ddfdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sun, 11 Jan 2026 12:33:10 +0000 Subject: [PATCH] feat: add http handlers to backend --- apps/backend/cmd/kandev/main.go | 26 +- .../internal/integration/integration_test.go | 26 +- .../internal/integration/orchestrator_test.go | 48 +-- .../backend/internal/task/controller/board.go | 133 +++++++ apps/backend/internal/task/controller/task.go | 98 +++++ apps/backend/internal/task/dto/dto.go | 135 +++++++ apps/backend/internal/task/dto/requests.go | 92 +++++ .../internal/task/handlers/board_handlers.go | 273 +++++++++++++ apps/backend/internal/task/handlers/errors.go | 26 ++ .../internal/task/handlers/task_handlers.go | 362 ++++++++++++++++++ .../task/wshandlers/board_handlers.go | 184 --------- .../task/wshandlers/column_handlers.go | 153 -------- .../internal/task/wshandlers/handlers.go | 48 --- .../internal/task/wshandlers/task_handlers.go | 283 -------------- 14 files changed, 1174 insertions(+), 713 deletions(-) create mode 100644 apps/backend/internal/task/controller/board.go create mode 100644 apps/backend/internal/task/controller/task.go create mode 100644 apps/backend/internal/task/dto/dto.go create mode 100644 apps/backend/internal/task/dto/requests.go create mode 100644 apps/backend/internal/task/handlers/board_handlers.go create mode 100644 apps/backend/internal/task/handlers/errors.go create mode 100644 apps/backend/internal/task/handlers/task_handlers.go delete mode 100644 apps/backend/internal/task/wshandlers/board_handlers.go delete mode 100644 apps/backend/internal/task/wshandlers/column_handlers.go delete mode 100644 apps/backend/internal/task/wshandlers/handlers.go delete mode 100644 apps/backend/internal/task/wshandlers/task_handlers.go diff --git a/apps/backend/cmd/kandev/main.go b/apps/backend/cmd/kandev/main.go index b91ee96009..3b42a02886 100644 --- a/apps/backend/cmd/kandev/main.go +++ b/apps/backend/cmd/kandev/main.go @@ -1,6 +1,6 @@ // Package main is the unified entry point for Kandev. // This single binary runs all services together with shared infrastructure. -// All communication happens over WebSocket - no REST API. +// The server exposes WebSocket and HTTP endpoints. package main import ( @@ -38,9 +38,10 @@ import ( orchestratorwshandlers "github.com/kandev/kandev/internal/orchestrator/wshandlers" // Task Service packages + taskcontroller "github.com/kandev/kandev/internal/task/controller" + taskhandlers "github.com/kandev/kandev/internal/task/handlers" "github.com/kandev/kandev/internal/task/repository" taskservice "github.com/kandev/kandev/internal/task/service" - taskwshandlers "github.com/kandev/kandev/internal/task/wshandlers" // ACP "github.com/kandev/kandev/internal/orchestrator/acp" @@ -200,10 +201,9 @@ func main() { // Create the unified WebSocket gateway gateway := gateways.NewGateway(log) - // Register WebSocket message handlers for each service - taskWSHandlers := taskwshandlers.NewHandlers(taskSvc, log) - taskWSHandlers.RegisterHandlers(gateway.Dispatcher) - log.Info("Registered Task Service WebSocket handlers") + // Prepare Task Service controllers for HTTP + WebSocket handlers + boardController := taskcontroller.NewBoardController(taskSvc) + taskController := taskcontroller.NewTaskController(taskSvc) orchestratorWSHandlers := orchestratorwshandlers.NewHandlers(orchestratorSvc, log) orchestratorWSHandlers.SetACPHandler(acpHandler) @@ -301,7 +301,7 @@ func main() { log.Info("Orchestrator initialized") // ============================================ - // HTTP SERVER (WebSocket endpoint only) + // HTTP SERVER (WebSocket + HTTP endpoints) // ============================================ if cfg.Logging.Level != "debug" { gin.SetMode(gin.ReleaseMode) @@ -310,15 +310,20 @@ func main() { router.Use(gin.Recovery()) router.Use(corsMiddleware()) - // WebSocket endpoint - the only way to communicate with the backend + // WebSocket endpoint - primary realtime transport gateway.SetupRoutes(router) + // Task Service handlers (HTTP + WebSocket) + taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log) + taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log) + log.Info("Registered Task Service handlers (HTTP + WebSocket)") + // Health check (simple HTTP for load balancers/monitoring) router.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", "service": "kandev", - "mode": "websocket-only", + "mode": "websocket+http", }) }) @@ -343,9 +348,10 @@ func main() { }() // Print routes summary - log.Info("WebSocket-only API configured", + log.Info("API configured", zap.String("websocket", "/ws"), zap.String("health", "/health"), + zap.String("http", "/api/v1"), ) // ============================================ diff --git a/apps/backend/internal/integration/integration_test.go b/apps/backend/internal/integration/integration_test.go index c4b76c123e..d4c750bb9d 100644 --- a/apps/backend/internal/integration/integration_test.go +++ b/apps/backend/internal/integration/integration_test.go @@ -21,9 +21,10 @@ import ( "github.com/kandev/kandev/internal/events/bus" gateways "github.com/kandev/kandev/internal/gateway/websocket" "github.com/kandev/kandev/internal/orchestrator/acp" + taskcontroller "github.com/kandev/kandev/internal/task/controller" + taskhandlers "github.com/kandev/kandev/internal/task/handlers" "github.com/kandev/kandev/internal/task/repository" taskservice "github.com/kandev/kandev/internal/task/service" - taskwshandlers "github.com/kandev/kandev/internal/task/wshandlers" "github.com/kandev/kandev/pkg/acp/protocol" ws "github.com/kandev/kandev/pkg/websocket" ) @@ -65,10 +66,6 @@ func NewTestServer(t *testing.T) *TestServer { // Create WebSocket gateway gateway := gateways.NewGateway(log) - // Register handlers - taskWSHandlers := taskwshandlers.NewHandlers(taskSvc, log) - taskWSHandlers.RegisterHandlers(gateway.Dispatcher) - // Start hub go gateway.Hub.Run(ctx) @@ -77,6 +74,12 @@ func NewTestServer(t *testing.T) *TestServer { router := gin.New() gateway.SetupRoutes(router) + // Register handlers (HTTP + WS) + boardController := taskcontroller.NewBoardController(taskSvc) + taskController := taskcontroller.NewTaskController(taskSvc) + taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log) + taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log) + // Create test server server := httptest.NewServer(router) @@ -291,7 +294,6 @@ func TestBoardCRUD(t *testing.T) { }) } - // ============================================ // COLUMN TESTS // ============================================ @@ -506,7 +508,6 @@ func TestTaskCRUD(t *testing.T) { }) } - // ============================================ // TASK STATE TESTS // ============================================ @@ -747,7 +748,6 @@ func TestMultipleClients(t *testing.T) { assert.Len(t, tasks, 1) } - // ============================================ // ERROR HANDLING TESTS // ============================================ @@ -920,10 +920,6 @@ func NewTestServerWithACP(t *testing.T) *TestServerWithACP { // Create WebSocket gateway gateway := gateways.NewGateway(log) - // Register handlers - taskWSHandlers := taskwshandlers.NewHandlers(taskSvc, log) - taskWSHandlers.RegisterHandlers(gateway.Dispatcher) - // Register a mock handler for task.logs that uses the ACP handler gateway.Dispatcher.RegisterFunc(ws.ActionTaskLogs, func(ctx context.Context, msg *ws.Message) (*ws.Message, error) { var req struct { @@ -1006,6 +1002,12 @@ func NewTestServerWithACP(t *testing.T) *TestServerWithACP { router := gin.New() gateway.SetupRoutes(router) + // Register handlers (HTTP + WS) + boardController := taskcontroller.NewBoardController(taskSvc) + taskController := taskcontroller.NewTaskController(taskSvc) + taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log) + taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log) + // Create test server server := httptest.NewServer(router) diff --git a/apps/backend/internal/integration/orchestrator_test.go b/apps/backend/internal/integration/orchestrator_test.go index 42be91055e..0238c4795a 100644 --- a/apps/backend/internal/integration/orchestrator_test.go +++ b/apps/backend/internal/integration/orchestrator_test.go @@ -27,9 +27,10 @@ import ( "github.com/kandev/kandev/internal/orchestrator" "github.com/kandev/kandev/internal/orchestrator/executor" orchestratorwshandlers "github.com/kandev/kandev/internal/orchestrator/wshandlers" + taskcontroller "github.com/kandev/kandev/internal/task/controller" + taskhandlers "github.com/kandev/kandev/internal/task/handlers" "github.com/kandev/kandev/internal/task/repository" taskservice "github.com/kandev/kandev/internal/task/service" - taskwshandlers "github.com/kandev/kandev/internal/task/wshandlers" "github.com/kandev/kandev/pkg/acp/protocol" v1 "github.com/kandev/kandev/pkg/api/v1" ws "github.com/kandev/kandev/pkg/websocket" @@ -42,17 +43,17 @@ import ( // SimulatedAgentManagerClient simulates agent container behavior for testing. // It publishes realistic agent events (started, ACP messages, completion) to the event bus. type SimulatedAgentManagerClient struct { - eventBus bus.EventBus - logger *logger.Logger - mu sync.Mutex - instances map[string]*simulatedInstance - launchDelay time.Duration - executionTime time.Duration - shouldFail bool - failAfter int // Fail after N successful launches - launchCount int32 - acpMessageFn func(taskID, instanceID string) []protocol.Message // Custom ACP messages - stopCh chan struct{} + eventBus bus.EventBus + logger *logger.Logger + mu sync.Mutex + instances map[string]*simulatedInstance + launchDelay time.Duration + executionTime time.Duration + shouldFail bool + failAfter int // Fail after N successful launches + launchCount int32 + acpMessageFn func(taskID, instanceID string) []protocol.Message // Custom ACP messages + stopCh chan struct{} } // simulatedInstance tracks a simulated agent instance @@ -201,9 +202,9 @@ func (s *SimulatedAgentManagerClient) publishACPMessages(instance *simulatedInst TaskID: instance.taskID, Timestamp: time.Now(), Data: map[string]interface{}{ - "progress": 25, - "stage": "starting", - "message": "Agent started processing task", + "progress": 25, + "stage": "starting", + "message": "Agent started processing task", }, }, { @@ -220,9 +221,9 @@ func (s *SimulatedAgentManagerClient) publishACPMessages(instance *simulatedInst TaskID: instance.taskID, Timestamp: time.Now(), Data: map[string]interface{}{ - "progress": 75, - "stage": "executing", - "message": "Task execution in progress", + "progress": 75, + "stage": "executing", + "message": "Task execution in progress", }, }, } @@ -455,10 +456,6 @@ func NewOrchestratorTestServer(t *testing.T) *OrchestratorTestServer { // Create WebSocket gateway gateway := gateways.NewGateway(log) - // Register task handlers - taskWSHandlers := taskwshandlers.NewHandlers(taskSvc, log) - taskWSHandlers.RegisterHandlers(gateway.Dispatcher) - // Register orchestrator handlers orchestratorWSHandlers := orchestratorwshandlers.NewHandlers(orchestratorSvc, log) orchestratorWSHandlers.RegisterHandlers(gateway.Dispatcher) @@ -499,6 +496,12 @@ func NewOrchestratorTestServer(t *testing.T) *OrchestratorTestServer { router := gin.New() gateway.SetupRoutes(router) + // Register handlers (HTTP + WS) + boardController := taskcontroller.NewBoardController(taskSvc) + taskController := taskcontroller.NewTaskController(taskSvc) + taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log) + taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log) + // Create test server server := httptest.NewServer(router) @@ -1388,4 +1391,3 @@ func TestOrchestratorSessionInfo(t *testing.T) { require.True(t, ok, "auggie_session_id should be present in task metadata") assert.Equal(t, expectedSessionID, storedSessionID, "session ID should match what the agent sent") } - diff --git a/apps/backend/internal/task/controller/board.go b/apps/backend/internal/task/controller/board.go new file mode 100644 index 0000000000..750cf13cba --- /dev/null +++ b/apps/backend/internal/task/controller/board.go @@ -0,0 +1,133 @@ +package controller + +import ( + "context" + + "github.com/kandev/kandev/internal/task/dto" + "github.com/kandev/kandev/internal/task/service" +) + +type BoardController struct { + service *service.Service +} + +func NewBoardController(svc *service.Service) *BoardController { + return &BoardController{service: svc} +} + +func (c *BoardController) ListBoards(ctx context.Context, _ dto.ListBoardsRequest) (dto.ListBoardsResponse, error) { + boards, err := c.service.ListBoards(ctx) + if err != nil { + return dto.ListBoardsResponse{}, err + } + resp := dto.ListBoardsResponse{ + Boards: make([]dto.BoardDTO, 0, len(boards)), + Total: len(boards), + } + for _, board := range boards { + resp.Boards = append(resp.Boards, dto.FromBoard(board)) + } + return resp, nil +} + +func (c *BoardController) GetBoard(ctx context.Context, req dto.GetBoardRequest) (dto.BoardDTO, error) { + board, err := c.service.GetBoard(ctx, req.ID) + if err != nil { + return dto.BoardDTO{}, err + } + return dto.FromBoard(board), nil +} + +func (c *BoardController) CreateBoard(ctx context.Context, req dto.CreateBoardRequest) (dto.BoardDTO, error) { + board, err := c.service.CreateBoard(ctx, &service.CreateBoardRequest{ + Name: req.Name, + Description: req.Description, + OwnerID: req.OwnerID, + }) + if err != nil { + return dto.BoardDTO{}, err + } + return dto.FromBoard(board), nil +} + +func (c *BoardController) UpdateBoard(ctx context.Context, req dto.UpdateBoardRequest) (dto.BoardDTO, error) { + board, err := c.service.UpdateBoard(ctx, req.ID, &service.UpdateBoardRequest{ + Name: req.Name, + Description: req.Description, + }) + if err != nil { + return dto.BoardDTO{}, err + } + return dto.FromBoard(board), nil +} + +func (c *BoardController) DeleteBoard(ctx context.Context, req dto.DeleteBoardRequest) (dto.SuccessResponse, error) { + if err := c.service.DeleteBoard(ctx, req.ID); err != nil { + return dto.SuccessResponse{}, err + } + return dto.SuccessResponse{Success: true}, nil +} + +func (c *BoardController) ListColumns(ctx context.Context, req dto.ListColumnsRequest) (dto.ListColumnsResponse, error) { + columns, err := c.service.ListColumns(ctx, req.BoardID) + if err != nil { + return dto.ListColumnsResponse{}, err + } + resp := dto.ListColumnsResponse{ + Columns: make([]dto.ColumnDTO, 0, len(columns)), + Total: len(columns), + } + for _, column := range columns { + resp.Columns = append(resp.Columns, dto.FromColumn(column)) + } + return resp, nil +} + +func (c *BoardController) GetColumn(ctx context.Context, req dto.GetColumnRequest) (dto.ColumnDTO, error) { + column, err := c.service.GetColumn(ctx, req.ID) + if err != nil { + return dto.ColumnDTO{}, err + } + return dto.FromColumn(column), nil +} + +func (c *BoardController) CreateColumn(ctx context.Context, req dto.CreateColumnRequest) (dto.ColumnDTO, error) { + column, err := c.service.CreateColumn(ctx, &service.CreateColumnRequest{ + BoardID: req.BoardID, + Name: req.Name, + Position: req.Position, + State: req.State, + }) + if err != nil { + return dto.ColumnDTO{}, err + } + return dto.FromColumn(column), nil +} + +func (c *BoardController) GetSnapshot(ctx context.Context, req dto.GetBoardSnapshotRequest) (dto.BoardSnapshotDTO, error) { + board, err := c.service.GetBoard(ctx, req.BoardID) + if err != nil { + return dto.BoardSnapshotDTO{}, err + } + columns, err := c.service.ListColumns(ctx, req.BoardID) + if err != nil { + return dto.BoardSnapshotDTO{}, err + } + tasks, err := c.service.ListTasks(ctx, req.BoardID) + if err != nil { + return dto.BoardSnapshotDTO{}, err + } + + resp := dto.BoardSnapshotDTO{ + Board: dto.FromBoard(board), + Columns: make([]dto.ColumnDTO, 0, len(columns)), + Tasks: make([]dto.TaskDTO, 0, len(tasks)), + } + for _, column := range columns { + resp.Columns = append(resp.Columns, dto.FromColumn(column)) + } + for _, task := range tasks { + resp.Tasks = append(resp.Tasks, dto.FromTask(task)) + } + return resp, nil +} diff --git a/apps/backend/internal/task/controller/task.go b/apps/backend/internal/task/controller/task.go new file mode 100644 index 0000000000..2b85580c5e --- /dev/null +++ b/apps/backend/internal/task/controller/task.go @@ -0,0 +1,98 @@ +package controller + +import ( + "context" + + "github.com/kandev/kandev/internal/task/dto" + "github.com/kandev/kandev/internal/task/service" +) + +type TaskController struct { + service *service.Service +} + +func NewTaskController(svc *service.Service) *TaskController { + return &TaskController{service: svc} +} + +func (c *TaskController) ListTasks(ctx context.Context, req dto.ListTasksRequest) (dto.ListTasksResponse, error) { + tasks, err := c.service.ListTasks(ctx, req.BoardID) + if err != nil { + return dto.ListTasksResponse{}, err + } + resp := dto.ListTasksResponse{ + Tasks: make([]dto.TaskDTO, 0, len(tasks)), + Total: len(tasks), + } + for _, task := range tasks { + resp.Tasks = append(resp.Tasks, dto.FromTask(task)) + } + return resp, nil +} + +func (c *TaskController) GetTask(ctx context.Context, req dto.GetTaskRequest) (dto.TaskDTO, error) { + task, err := c.service.GetTask(ctx, req.ID) + if err != nil { + return dto.TaskDTO{}, err + } + return dto.FromTask(task), nil +} + +func (c *TaskController) CreateTask(ctx context.Context, req dto.CreateTaskRequest) (dto.TaskDTO, error) { + task, err := c.service.CreateTask(ctx, &service.CreateTaskRequest{ + BoardID: req.BoardID, + ColumnID: req.ColumnID, + Title: req.Title, + Description: req.Description, + Priority: req.Priority, + AgentType: req.AgentType, + RepositoryURL: req.RepositoryURL, + Branch: req.Branch, + AssignedTo: req.AssignedTo, + Position: req.Position, + Metadata: req.Metadata, + }) + if err != nil { + return dto.TaskDTO{}, err + } + return dto.FromTask(task), nil +} + +func (c *TaskController) UpdateTask(ctx context.Context, req dto.UpdateTaskRequest) (dto.TaskDTO, error) { + task, err := c.service.UpdateTask(ctx, req.ID, &service.UpdateTaskRequest{ + Title: req.Title, + Description: req.Description, + Priority: req.Priority, + AgentType: req.AgentType, + AssignedTo: req.AssignedTo, + Position: req.Position, + Metadata: req.Metadata, + }) + if err != nil { + return dto.TaskDTO{}, err + } + return dto.FromTask(task), nil +} + +func (c *TaskController) DeleteTask(ctx context.Context, req dto.DeleteTaskRequest) (dto.SuccessResponse, error) { + if err := c.service.DeleteTask(ctx, req.ID); err != nil { + return dto.SuccessResponse{}, err + } + return dto.SuccessResponse{Success: true}, nil +} + +func (c *TaskController) MoveTask(ctx context.Context, req dto.MoveTaskRequest) (dto.TaskDTO, error) { + task, err := c.service.MoveTask(ctx, req.ID, req.ColumnID, req.Position) + if err != nil { + return dto.TaskDTO{}, err + } + return dto.FromTask(task), nil +} + +func (c *TaskController) UpdateTaskState(ctx context.Context, req dto.UpdateTaskStateRequest) (dto.TaskDTO, error) { + task, err := c.service.UpdateTaskState(ctx, req.ID, req.State) + if err != nil { + return dto.TaskDTO{}, err + } + return dto.FromTask(task), nil +} diff --git a/apps/backend/internal/task/dto/dto.go b/apps/backend/internal/task/dto/dto.go new file mode 100644 index 0000000000..c35989419a --- /dev/null +++ b/apps/backend/internal/task/dto/dto.go @@ -0,0 +1,135 @@ +package dto + +import ( + "time" + + "github.com/kandev/kandev/internal/task/models" + v1 "github.com/kandev/kandev/pkg/api/v1" +) + +type BoardDTO struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + OwnerID string `json:"owner_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ColumnDTO struct { + ID string `json:"id"` + BoardID string `json:"board_id"` + Name string `json:"name"` + Position int `json:"position"` + State v1.TaskState `json:"state"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TaskDTO struct { + ID string `json:"id"` + BoardID string `json:"board_id"` + ColumnID string `json:"column_id"` + Title string `json:"title"` + Description string `json:"description"` + State v1.TaskState `json:"state"` + Priority int `json:"priority"` + AgentType *string `json:"agent_type,omitempty"` + RepositoryURL *string `json:"repository_url,omitempty"` + Branch *string `json:"branch,omitempty"` + AssignedAgentID *string `json:"assigned_agent_id,omitempty"` + Position int `json:"position"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type BoardSnapshotDTO struct { + Board BoardDTO `json:"board"` + Columns []ColumnDTO `json:"columns"` + Tasks []TaskDTO `json:"tasks"` +} + +type ListBoardsResponse struct { + Boards []BoardDTO `json:"boards"` + Total int `json:"total"` +} + +type ListColumnsResponse struct { + Columns []ColumnDTO `json:"columns"` + Total int `json:"total"` +} + +type ListTasksResponse struct { + Tasks []TaskDTO `json:"tasks"` + Total int `json:"total"` +} + +type SuccessResponse struct { + Success bool `json:"success"` +} + +func FromBoard(board *models.Board) BoardDTO { + var description *string + if board.Description != "" { + description = &board.Description + } + + return BoardDTO{ + ID: board.ID, + Name: board.Name, + Description: description, + OwnerID: board.OwnerID, + CreatedAt: board.CreatedAt, + UpdatedAt: board.UpdatedAt, + } +} + +func FromColumn(column *models.Column) ColumnDTO { + return ColumnDTO{ + ID: column.ID, + BoardID: column.BoardID, + Name: column.Name, + Position: column.Position, + State: column.State, + CreatedAt: column.CreatedAt, + UpdatedAt: column.UpdatedAt, + } +} + +func FromTask(task *models.Task) TaskDTO { + var agentType *string + if task.AgentType != "" { + agentType = &task.AgentType + } + var repositoryURL *string + if task.RepositoryURL != "" { + repositoryURL = &task.RepositoryURL + } + var branch *string + if task.Branch != "" { + branch = &task.Branch + } + var assignedAgentID *string + if task.AssignedTo != "" { + assignedAgentID = &task.AssignedTo + } + + return TaskDTO{ + ID: task.ID, + BoardID: task.BoardID, + ColumnID: task.ColumnID, + Title: task.Title, + Description: task.Description, + State: task.State, + Priority: task.Priority, + AgentType: agentType, + RepositoryURL: repositoryURL, + Branch: branch, + AssignedAgentID: assignedAgentID, + Position: task.Position, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt, + Metadata: task.Metadata, + } +} diff --git a/apps/backend/internal/task/dto/requests.go b/apps/backend/internal/task/dto/requests.go new file mode 100644 index 0000000000..74f012f732 --- /dev/null +++ b/apps/backend/internal/task/dto/requests.go @@ -0,0 +1,92 @@ +package dto + +import v1 "github.com/kandev/kandev/pkg/api/v1" + +type ListBoardsRequest struct{} + +type GetBoardRequest struct { + ID string +} + +type CreateBoardRequest struct { + Name string + Description string + OwnerID string +} + +type UpdateBoardRequest struct { + ID string + Name *string + Description *string +} + +type DeleteBoardRequest struct { + ID string +} + +type ListColumnsRequest struct { + BoardID string +} + +type GetColumnRequest struct { + ID string +} + +type CreateColumnRequest struct { + BoardID string + Name string + Position int + State v1.TaskState +} + +type GetBoardSnapshotRequest struct { + BoardID string +} + +type ListTasksRequest struct { + BoardID string +} + +type GetTaskRequest struct { + ID string +} + +type CreateTaskRequest struct { + BoardID string + ColumnID string + Title string + Description string + Priority int + AgentType string + RepositoryURL string + Branch string + AssignedTo string + Position int + Metadata map[string]interface{} +} + +type UpdateTaskRequest struct { + ID string + Title *string + Description *string + Priority *int + AgentType *string + AssignedTo *string + Position *int + Metadata map[string]interface{} +} + +type DeleteTaskRequest struct { + ID string +} + +type MoveTaskRequest struct { + ID string + ColumnID string + Position int +} + +type UpdateTaskStateRequest struct { + ID string + State v1.TaskState +} diff --git a/apps/backend/internal/task/handlers/board_handlers.go b/apps/backend/internal/task/handlers/board_handlers.go new file mode 100644 index 0000000000..e399146721 --- /dev/null +++ b/apps/backend/internal/task/handlers/board_handlers.go @@ -0,0 +1,273 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/kandev/kandev/internal/common/logger" + "github.com/kandev/kandev/internal/task/controller" + "github.com/kandev/kandev/internal/task/dto" + v1 "github.com/kandev/kandev/pkg/api/v1" + ws "github.com/kandev/kandev/pkg/websocket" + "go.uber.org/zap" +) + +type BoardHandlers struct { + controller *controller.BoardController + logger *logger.Logger +} + +func NewBoardHandlers(svc *controller.BoardController, log *logger.Logger) *BoardHandlers { + return &BoardHandlers{ + controller: svc, + logger: log.WithFields(zap.String("component", "task-board-handlers")), + } +} + +func RegisterBoardRoutes(router *gin.Engine, dispatcher *ws.Dispatcher, ctrl *controller.BoardController, log *logger.Logger) { + handlers := NewBoardHandlers(ctrl, log) + handlers.registerHTTP(router) + handlers.registerWS(dispatcher) +} + +func (h *BoardHandlers) registerHTTP(router *gin.Engine) { + api := router.Group("/api/v1") + api.GET("/boards", h.httpListBoards) + api.GET("/boards/:id", h.httpGetBoard) + api.GET("/boards/:id/columns", h.httpListColumns) + api.GET("/boards/:id/snapshot", h.httpGetBoardSnapshot) +} + +func (h *BoardHandlers) registerWS(dispatcher *ws.Dispatcher) { + dispatcher.RegisterFunc(ws.ActionBoardList, h.wsListBoards) + dispatcher.RegisterFunc(ws.ActionBoardCreate, h.wsCreateBoard) + dispatcher.RegisterFunc(ws.ActionBoardGet, h.wsGetBoard) + dispatcher.RegisterFunc(ws.ActionBoardUpdate, h.wsUpdateBoard) + dispatcher.RegisterFunc(ws.ActionBoardDelete, h.wsDeleteBoard) + dispatcher.RegisterFunc(ws.ActionColumnList, h.wsListColumns) + dispatcher.RegisterFunc(ws.ActionColumnCreate, h.wsCreateColumn) + dispatcher.RegisterFunc(ws.ActionColumnGet, h.wsGetColumn) +} + +// HTTP handlers + +func (h *BoardHandlers) httpListBoards(c *gin.Context) { + resp, err := h.controller.ListBoards(c.Request.Context(), dto.ListBoardsRequest{}) + if err != nil { + h.logger.Error("failed to list boards", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list boards"}) + return + } + c.JSON(http.StatusOK, resp) +} + +func (h *BoardHandlers) httpGetBoard(c *gin.Context) { + resp, err := h.controller.GetBoard(c.Request.Context(), dto.GetBoardRequest{ID: c.Param("id")}) + if err != nil { + handleNotFound(c, h.logger, err, "board not found") + return + } + c.JSON(http.StatusOK, resp) +} + +func (h *BoardHandlers) httpListColumns(c *gin.Context) { + resp, err := h.controller.ListColumns(c.Request.Context(), dto.ListColumnsRequest{BoardID: c.Param("id")}) + if err != nil { + handleNotFound(c, h.logger, err, "columns not found") + return + } + c.JSON(http.StatusOK, resp) +} + +func (h *BoardHandlers) httpGetBoardSnapshot(c *gin.Context) { + resp, err := h.controller.GetSnapshot(c.Request.Context(), dto.GetBoardSnapshotRequest{BoardID: c.Param("id")}) + if err != nil { + handleNotFound(c, h.logger, err, "board not found") + return + } + c.JSON(http.StatusOK, resp) +} + +// WS handlers + +func (h *BoardHandlers) wsListBoards(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + resp, err := h.controller.ListBoards(ctx, dto.ListBoardsRequest{}) + if err != nil { + h.logger.Error("failed to list boards", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list boards", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsCreateBoardRequest struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` +} + +func (h *BoardHandlers) wsCreateBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsCreateBoardRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.Name == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "name is required", nil) + } + + resp, err := h.controller.CreateBoard(ctx, dto.CreateBoardRequest{ + Name: req.Name, + Description: req.Description, + }) + if err != nil { + h.logger.Error("failed to create board", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create board", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsGetBoardRequest struct { + ID string `json:"id"` +} + +func (h *BoardHandlers) wsGetBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsGetBoardRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + + resp, err := h.controller.GetBoard(ctx, dto.GetBoardRequest{ID: req.ID}) + if err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Board not found", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsUpdateBoardRequest struct { + ID string `json:"id"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` +} + +func (h *BoardHandlers) wsUpdateBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsUpdateBoardRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + + resp, err := h.controller.UpdateBoard(ctx, dto.UpdateBoardRequest{ + ID: req.ID, + Name: req.Name, + Description: req.Description, + }) + if err != nil { + h.logger.Error("failed to update board", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update board", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsDeleteBoardRequest struct { + ID string `json:"id"` +} + +func (h *BoardHandlers) wsDeleteBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsDeleteBoardRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + + resp, err := h.controller.DeleteBoard(ctx, dto.DeleteBoardRequest{ID: req.ID}) + if err != nil { + h.logger.Error("failed to delete board", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to delete board", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsListColumnsRequest struct { + BoardID string `json:"board_id"` +} + +func (h *BoardHandlers) wsListColumns(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsListColumnsRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.BoardID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) + } + + resp, err := h.controller.ListColumns(ctx, dto.ListColumnsRequest{BoardID: req.BoardID}) + if err != nil { + h.logger.Error("failed to list columns", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list columns", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsCreateColumnRequest struct { + BoardID string `json:"board_id"` + Name string `json:"name"` + Position int `json:"position"` + State string `json:"state,omitempty"` +} + +func (h *BoardHandlers) wsCreateColumn(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsCreateColumnRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.BoardID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) + } + if req.Name == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "name is required", nil) + } + + state := v1.TaskState(req.State) + if state == "" { + state = v1.TaskStateTODO + } + + resp, err := h.controller.CreateColumn(ctx, dto.CreateColumnRequest{ + BoardID: req.BoardID, + Name: req.Name, + Position: req.Position, + State: state, + }) + if err != nil { + h.logger.Error("failed to create column", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create column", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsGetColumnRequest struct { + ID string `json:"id"` +} + +func (h *BoardHandlers) wsGetColumn(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsGetColumnRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + + resp, err := h.controller.GetColumn(ctx, dto.GetColumnRequest{ID: req.ID}) + if err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Column not found", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} diff --git a/apps/backend/internal/task/handlers/errors.go b/apps/backend/internal/task/handlers/errors.go new file mode 100644 index 0000000000..b296d6c425 --- /dev/null +++ b/apps/backend/internal/task/handlers/errors.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/kandev/kandev/internal/common/logger" + "go.uber.org/zap" +) + +func handleNotFound(c *gin.Context, log *logger.Logger, err error, fallback string) { + if isNotFound(err) { + c.JSON(http.StatusNotFound, gin.H{"error": fallback}) + return + } + log.Error("request failed", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "request failed"}) +} + +func isNotFound(err error) bool { + if err == nil { + return false + } + return strings.Contains(strings.ToLower(err.Error()), "not found") +} diff --git a/apps/backend/internal/task/handlers/task_handlers.go b/apps/backend/internal/task/handlers/task_handlers.go new file mode 100644 index 0000000000..d4d843e2d7 --- /dev/null +++ b/apps/backend/internal/task/handlers/task_handlers.go @@ -0,0 +1,362 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/kandev/kandev/internal/common/logger" + "github.com/kandev/kandev/internal/task/controller" + "github.com/kandev/kandev/internal/task/dto" + v1 "github.com/kandev/kandev/pkg/api/v1" + ws "github.com/kandev/kandev/pkg/websocket" + "go.uber.org/zap" +) + +type TaskHandlers struct { + controller *controller.TaskController + logger *logger.Logger +} + +func NewTaskHandlers(ctrl *controller.TaskController, log *logger.Logger) *TaskHandlers { + return &TaskHandlers{ + controller: ctrl, + logger: log.WithFields(zap.String("component", "task-task-handlers")), + } +} + +func RegisterTaskRoutes(router *gin.Engine, dispatcher *ws.Dispatcher, ctrl *controller.TaskController, log *logger.Logger) { + handlers := NewTaskHandlers(ctrl, log) + handlers.registerHTTP(router) + handlers.registerWS(dispatcher) +} + +func (h *TaskHandlers) registerHTTP(router *gin.Engine) { + api := router.Group("/api/v1") + api.GET("/boards/:id/tasks", h.httpListTasks) + api.GET("/tasks/:id", h.httpGetTask) + api.POST("/tasks", h.httpCreateTask) + api.PATCH("/tasks/:id", h.httpUpdateTask) + api.DELETE("/tasks/:id", h.httpDeleteTask) +} + +func (h *TaskHandlers) registerWS(dispatcher *ws.Dispatcher) { + dispatcher.RegisterFunc(ws.ActionTaskList, h.wsListTasks) + dispatcher.RegisterFunc(ws.ActionTaskCreate, h.wsCreateTask) + dispatcher.RegisterFunc(ws.ActionTaskGet, h.wsGetTask) + dispatcher.RegisterFunc(ws.ActionTaskUpdate, h.wsUpdateTask) + dispatcher.RegisterFunc(ws.ActionTaskDelete, h.wsDeleteTask) + dispatcher.RegisterFunc(ws.ActionTaskMove, h.wsMoveTask) + dispatcher.RegisterFunc(ws.ActionTaskState, h.wsUpdateTaskState) +} + +// HTTP handlers + +func (h *TaskHandlers) httpListTasks(c *gin.Context) { + resp, err := h.controller.ListTasks(c.Request.Context(), dto.ListTasksRequest{BoardID: c.Param("id")}) + if err != nil { + handleNotFound(c, h.logger, err, "tasks not found") + return + } + c.JSON(http.StatusOK, resp) +} + +func (h *TaskHandlers) httpGetTask(c *gin.Context) { + resp, err := h.controller.GetTask(c.Request.Context(), dto.GetTaskRequest{ID: c.Param("id")}) + if err != nil { + handleNotFound(c, h.logger, err, "task not found") + return + } + c.JSON(http.StatusOK, resp) +} + +type httpCreateTaskRequest struct { + BoardID string `json:"board_id"` + ColumnID string `json:"column_id"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + Priority int `json:"priority,omitempty"` + AgentType string `json:"agent_type,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + Branch string `json:"branch,omitempty"` + AssignedTo string `json:"assigned_to,omitempty"` + Position int `json:"position,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +func (h *TaskHandlers) httpCreateTask(c *gin.Context) { + var body httpCreateTaskRequest + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + resp, err := h.controller.CreateTask(c.Request.Context(), dto.CreateTaskRequest{ + BoardID: body.BoardID, + ColumnID: body.ColumnID, + Title: body.Title, + Description: body.Description, + Priority: body.Priority, + AgentType: body.AgentType, + RepositoryURL: body.RepositoryURL, + Branch: body.Branch, + AssignedTo: body.AssignedTo, + Position: body.Position, + Metadata: body.Metadata, + }) + if err != nil { + handleNotFound(c, h.logger, err, "task not created") + return + } + c.JSON(http.StatusOK, resp) +} + +type httpUpdateTaskRequest struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Priority *int `json:"priority,omitempty"` + AgentType *string `json:"agent_type,omitempty"` + AssignedTo *string `json:"assigned_to,omitempty"` + Position *int `json:"position,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +func (h *TaskHandlers) httpUpdateTask(c *gin.Context) { + var body httpUpdateTaskRequest + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + resp, err := h.controller.UpdateTask(c.Request.Context(), dto.UpdateTaskRequest{ + ID: c.Param("id"), + Title: body.Title, + Description: body.Description, + Priority: body.Priority, + AgentType: body.AgentType, + AssignedTo: body.AssignedTo, + Position: body.Position, + Metadata: body.Metadata, + }) + if err != nil { + handleNotFound(c, h.logger, err, "task not updated") + return + } + c.JSON(http.StatusOK, resp) +} + +func (h *TaskHandlers) httpDeleteTask(c *gin.Context) { + resp, err := h.controller.DeleteTask(c.Request.Context(), dto.DeleteTaskRequest{ID: c.Param("id")}) + if err != nil { + handleNotFound(c, h.logger, err, "task not deleted") + return + } + c.JSON(http.StatusOK, resp) +} + +// WS handlers + +type wsListTasksRequest struct { + BoardID string `json:"board_id"` +} + +func (h *TaskHandlers) wsListTasks(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsListTasksRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.BoardID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) + } + + resp, err := h.controller.ListTasks(ctx, dto.ListTasksRequest{BoardID: req.BoardID}) + if err != nil { + h.logger.Error("failed to list tasks", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list tasks", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsCreateTaskRequest struct { + BoardID string `json:"board_id"` + ColumnID string `json:"column_id"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + Priority int `json:"priority,omitempty"` + AgentType string `json:"agent_type,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + Branch string `json:"branch,omitempty"` + AssignedTo string `json:"assigned_to,omitempty"` + Position int `json:"position,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +func (h *TaskHandlers) wsCreateTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsCreateTaskRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.BoardID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) + } + if req.ColumnID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "column_id is required", nil) + } + if req.Title == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "title is required", nil) + } + + resp, err := h.controller.CreateTask(ctx, dto.CreateTaskRequest{ + BoardID: req.BoardID, + ColumnID: req.ColumnID, + Title: req.Title, + Description: req.Description, + Priority: req.Priority, + AgentType: req.AgentType, + RepositoryURL: req.RepositoryURL, + Branch: req.Branch, + AssignedTo: req.AssignedTo, + Position: req.Position, + Metadata: req.Metadata, + }) + if err != nil { + h.logger.Error("failed to create task", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create task", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsGetTaskRequest struct { + ID string `json:"id"` +} + +func (h *TaskHandlers) wsGetTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsGetTaskRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + + resp, err := h.controller.GetTask(ctx, dto.GetTaskRequest{ID: req.ID}) + if err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Task not found", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsUpdateTaskRequest struct { + ID string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Priority *int `json:"priority,omitempty"` + AgentType *string `json:"agent_type,omitempty"` + AssignedTo *string `json:"assigned_to,omitempty"` + Position *int `json:"position,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +func (h *TaskHandlers) wsUpdateTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsUpdateTaskRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + + resp, err := h.controller.UpdateTask(ctx, dto.UpdateTaskRequest{ + ID: req.ID, + Title: req.Title, + Description: req.Description, + Priority: req.Priority, + AgentType: req.AgentType, + AssignedTo: req.AssignedTo, + Position: req.Position, + Metadata: req.Metadata, + }) + if err != nil { + h.logger.Error("failed to update task", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update task", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsDeleteTaskRequest struct { + ID string `json:"id"` +} + +func (h *TaskHandlers) wsDeleteTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsDeleteTaskRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + + resp, err := h.controller.DeleteTask(ctx, dto.DeleteTaskRequest{ID: req.ID}) + if err != nil { + h.logger.Error("failed to delete task", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to delete task", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsMoveTaskRequest struct { + ID string `json:"id"` + ColumnID string `json:"column_id"` + Position int `json:"position"` +} + +func (h *TaskHandlers) wsMoveTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsMoveTaskRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + if req.ColumnID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "column_id is required", nil) + } + + resp, err := h.controller.MoveTask(ctx, dto.MoveTaskRequest{ + ID: req.ID, + ColumnID: req.ColumnID, + Position: req.Position, + }) + if err != nil { + h.logger.Error("failed to move task", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to move task", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} + +type wsUpdateTaskStateRequest struct { + ID string `json:"id"` + State string `json:"state"` +} + +func (h *TaskHandlers) wsUpdateTaskState(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + var req wsUpdateTaskStateRequest + if err := msg.ParsePayload(&req); err != nil { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) + } + if req.ID == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) + } + if req.State == "" { + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "state is required", nil) + } + + resp, err := h.controller.UpdateTaskState(ctx, dto.UpdateTaskStateRequest{ + ID: req.ID, + State: v1.TaskState(req.State), + }) + if err != nil { + h.logger.Error("failed to update task state", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update task state", nil) + } + return ws.NewResponse(msg.ID, msg.Action, resp) +} diff --git a/apps/backend/internal/task/wshandlers/board_handlers.go b/apps/backend/internal/task/wshandlers/board_handlers.go deleted file mode 100644 index 97012791a9..0000000000 --- a/apps/backend/internal/task/wshandlers/board_handlers.go +++ /dev/null @@ -1,184 +0,0 @@ -package wshandlers - -import ( - "context" - - "github.com/kandev/kandev/internal/task/service" - ws "github.com/kandev/kandev/pkg/websocket" - "go.uber.org/zap" -) - -// CreateBoardRequest is the payload for board.create -type CreateBoardRequest struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` -} - -// BoardResponse is the response for board operations -type BoardResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description,omitempty"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -// ListBoardsResponse is the response for board.list -type ListBoardsResponse struct { - Boards []BoardResponse `json:"boards"` - Total int `json:"total"` -} - -// CreateBoard handles board.create action -func (h *Handlers) CreateBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req CreateBoardRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.Name == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "name is required", nil) - } - - svcReq := &service.CreateBoardRequest{ - Name: req.Name, - Description: req.Description, - } - - board, err := h.service.CreateBoard(ctx, svcReq) - if err != nil { - h.logger.Error("failed to create board", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create board", nil) - } - - resp := BoardResponse{ - ID: board.ID, - Name: board.Name, - Description: board.Description, - CreatedAt: board.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: board.UpdatedAt.Format("2006-01-02T15:04:05Z"), - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - -// ListBoards handles board.list action -func (h *Handlers) ListBoards(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - boards, err := h.service.ListBoards(ctx) - if err != nil { - h.logger.Error("failed to list boards", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list boards", nil) - } - - resp := ListBoardsResponse{ - Boards: make([]BoardResponse, len(boards)), - Total: len(boards), - } - for i, b := range boards { - resp.Boards[i] = BoardResponse{ - ID: b.ID, - Name: b.Name, - Description: b.Description, - CreatedAt: b.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: b.UpdatedAt.Format("2006-01-02T15:04:05Z"), - } - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - -// GetBoardRequest is the payload for board.get -type GetBoardRequest struct { - ID string `json:"id"` -} - -// GetBoard handles board.get action -func (h *Handlers) GetBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req GetBoardRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - - board, err := h.service.GetBoard(ctx, req.ID) - if err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Board not found", nil) - } - - resp := BoardResponse{ - ID: board.ID, - Name: board.Name, - Description: board.Description, - CreatedAt: board.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: board.UpdatedAt.Format("2006-01-02T15:04:05Z"), - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - -// UpdateBoardRequest is the payload for board.update -type UpdateBoardRequest struct { - ID string `json:"id"` - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` -} - -// UpdateBoard handles board.update action -func (h *Handlers) UpdateBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req UpdateBoardRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - - svcReq := &service.UpdateBoardRequest{ - Name: req.Name, - Description: req.Description, - } - - board, err := h.service.UpdateBoard(ctx, req.ID, svcReq) - if err != nil { - h.logger.Error("failed to update board", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update board", nil) - } - - resp := BoardResponse{ - ID: board.ID, - Name: board.Name, - Description: board.Description, - CreatedAt: board.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: board.UpdatedAt.Format("2006-01-02T15:04:05Z"), - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - -// DeleteBoardRequest is the payload for board.delete -type DeleteBoardRequest struct { - ID string `json:"id"` -} - -// DeleteBoard handles board.delete action -func (h *Handlers) DeleteBoard(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req DeleteBoardRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - - if err := h.service.DeleteBoard(ctx, req.ID); err != nil { - h.logger.Error("failed to delete board", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to delete board", nil) - } - - return ws.NewResponse(msg.ID, msg.Action, map[string]bool{"success": true}) -} diff --git a/apps/backend/internal/task/wshandlers/column_handlers.go b/apps/backend/internal/task/wshandlers/column_handlers.go deleted file mode 100644 index 4e214c1c36..0000000000 --- a/apps/backend/internal/task/wshandlers/column_handlers.go +++ /dev/null @@ -1,153 +0,0 @@ -package wshandlers - -import ( - "context" - - "github.com/kandev/kandev/internal/task/service" - v1 "github.com/kandev/kandev/pkg/api/v1" - ws "github.com/kandev/kandev/pkg/websocket" - "go.uber.org/zap" -) - -// CreateColumnRequest is the payload for column.create -type CreateColumnRequest struct { - BoardID string `json:"board_id"` - Name string `json:"name"` - Position int `json:"position"` - State string `json:"state,omitempty"` -} - -// ColumnResponse is the response for column operations -type ColumnResponse struct { - ID string `json:"id"` - BoardID string `json:"board_id"` - Name string `json:"name"` - Position int `json:"position"` - State string `json:"state"` - CreatedAt string `json:"created_at"` -} - -// ListColumnsResponse is the response for column.list -type ListColumnsResponse struct { - Columns []ColumnResponse `json:"columns"` - Total int `json:"total"` -} - -// ListColumnsRequest is the payload for column.list -type ListColumnsRequest struct { - BoardID string `json:"board_id"` -} - -// ListColumns handles column.list action -func (h *Handlers) ListColumns(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req ListColumnsRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.BoardID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) - } - - columns, err := h.service.ListColumns(ctx, req.BoardID) - if err != nil { - h.logger.Error("failed to list columns", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list columns", nil) - } - - resp := ListColumnsResponse{ - Columns: make([]ColumnResponse, len(columns)), - Total: len(columns), - } - for i, c := range columns { - resp.Columns[i] = ColumnResponse{ - ID: c.ID, - BoardID: c.BoardID, - Name: c.Name, - Position: c.Position, - State: string(c.State), - CreatedAt: c.CreatedAt.Format("2006-01-02T15:04:05Z"), - } - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - -// CreateColumn handles column.create action -func (h *Handlers) CreateColumn(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req CreateColumnRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.BoardID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) - } - if req.Name == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "name is required", nil) - } - - // Default state to TODO if not specified - state := v1.TaskState(req.State) - if state == "" { - state = v1.TaskStateTODO - } - - svcReq := &service.CreateColumnRequest{ - BoardID: req.BoardID, - Name: req.Name, - Position: req.Position, - State: state, - } - - column, err := h.service.CreateColumn(ctx, svcReq) - if err != nil { - h.logger.Error("failed to create column", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create column", nil) - } - - resp := ColumnResponse{ - ID: column.ID, - BoardID: column.BoardID, - Name: column.Name, - Position: column.Position, - State: string(column.State), - CreatedAt: column.CreatedAt.Format("2006-01-02T15:04:05Z"), - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - -// GetColumnRequest is the payload for column.get -type GetColumnRequest struct { - ID string `json:"id"` -} - -// GetColumn handles column.get action -func (h *Handlers) GetColumn(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req GetColumnRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - - column, err := h.service.GetColumn(ctx, req.ID) - if err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Column not found", nil) - } - - resp := ColumnResponse{ - ID: column.ID, - BoardID: column.BoardID, - Name: column.Name, - Position: column.Position, - State: string(column.State), - CreatedAt: column.CreatedAt.Format("2006-01-02T15:04:05Z"), - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - diff --git a/apps/backend/internal/task/wshandlers/handlers.go b/apps/backend/internal/task/wshandlers/handlers.go deleted file mode 100644 index a5ea0ff6b7..0000000000 --- a/apps/backend/internal/task/wshandlers/handlers.go +++ /dev/null @@ -1,48 +0,0 @@ -// Package wshandlers provides WebSocket message handlers for the task service. -package wshandlers - -import ( - "github.com/kandev/kandev/internal/common/logger" - "github.com/kandev/kandev/internal/task/service" - ws "github.com/kandev/kandev/pkg/websocket" - "go.uber.org/zap" -) - -// Handlers contains WebSocket handlers for the task API -type Handlers struct { - service *service.Service - logger *logger.Logger -} - -// NewHandlers creates a new WebSocket handlers instance -func NewHandlers(svc *service.Service, log *logger.Logger) *Handlers { - return &Handlers{ - service: svc, - logger: log.WithFields(zap.String("component", "task-ws-handlers")), - } -} - -// RegisterHandlers registers all task handlers with the dispatcher -func (h *Handlers) RegisterHandlers(d *ws.Dispatcher) { - // Board handlers - d.RegisterFunc(ws.ActionBoardList, h.ListBoards) - d.RegisterFunc(ws.ActionBoardCreate, h.CreateBoard) - d.RegisterFunc(ws.ActionBoardGet, h.GetBoard) - d.RegisterFunc(ws.ActionBoardUpdate, h.UpdateBoard) - d.RegisterFunc(ws.ActionBoardDelete, h.DeleteBoard) - - // Column handlers - d.RegisterFunc(ws.ActionColumnList, h.ListColumns) - d.RegisterFunc(ws.ActionColumnCreate, h.CreateColumn) - d.RegisterFunc(ws.ActionColumnGet, h.GetColumn) - - // Task handlers - d.RegisterFunc(ws.ActionTaskList, h.ListTasks) - d.RegisterFunc(ws.ActionTaskCreate, h.CreateTask) - d.RegisterFunc(ws.ActionTaskGet, h.GetTask) - d.RegisterFunc(ws.ActionTaskUpdate, h.UpdateTask) - d.RegisterFunc(ws.ActionTaskDelete, h.DeleteTask) - d.RegisterFunc(ws.ActionTaskMove, h.MoveTask) - d.RegisterFunc(ws.ActionTaskState, h.UpdateTaskState) -} - diff --git a/apps/backend/internal/task/wshandlers/task_handlers.go b/apps/backend/internal/task/wshandlers/task_handlers.go deleted file mode 100644 index 1b226632a3..0000000000 --- a/apps/backend/internal/task/wshandlers/task_handlers.go +++ /dev/null @@ -1,283 +0,0 @@ -package wshandlers - -import ( - "context" - - "github.com/kandev/kandev/internal/task/models" - "github.com/kandev/kandev/internal/task/service" - v1 "github.com/kandev/kandev/pkg/api/v1" - ws "github.com/kandev/kandev/pkg/websocket" - "go.uber.org/zap" -) - -// CreateTaskRequest is the payload for task.create -type CreateTaskRequest struct { - BoardID string `json:"board_id"` - ColumnID string `json:"column_id"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - Priority int `json:"priority,omitempty"` - AgentType string `json:"agent_type,omitempty"` - RepositoryURL string `json:"repository_url,omitempty"` - Branch string `json:"branch,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} - -// TaskResponse is the response for task operations -type TaskResponse struct { - ID string `json:"id"` - BoardID string `json:"board_id"` - ColumnID string `json:"column_id"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - State string `json:"state"` - Priority int `json:"priority"` - AgentType string `json:"agent_type,omitempty"` - RepositoryURL string `json:"repository_url,omitempty"` - Branch string `json:"branch,omitempty"` - Position int `json:"position"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} - -// ListTasksRequest is the payload for task.list -type ListTasksRequest struct { - BoardID string `json:"board_id"` -} - -// ListTasksResponse is the response for task.list -type ListTasksResponse struct { - Tasks []TaskResponse `json:"tasks"` - Total int `json:"total"` -} - -// CreateTask handles task.create action -func (h *Handlers) CreateTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req CreateTaskRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.BoardID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) - } - if req.ColumnID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "column_id is required", nil) - } - if req.Title == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "title is required", nil) - } - - svcReq := &service.CreateTaskRequest{ - BoardID: req.BoardID, - ColumnID: req.ColumnID, - Title: req.Title, - Description: req.Description, - Priority: req.Priority, - AgentType: req.AgentType, - RepositoryURL: req.RepositoryURL, - Branch: req.Branch, - Metadata: req.Metadata, - } - - task, err := h.service.CreateTask(ctx, svcReq) - if err != nil { - h.logger.Error("failed to create task", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create task", nil) - } - - return ws.NewResponse(msg.ID, msg.Action, taskToResponse(task)) -} - -// ListTasks handles task.list action -func (h *Handlers) ListTasks(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req ListTasksRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.BoardID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil) - } - - tasks, err := h.service.ListTasks(ctx, req.BoardID) - if err != nil { - h.logger.Error("failed to list tasks", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list tasks", nil) - } - - resp := ListTasksResponse{ - Tasks: make([]TaskResponse, len(tasks)), - Total: len(tasks), - } - for i, t := range tasks { - resp.Tasks[i] = taskToResponse(t) - } - - return ws.NewResponse(msg.ID, msg.Action, resp) -} - -// GetTaskRequest is the payload for task.get -type GetTaskRequest struct { - ID string `json:"id"` -} - -// GetTask handles task.get action -func (h *Handlers) GetTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req GetTaskRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - - task, err := h.service.GetTask(ctx, req.ID) - if err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Task not found", nil) - } - - return ws.NewResponse(msg.ID, msg.Action, taskToResponse(task)) -} - -// UpdateTaskRequest is the payload for task.update -type UpdateTaskRequest struct { - ID string `json:"id"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - Priority *int `json:"priority,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} - -// UpdateTask handles task.update action -func (h *Handlers) UpdateTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req UpdateTaskRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - - svcReq := &service.UpdateTaskRequest{ - Title: req.Title, - Description: req.Description, - Priority: req.Priority, - Metadata: req.Metadata, - } - - task, err := h.service.UpdateTask(ctx, req.ID, svcReq) - if err != nil { - h.logger.Error("failed to update task", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update task", nil) - } - - return ws.NewResponse(msg.ID, msg.Action, taskToResponse(task)) -} - -// DeleteTaskRequest is the payload for task.delete -type DeleteTaskRequest struct { - ID string `json:"id"` -} - -// DeleteTask handles task.delete action -func (h *Handlers) DeleteTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req DeleteTaskRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - - if err := h.service.DeleteTask(ctx, req.ID); err != nil { - h.logger.Error("failed to delete task", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to delete task", nil) - } - - return ws.NewResponse(msg.ID, msg.Action, map[string]bool{"success": true}) -} - -// MoveTaskRequest is the payload for task.move -type MoveTaskRequest struct { - ID string `json:"id"` - ColumnID string `json:"column_id"` - Position int `json:"position"` -} - -// MoveTask handles task.move action -func (h *Handlers) MoveTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req MoveTaskRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - if req.ColumnID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "column_id is required", nil) - } - - task, err := h.service.MoveTask(ctx, req.ID, req.ColumnID, req.Position) - if err != nil { - h.logger.Error("failed to move task", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to move task", nil) - } - - return ws.NewResponse(msg.ID, msg.Action, taskToResponse(task)) -} - -// UpdateTaskStateRequest is the payload for task.state -type UpdateTaskStateRequest struct { - ID string `json:"id"` - State string `json:"state"` -} - -// UpdateTaskState handles task.state action -func (h *Handlers) UpdateTaskState(ctx context.Context, msg *ws.Message) (*ws.Message, error) { - var req UpdateTaskStateRequest - if err := msg.ParsePayload(&req); err != nil { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil) - } - - if req.ID == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil) - } - if req.State == "" { - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "state is required", nil) - } - - task, err := h.service.UpdateTaskState(ctx, req.ID, v1.TaskState(req.State)) - if err != nil { - h.logger.Error("failed to update task state", zap.Error(err)) - return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update task state", nil) - } - - return ws.NewResponse(msg.ID, msg.Action, taskToResponse(task)) -} - -// taskToResponse converts a task model to response -func taskToResponse(t *models.Task) TaskResponse { - return TaskResponse{ - ID: t.ID, - BoardID: t.BoardID, - ColumnID: t.ColumnID, - Title: t.Title, - Description: t.Description, - State: string(t.State), - Priority: t.Priority, - AgentType: t.AgentType, - RepositoryURL: t.RepositoryURL, - Branch: t.Branch, - Position: t.Position, - CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z"), - Metadata: t.Metadata, - } -} -