A per-DataDir background process owns ticket JSON, history, and sync so multiple TUI instances stay consistent; MCP token APIs are ready for phase 2. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
5.4 KiB
Go
215 lines
5.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"task/internal/command"
|
|
"task/internal/ticket"
|
|
)
|
|
|
|
func (s *Server) handleListTime(w http.ResponseWriter, r *http.Request) {
|
|
list, err := s.timeStore.List()
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if ClientKind(r.Context()) == ClientMCP {
|
|
pid := MCPProjectID(r.Context())
|
|
filtered := make([]ticket.TimeEntry, 0)
|
|
for _, e := range list {
|
|
if e.ProjectID == pid {
|
|
filtered = append(filtered, e)
|
|
}
|
|
}
|
|
list = filtered
|
|
}
|
|
writeJSON(w, http.StatusOK, list)
|
|
}
|
|
|
|
func (s *Server) handleListTimeByTicket(w http.ResponseWriter, r *http.Request) {
|
|
ticketID := r.PathValue("ticketId")
|
|
t, err := s.store.Get(ticketID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
if !requireMCPProject(r, t.ProjectID) {
|
|
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
|
return
|
|
}
|
|
list, err := s.timeStore.ListByTicket(ticketID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, list)
|
|
}
|
|
|
|
func (s *Server) handleSumTime(w http.ResponseWriter, r *http.Request) {
|
|
ticketID := r.PathValue("ticketId")
|
|
t, err := s.store.Get(ticketID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
if !requireMCPProject(r, t.ProjectID) {
|
|
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
|
return
|
|
}
|
|
d, err := s.timeStore.SumByTicket(ticketID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]int64{"duration": int64(d)})
|
|
}
|
|
|
|
func (s *Server) handleAddTime(w http.ResponseWriter, r *http.Request) {
|
|
var body ticket.TimeEntry
|
|
if err := readJSON(r, &body); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if !requireMCPProject(r, body.ProjectID) {
|
|
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
|
return
|
|
}
|
|
cmd := &command.AddTimeEntry{Store: s.timeStore, Entry: body}
|
|
if err := s.history.Do(cmd); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.notify(cmd.Entry.ProjectID)
|
|
writeJSON(w, http.StatusCreated, cmd.Entry)
|
|
}
|
|
|
|
func (s *Server) handleUpdateTime(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
entries, err := s.timeStore.List()
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
var before *ticket.TimeEntry
|
|
for i := range entries {
|
|
if entries[i].ID == id {
|
|
before = &entries[i]
|
|
break
|
|
}
|
|
}
|
|
if before == nil {
|
|
writeErr(w, http.StatusNotFound, "time entry not found")
|
|
return
|
|
}
|
|
if !requireMCPProject(r, before.ProjectID) {
|
|
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
|
return
|
|
}
|
|
var after ticket.TimeEntry
|
|
if err := readJSON(r, &after); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
after.ID = id
|
|
if after.ProjectID == "" {
|
|
after.ProjectID = before.ProjectID
|
|
}
|
|
cmd := &command.UpdateTimeEntry{Store: s.timeStore, Before: *before, After: after}
|
|
if err := s.history.Do(cmd); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.notify(after.ProjectID)
|
|
writeJSON(w, http.StatusOK, after)
|
|
}
|
|
|
|
func (s *Server) handleRemoveTime(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
entries, err := s.timeStore.List()
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
var snap *ticket.TimeEntry
|
|
for i := range entries {
|
|
if entries[i].ID == id {
|
|
snap = &entries[i]
|
|
break
|
|
}
|
|
}
|
|
if snap == nil {
|
|
writeErr(w, http.StatusNotFound, "time entry not found")
|
|
return
|
|
}
|
|
if !requireMCPProject(r, snap.ProjectID) {
|
|
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
|
return
|
|
}
|
|
cmd := &command.RemoveTimeEntry{Store: s.timeStore, Snapshot: *snap}
|
|
if err := s.history.Do(cmd); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.notify(snap.ProjectID)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleGetTimer(w http.ResponseWriter, r *http.Request) {
|
|
t, err := s.timeStore.LoadActiveTimer()
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if t != nil && ClientKind(r.Context()) == ClientMCP && t.ProjectID != MCPProjectID(r.Context()) {
|
|
writeJSON(w, http.StatusOK, nil)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, t)
|
|
}
|
|
|
|
func (s *Server) handleSetTimer(w http.ResponseWriter, r *http.Request) {
|
|
defer r.Body.Close()
|
|
var raw map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
var body *ticket.ActiveTimer
|
|
if raw != nil {
|
|
b, err := json.Marshal(raw)
|
|
if err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
var t ticket.ActiveTimer
|
|
if err := json.Unmarshal(b, &t); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if t.TicketID == "" && t.ProjectID == "" {
|
|
body = nil
|
|
} else {
|
|
if !requireMCPProject(r, t.ProjectID) {
|
|
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
|
return
|
|
}
|
|
if t.StartedAt.IsZero() {
|
|
t.StartedAt = time.Now()
|
|
}
|
|
body = &t
|
|
}
|
|
}
|
|
if err := s.timeStore.SaveActiveTimer(body); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if body != nil {
|
|
s.notify(body.ProjectID)
|
|
} else {
|
|
s.notify("")
|
|
}
|
|
writeJSON(w, http.StatusOK, body)
|
|
}
|