110 lines
3.3 KiB
Go
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),
|
|
}
|
|
}
|