diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 09:43:48 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 09:43:48 +0000 |
| commit | 957549d8920423a1358e300f0848f5a0332cc48a (patch) | |
| tree | 24460d4f9c4b5d9adcbd2c3df98db2d336f55397 | |
| parent | e1d93d9de7a8af3d47fefdb797ae36c7bde555aa (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
| -rw-r--r-- | docs/superpowers/plans/2026-07-12-widget-overdue-badge.md | 260 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-07-12-widget-overdue-badge-design.md | 30 |
2 files changed, 290 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. diff --git a/docs/superpowers/specs/2026-07-12-widget-overdue-badge-design.md b/docs/superpowers/specs/2026-07-12-widget-overdue-badge-design.md new file mode 100644 index 0000000..db8cdce --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-widget-overdue-badge-design.md @@ -0,0 +1,30 @@ +# Widget Overdue Badge — Design + +## Context + +Second of five widget features (refresh → **overdue badge** → clickable reschedule → quick add → recurrence). Direct follow-on from the 2026-07-12 fix (commit `f01b292`) that made overdue native tasks appear in the widget at all: `TimelineItem.IsOverdue` is computed server-side but never reaches the client. Today all 10 previously-invisible overdue tasks render identically to a normal task due today — same color, same style, no way to tell "this is 12 days late" from "this is due today." + +Decided without a user Q&A round (per explicit instruction to proceed autonomously); the choices below are the minimal, lowest-risk options consistent with existing patterns in the codebase. + +## Goal + +Visually distinguish overdue tasks from normal tasks in the widget. + +## Design + +**Server (Go):** +- `models.WidgetItem` (`internal/models/widget.go`) gains `IsOverdue bool `json:"is_overdue"``. +- `TimelineItemToWidgetItem` (`internal/handlers/widget.go`) sets `wi.IsOverdue = item.IsOverdue` — `TimelineItem.IsOverdue` is already computed correctly by `ComputeDaySection` (confirmed by the prior fix), this just forwards the existing field. + +**Android (Kotlin):** +- `WidgetItem.kt` gains `@SerialName("is_overdue") val isOverdue: Boolean = false`. +- `TaskRow` (`DootWidget.kt`) is the single rendering path for every task (today's floating queue, tomorrow's section, and now overdue tasks all flow through it — confirmed by grounding: `TomorrowSection` and the floating-task fragments both call `TaskRow`). When `task.isOverdue == true`, the title `Text` uses a warning color (`Color(0xFFF87171)` — a soft red, distinct from the existing grey `0xFFDDDDDD` and from every `sourceColor` value) instead of the default grey. No other layout change — same row height, same checkbox, same tap targets. + +**Why title-color instead of a new badge/icon/label:** the widget is already dense (hour grid + floating queue + tomorrow section); every existing "this is special" signal in this file is done via color (see `sourceColor`, `AllDayRow`'s colored bar, `EventBlock`'s past-event alpha dimming) rather than added text or icons. A color change is the lowest-risk, most consistent option and needs no new layout space. + +**Out of scope:** No "N days overdue" text, no sort-order change (overdue tasks already interleave into the floating queue via `SlotPacker` same as any other floating task — leaving that alone, this feature is purely visual). + +## Testing + +- Server: unit test for `TimelineItemToWidgetItem` asserting `IsOverdue` is forwarded (mirrors the existing `TestTimelineItemToWidgetItem_AllDayEvent` pattern in `widget_test.go`). +- Android: no unit-test surface (consistent with all other `DootWidget.kt` changes this session) — verified by build + manual on-device check, deferred to the controller same as Feature 1's Step 8. |
