summaryrefslogtreecommitdiff
path: root/docs
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
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')
-rw-r--r--docs/superpowers/plans/2026-07-12-widget-overdue-badge.md260
-rw-r--r--docs/superpowers/specs/2026-07-12-widget-overdue-badge-design.md30
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.