summaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-07-12-widget-overdue-badge.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-12 09:43:48 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-12 09:43:48 +0000
commit957549d8920423a1358e300f0848f5a0332cc48a (patch)
tree24460d4f9c4b5d9adcbd2c3df98db2d336f55397 /docs/superpowers/plans/2026-07-12-widget-overdue-badge.md
parente1d93d9de7a8af3d47fefdb797ae36c7bde555aa (diff)
docs: add widget overdue-badge design spec and implementation plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
Diffstat (limited to 'docs/superpowers/plans/2026-07-12-widget-overdue-badge.md')
-rw-r--r--docs/superpowers/plans/2026-07-12-widget-overdue-badge.md260
1 files changed, 260 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-07-12-widget-overdue-badge.md b/docs/superpowers/plans/2026-07-12-widget-overdue-badge.md
new file mode 100644
index 0000000..c67cf72
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-12-widget-overdue-badge.md
@@ -0,0 +1,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.