summaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-07-12-widget-overdue-badge.md
blob: c67cf72a26c62b973dd53e458bee82dc1368fa39 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# 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<WidgetItem>
)
```

- [ ] **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.