Add local JSON daemon and route TUI/CLI through it.
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>
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user