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) }