Files
kandev/apps/backend/internal/office/dashboard/handler_comments.go
T
2026-05-16 20:07:57 +01:00

110 lines
3.3 KiB
Go

package dashboard
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/kandev/kandev/internal/office/models"
"github.com/kandev/kandev/internal/office/repository/sqlite"
"go.uber.org/zap"
)
// -- Comments --
//
// Split out of handler.go so that file stays under revive's
// file-length-limit. All handlers below hang off *Handler and share its
// service, logger, and DTO shapes (CommentDTO, CreateCommentRequest, etc.,
// declared in dto.go).
func (h *Handler) listComments(c *gin.Context) {
ctx := c.Request.Context()
comments, err := h.svc.ListComments(ctx, c.Param("id"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
runByComment := h.fetchRunStatusForComments(ctx, comments)
dtos := make([]*CommentDTO, len(comments))
for i, cm := range comments {
dto := commentToDTO(cm)
if r, ok := runByComment[cm.ID]; ok {
dto.RunID = r.RunID
dto.RunStatus = r.Status
dto.RunError = r.ErrorMessage
}
dtos[i] = dto
}
c.JSON(http.StatusOK, CommentListResponse{Comments: dtos})
}
// fetchRunStatusForComments batches a per-comment run-status lookup.
// Returns an empty map when no comments are passed or the lookup
// fails — the handler degrades to the legacy run-less DTO shape so
// the comments list never errors on a missing index/table.
func (h *Handler) fetchRunStatusForComments(
ctx context.Context, comments []*models.TaskComment,
) map[string]sqlite.CommentRunStatus {
if len(comments) == 0 {
return map[string]sqlite.CommentRunStatus{}
}
ids := make([]string, len(comments))
for i, cm := range comments {
ids[i] = cm.ID
}
runs, err := h.svc.GetRunsByCommentIDs(ctx, ids)
if err != nil {
h.logger.Warn("fetch run status for comments failed", zap.Error(err))
return map[string]sqlite.CommentRunStatus{}
}
return runs
}
func (h *Handler) createComment(c *gin.Context) {
var req CreateCommentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Body == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "body is required"})
return
}
authorType := req.AuthorType
if authorType == "" {
authorType = userSentinel
}
comment := &models.TaskComment{
ID: uuid.New().String(),
TaskID: c.Param("id"),
AuthorType: authorType,
AuthorID: userSentinel,
Body: req.Body,
Source: userSentinel,
}
if err := h.svc.CreateComment(c.Request.Context(), comment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, CommentResponse{Comment: commentToDTO(comment)})
}
func commentToDTO(cm *models.TaskComment) *CommentDTO {
return &CommentDTO{
ID: cm.ID,
TaskID: cm.TaskID,
AuthorType: cm.AuthorType,
AuthorID: cm.AuthorID,
Body: cm.Body,
Source: cm.Source,
// RFC3339Nano keeps sub-second precision so per-comment turn windows
// in the UI can correctly include the agent message that triggered
// the bridge — both timestamps are written within the same second
// in office sessions, so seconds-only formatting collapses the
// agent_message > comment ordering and excludes the reply.
CreatedAt: cm.CreatedAt.UTC().Format(time.RFC3339Nano),
}
}