# Widget Overdue Badge Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Forward the already-computed `IsOverdue` flag from the server's `TimelineItem` through the widget API to the Android client, and render overdue task titles in a distinct warning color. **Architecture:** Add `IsOverdue` to `models.WidgetItem` (Go) and forward it in `TimelineItemToWidgetItem`. Add the matching `isOverdue` field to the Android `WidgetItem` data class. `TaskRow` (the single shared rendering path for every task row in the widget) reads the flag and colors the title text accordingly. **Tech Stack:** Go (`internal/models`, `internal/handlers`), Kotlin/Jetpack Glance (Android widget). ## Global Constraints - Visual signal is a title-text color change only — no new icon, badge, or label, and no sort-order change. This matches every other "this is special" signal in `DootWidget.kt`, which is color-only (`sourceColor`, `AllDayRow`'s bar, `EventBlock`'s past-event dimming). - The overdue color is `Color(0xFFF87171)` (soft red) — distinct from the default title color `Color(0xFFDDDDDD)` and from every value in `sourceColor()`. - Server-side test follows the existing pattern in `internal/handlers/widget_test.go` (see `TestTimelineItemToWidgetItem_AllDayEvent` for the style: table-free, one behavior per test function, doc comment explaining the "why"). --- ### Task 1: Server — forward IsOverdue through the widget API **Files:** - Modify: `internal/models/widget.go` - Modify: `internal/handlers/widget.go` - Test: `internal/handlers/widget_test.go` **Interfaces:** - Produces: `models.WidgetItem.IsOverdue bool` (JSON field `is_overdue`) - Consumes: `models.TimelineItem.IsOverdue` (already exists, already correctly computed — see `internal/models/timeline.go:62`) - [ ] **Step 1: Write the failing test** Add to `internal/handlers/widget_test.go`, directly after `TestTimelineItemToWidgetItem_AllDayTask_KeepsFloatingBehavior`: ```go // TestTimelineItemToWidgetItem_ForwardsIsOverdue proves the 2026-07-12 // overdue-badge fix: TimelineItem.IsOverdue (already computed correctly by // ComputeDaySection, confirmed by the earlier fix that made overdue tasks // appear in the timeline at all) must be forwarded onto WidgetItem so the // Android client can render it distinctly -- previously it was silently // dropped, so an overdue task looked identical to a normal one on the // widget. func TestTimelineItemToWidgetItem_ForwardsIsOverdue(t *testing.T) { item := models.TimelineItem{ ID: "overdue-1", Title: "Pay the water bill", Source: "doot", Type: models.TimelineItemTypeTask, Time: time.Now(), IsOverdue: true, } wi := TimelineItemToWidgetItem(item) if !wi.IsOverdue { t.Error("expected IsOverdue to be forwarded as true") } } func TestTimelineItemToWidgetItem_NotOverdueByDefault(t *testing.T) { item := models.TimelineItem{ ID: "today-1", Title: "Water the plants", Source: "doot", Type: models.TimelineItemTypeTask, Time: time.Now(), } wi := TimelineItemToWidgetItem(item) if wi.IsOverdue { t.Error("expected IsOverdue to be false when the source item isn't overdue") } } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `go test ./internal/handlers/ -run TestTimelineItemToWidgetItem_ForwardsIsOverdue -v` Expected: FAIL — `wi.IsOverdue` is always `false` (the zero value), because `models.WidgetItem` has no such field yet (this will actually be a compile error first: `unknown field IsOverdue in struct literal` is not applicable here since the test only reads `wi.IsOverdue` — the compile error will be `wi.IsOverdue undefined (type models.WidgetItem has no field or method IsOverdue)`). - [ ] **Step 3: Add the field to `models.WidgetItem`** In `internal/models/widget.go`, add `IsOverdue` to the `WidgetItem` struct (after `IsAllDay`, before `URL`, to keep related boolean flags grouped): ```go type WidgetItem struct { ID string `json:"id"` Title string `json:"title"` Source string `json:"source"` Type string `json:"type"` Start *time.Time `json:"start,omitempty"` // nil = no specific time (floating task) End *time.Time `json:"end,omitempty"` IsAllDay bool `json:"is_all_day"` IsOverdue bool `json:"is_overdue"` URL string `json:"url,omitempty"` Completable bool `json:"completable"` // true = doot task (checkbox shown) } ``` - [ ] **Step 4: Forward the field in `TimelineItemToWidgetItem`** In `internal/handlers/widget.go`, find: ```go wi := models.WidgetItem{ ID: item.ID, Title: item.Title, Source: item.Source, IsAllDay: item.IsAllDay, URL: item.URL, } ``` Replace with: ```go wi := models.WidgetItem{ ID: item.ID, Title: item.Title, Source: item.Source, IsAllDay: item.IsAllDay, IsOverdue: item.IsOverdue, URL: item.URL, } ``` - [ ] **Step 5: Run tests to verify they pass** Run: `go test ./internal/handlers/ -run TestTimelineItemToWidgetItem -v` Expected: PASS — all `TestTimelineItemToWidgetItem_*` tests pass, including the two new ones. - [ ] **Step 6: Run the full Go test suite and gofmt check** Run: `go build ./... && go test ./... 2>&1 | tail -20` Expected: only the two pre-existing failures unrelated to this change (`TestHandleAgentTaskWriteOperations`, `TestHandleAgentCreateOperations` in `internal/handlers`, and the `internal/models` vet failure for `undefined: MealToAtom`) — confirm no new failures. Run: `gofmt -l internal/models/widget.go internal/handlers/widget.go internal/handlers/widget_test.go` Expected: no output (clean). - [ ] **Step 7: Commit** ```bash git add internal/models/widget.go internal/handlers/widget.go internal/handlers/widget_test.go git commit -m "feat(widget): forward IsOverdue from TimelineItem to WidgetItem API" ``` --- ### Task 2: Android — render overdue tasks with a distinct title color **Depends on:** Task 1 must be deployed (or at least merged) first — the Android build needs `is_overdue` in the JSON response to have somewhere to come from, though the field itself is optional/defaults false so the build will compile fine either way. Sequencing is for a meaningful on-device test, not a compile dependency. **Files:** - Modify: `android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt` - Modify: `android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt` **Interfaces:** - Consumes: JSON field `is_overdue` (produced by Task 1) - Produces: `WidgetItem.isOverdue: Boolean` (default `false`) - [ ] **Step 1: Add the field to the Android `WidgetItem` data class** Replace the full contents of `android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt`: ```kotlin package org.terst.doot.widget.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class WidgetItem( val id: String, val title: String, val source: String, // "todoist" | "trello" | "calendar" | "plantoeat" | "gtasks" val type: String, // "task" | "event" val start: String? = null, // ISO-8601 or null (floating task) val end: String? = null, // ISO-8601 or null @SerialName("is_all_day") val isAllDay: Boolean = false, @SerialName("is_overdue") val isOverdue: Boolean = false, val url: String = "", val completable: Boolean = false ) @Serializable data class WidgetResponse( val now: String, val items: List ) ``` - [ ] **Step 2: Color the title text for overdue tasks in `TaskRow`** In `android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt`, find the `TaskRow` composable's title `Text` (currently around line 344-351): ```kotlin Box( modifier = GlanceModifier .defaultWeight() .clickable(actionStartActivity(detailIntent)) .padding(start = 8.dp), contentAlignment = Alignment.CenterStart ) { Text( text = task.title, style = TextStyle( color = ColorProvider(Color(0xFFDDDDDD.toInt())), fontSize = 14.sp ), maxLines = 1 ) } ``` Replace with: ```kotlin Box( modifier = GlanceModifier .defaultWeight() .clickable(actionStartActivity(detailIntent)) .padding(start = 8.dp), contentAlignment = Alignment.CenterStart ) { Text( text = task.title, style = TextStyle( color = ColorProvider( if (task.isOverdue) Color(0xFFF87171) else Color(0xFFDDDDDD.toInt()) ), fontSize = 14.sp ), maxLines = 1 ) } ``` - [ ] **Step 3: Build and confirm no compile errors** ```bash cd android && ./gradlew assembleDebug ``` Expected: `BUILD SUCCESSFUL`. - [ ] **Step 4: Commit** ```bash git add android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt \ android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt git commit -m "feat(widget): color overdue task titles distinctly" ``` Manual on-device verification (release APK build, deploy, visual check) is deferred to the controller, same as the refresh-button feature's Step 8. --- ## Out of scope Clickable-date reschedule, quick add, and recurrence display — each gets its own spec and plan.