# API Serialization Fix & Urgency Field ## Problem Two issues block showing urgency scores in the web frontend: 1. **The Task struct has no JSON tags.** Go defaults to PascalCase field names (`Description`, `ParentUUID`, `RecurrenceDuration`), but the frontend expects snake_case (`description`, `parent_uuid`, `recurrence_duration`). There is no transformation layer on either side — the frontend code works today only because single-word fields like `description`/`Description` are case-insensitive in JavaScript property access... actually they're not. **This is a latent bug** — any field with multiple words (`RecurrenceDuration`, `ParentUUID`) is broken in production. 2. **Urgency is computed but never exposed.** The report engine calculates urgency internally for sorting (`sortByUrgency` in `report.go`) but discards the score before serialization. The `Task` struct has no urgency field. ### Secondary issue: `time.Time` serialization Go's `time.Time` marshals as RFC3339 strings (`"2026-02-15T10:30:00Z"`), but the frontend expects unix timestamps (numbers). The `Status` type (`byte`) marshals as an integer (80 for `'P'`), but the frontend expects a string character (`"P"`). These need explicit handling. --- ## Scope ### Backend (`opal-task`) #### 1. Add json tags to `engine.Task` File: `internal/engine/task.go` ```go type Task struct { UUID uuid.UUID `json:"uuid"` ID int `json:"id"` Status Status `json:"status"` Description string `json:"description"` Project *string `json:"project"` Priority Priority `json:"priority"` Created time.Time `json:"created"` Modified time.Time `json:"modified"` Start *time.Time `json:"start,omitempty"` End *time.Time `json:"end,omitempty"` Due *time.Time `json:"due,omitempty"` Scheduled *time.Time `json:"scheduled,omitempty"` Wait *time.Time `json:"wait,omitempty"` Until *time.Time `json:"until,omitempty"` RecurrenceDuration *time.Duration `json:"recurrence_duration,omitempty"` ParentUUID *uuid.UUID `json:"parent_uuid,omitempty"` Tags []string `json:"tags"` Urgency float64 `json:"urgency"` } ``` #### 2. Custom JSON marshaling for `Status` and timestamps `Status` is a `byte` — it will serialize as `80` not `"P"`. Add a `MarshalJSON`/`UnmarshalJSON` pair on `Status` to emit a single-character string. For `time.Time`, the cleanest approach is a custom `MarshalJSON` on `Task` that emits unix timestamps for all time fields. This matches the existing frontend expectation and avoids date-parsing complexity in the browser. ```go // On Status type func (s Status) MarshalJSON() ([]byte, error) { return json.Marshal(string(s)) } func (s *Status) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { return err } if len(str) != 1 { return fmt.Errorf("invalid status: %q", str) } *s = Status(str[0]) return nil } ``` For timestamps, implement `MarshalJSON` and `UnmarshalJSON` on `Task` that convert between `time.Time` and unix seconds (int64). Nullable time fields become `null` or the unix value. `RecurrenceDuration` (a `time.Duration`, stored internally as nanoseconds) must also be converted to seconds for consistency — the API uses seconds as its universal time unit. The symmetric pair ensures `json.Unmarshal` into a `Task` works correctly (for sync, tests, client-to-client), not just the handler request structs. #### 3. Populate urgency before returning **Where:** Centralize in a helper that handlers call before responding. ```go // In a new file or in task.go func PopulateUrgency(tasks ...*Task) { cfg, _ := GetConfig() coeffs := BuildUrgencyCoefficients(cfg) for _, t := range tasks { t.Urgency = t.CalculateUrgency(coeffs) } } ``` **Call sites** — every handler in `handlers/tasks.go` that returns task(s): | Handler | Returns | |----------------|-----------| | `ListTasks` | `[]*Task` | | `CreateTask` | `*Task` | | `GetTask` | `*Task` | | `UpdateTask` | `*Task` | | `CompleteTask` | `*Task` | | `StartTask` | `*Task` | | `StopTask` | `*Task` | | `AddTaskTag` | `*Task` | | `RemoveTaskTag`| `*Task` | | `ParseTask` | `*Task` | The report path (`ListTasks` with `?report=`) already sorts by urgency via `sortByUrgency` — it should also populate the field so the score is in the response. Currently urgency is calculated, used for sorting, then thrown away. #### 4. Update tests `handlers/tasks_test.go` currently asserts PascalCase keys (`task["Description"]`). Update to snake_case (`task["description"]`). Add assertions for `urgency` field presence and that it's a number. --- ### Frontend (`opal-web`) #### 5. Add `urgency` to Task type File: `src/lib/api/types.js` ```javascript /** * @typedef {Object} Task * ...existing fields... * @property {number} urgency */ ``` #### 6. Display urgency in TaskItem File: `src/lib/components/TaskItem.svelte` Show the urgency score as a small numeric badge in the task metadata row. Render it as a one-decimal float (e.g. `8.2`) with color coding: | Range | Meaning | Color | |----------|-------------|--------------------------------| | >= 10 | Critical | `--color-priority-high-text` | | >= 5 | High | `--color-priority-medium-text` | | > 0 | Normal | `--text-secondary` | | 0 | None | Don't render | Position: rightmost item in the meta row, right-aligned. Use a monospace or tabular-nums font variant so scores don't cause layout shift. #### 7. Update mock data File: `src/lib/mock/tasks.js` Add `urgency` field to mock tasks with representative values so mock mode continues to work. --- ## Technical Decisions ### ADR-7: JSON tags with snake_case convention **Context:** The Go Task struct has no json tags. PascalCase default breaks multi-word fields in the frontend. **Decision:** Add explicit `json:"snake_case"` tags to all Task fields. **Alternatives:** - Frontend transformation layer (rejected — masks the real problem, adds runtime overhead, easy to forget when adding new fields) - Middleware that converts all response keys (rejected — fragile, doesn't handle nested types, hides the contract) **Consequences:** Breaking change to API response shape for any existing consumers. Since the web frontend is the only consumer and its types already expect snake_case, this is actually a **fix** not a break. ### ADR-8: Custom MarshalJSON for unix timestamps **Context:** `time.Time` marshals as RFC3339 strings, frontend expects unix ints. **Decision:** Implement `MarshalJSON` on `Task` that emits unix seconds for all time fields and string for Status. **Alternatives:** - Use a custom `UnixTime` type wrapper (rejected — too invasive, changes every function that touches time fields) - Parse RFC3339 in the frontend (rejected — adds complexity to every consumer, breaks existing date math that assumes unix seconds) **Consequences:** Time values in API responses are plain integers (seconds). Nullable times are `null`. `recurrence_duration` is also seconds (not nanoseconds) — a 1-week recurrence is `604800`, not `604800000000000`. Simple to consume in any language. ### ADR-9: Urgency as a derived field on Task struct **Context:** Urgency is computed from task attributes + config coefficients. It's used for sorting in reports but never exposed to consumers. **Decision:** Add `Urgency float64` to the Task struct alongside `Tags` (both are derived/computed, not stored in DB). Populate via a `PopulateUrgency` helper called in handlers before serialization. **Alternatives:** - Separate response wrapper struct (rejected — duplicates the entire type for one field, adds mapping boilerplate in every handler) - Compute client-side (rejected — requires shipping coefficient config to the frontend, duplicates complex calculation logic) **Consequences:** Every API response that includes tasks will have urgency scores. The score is a snapshot at response time — it may drift slightly from what the sort order used if computed at different moments, but this is negligible. --- ## File Change Summary ``` opal-task/ internal/engine/ task.go .............. ADD json tags, ADD Urgency field, ADD MarshalJSON ADD PopulateUrgency helper urgency.go ........... No changes report.go ............ Populate urgency after sort (in sortByUrgency or Execute) internal/api/handlers/ tasks.go ............. Call PopulateUrgency before jsonResponse in all handlers tasks_test.go ........ Update key assertions to snake_case, add urgency checks opal-web/ src/lib/api/types.js ... ADD urgency field to Task typedef src/lib/components/ TaskItem.svelte ...... ADD urgency badge to meta row src/lib/mock/tasks.js .. ADD urgency values to mock data ```