feat: add JSON serialization, urgency field, and snake_case API contract
Fix latent API bug where multi-word fields (RecurrenceDuration, ParentUUID, CreatedAt) serialized as PascalCase, breaking the frontend. Add explicit snake_case json tags and custom MarshalJSON/UnmarshalJSON on Task, Status, and APIKey to emit unix timestamps and string status codes. Add Urgency float64 as a derived field on Task, populated via PopulateUrgency helper in all handlers before serialization. The report engine's sortByUrgency now also retains the computed score. Frontend updated with urgency type, color-coded badge in TaskItem, and mock data values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,9 @@ func ListTasks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Report sorts may already populate urgency, but ensure it for all paths
|
||||
engine.PopulateUrgency(tasks...)
|
||||
|
||||
jsonResponse(w, http.StatusOK, map[string]interface{}{
|
||||
"report": reportName,
|
||||
"tasks": tasks,
|
||||
@@ -87,6 +90,7 @@ func ListTasks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(tasks...)
|
||||
jsonResponse(w, http.StatusOK, tasks)
|
||||
}
|
||||
|
||||
@@ -159,6 +163,7 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusCreated, task)
|
||||
}
|
||||
|
||||
@@ -178,6 +183,7 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@@ -271,6 +277,7 @@ func UpdateTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@@ -322,6 +329,7 @@ func CompleteTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@@ -346,6 +354,7 @@ func StartTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@@ -370,6 +379,7 @@ func StopTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@@ -429,6 +439,7 @@ func AddTaskTag(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@@ -489,6 +500,7 @@ func ParseTask(w http.ResponseWriter, r *http.Request) {
|
||||
errorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
engine.PopulateUrgency(instance)
|
||||
jsonResponse(w, http.StatusCreated, map[string]interface{}{"task": instance})
|
||||
return
|
||||
}
|
||||
@@ -500,6 +512,7 @@ func ParseTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusCreated, map[string]interface{}{"task": task})
|
||||
}
|
||||
|
||||
@@ -525,5 +538,6 @@ func RemoveTaskTag(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.PopulateUrgency(task)
|
||||
jsonResponse(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@@ -57,8 +57,14 @@ func TestParseTask_DescriptionOnly(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("expected task in data")
|
||||
}
|
||||
if task["Description"] != "buy groceries" {
|
||||
t.Errorf("expected description 'buy groceries', got %v", task["Description"])
|
||||
if task["description"] != "buy groceries" {
|
||||
t.Errorf("expected description 'buy groceries', got %v", task["description"])
|
||||
}
|
||||
if _, ok := task["urgency"]; !ok {
|
||||
t.Error("expected urgency field in response")
|
||||
}
|
||||
if _, ok := task["urgency"].(float64); !ok {
|
||||
t.Error("expected urgency to be a number")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +87,11 @@ func TestParseTask_WithModifiers(t *testing.T) {
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
task := data["task"].(map[string]interface{})
|
||||
if task["Description"] != "review PR" {
|
||||
t.Errorf("expected description 'review PR', got %v", task["Description"])
|
||||
if task["description"] != "review PR" {
|
||||
t.Errorf("expected description 'review PR', got %v", task["description"])
|
||||
}
|
||||
if task["Project"] != "backend" {
|
||||
t.Errorf("expected project 'backend', got %v", task["Project"])
|
||||
if task["project"] != "backend" {
|
||||
t.Errorf("expected project 'backend', got %v", task["project"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +115,9 @@ func TestParseTask_WithRecurrence(t *testing.T) {
|
||||
data := resp["data"].(map[string]interface{})
|
||||
task := data["task"].(map[string]interface{})
|
||||
|
||||
// The returned task should be the first instance (pending, with ParentUUID)
|
||||
if task["ParentUUID"] == nil {
|
||||
t.Error("expected ParentUUID to be set for recurring instance")
|
||||
// The returned task should be the first instance (pending, with parent_uuid)
|
||||
if task["parent_uuid"] == nil {
|
||||
t.Error("expected parent_uuid to be set for recurring instance")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user