feat: add http handlers to backend
This commit is contained in:
@@ -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"),
|
||||
)
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user