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:
2026-02-15 14:58:34 +01:00
parent 924b66bc64
commit 3bb2ef2759
9 changed files with 581 additions and 51 deletions
+15 -9
View File
@@ -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")
}
}