summaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-07-12-widget-refresh-button-design.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-13 07:11:04 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-13 07:11:04 +0000
commit44abf42ed45aa8f285e7ce031cbb9ef1ade667ea (patch)
treedbc21cadc2750c6e39e7f7613be0b3f73790c445 /docs/superpowers/specs/2026-07-12-widget-refresh-button-design.md
parent8310f802dd9fc6ef5dff0be7f640f79c5b39987f (diff)
parent4126fe4f56a6eb9703a084d4793a597f37bf2867 (diff)
Merge github/master: reconcile with parallel widget work
Another session pushed 27 commits in parallel covering quick-add, event detail popups, recurrence display, overdue badges, a manual refresh button, and its own fix for the same overdue-tasks bug (via a separate GetOverdueNativeTasks fetch folded into BuildTimeline, rather than widening GetNativeTasksByDateRange's bound directly). Reconciled rather than blindly taking one side: - Reverted GetNativeTasksByDateRange to its original bounded query and kept upstream's GetOverdueNativeTasks + BuildTimeline fold-in as the sole overdue mechanism for native tasks, to avoid double-counting overdue items (my widened query + their separate fetch would have both returned them). Re-pointed the regression test at the now-correct contract and added a store-level test for GetOverdueNativeTasks directly. - Kept my GetGoogleTasksByDateRange fix as-is (single unbounded query) -- upstream never touched Google Tasks overdue handling, so there's no duplication risk there. - Rewove WidgetRoot's LazyColumn structure (added for scrolling) around upstream's new header buttons, pinned all-day event rows, and the enhanced TomorrowSection, none of which were written LazyColumn-aware since that work landed on this side only. - Combined both sides' additions to TaskDetailActivity/TaskDetailSheet (description-edit detail popup + due-date reschedule label) and WidgetRepository/Actions (optimistic local removal + refresh button wiring) -- these were independent, non-overlapping features that both needed to survive. - Renumbered the migration collision: both sides independently added a migration numbered 022. Card-description was already applied to the live production DB under that filename earlier this session (migrations are tracked by filename), so it keeps 022; the recurring-event-id migration, never deployed under any name here, moves to 023. Verified: go build clean, full test suite passes (only the two pre-existing agent-handler failures and the pre-existing models package build error remain, both confirmed unrelated via git stash before this session began), and a dry run against a copy of the live production database applies both migrations cleanly with no re-run conflicts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'docs/superpowers/specs/2026-07-12-widget-refresh-button-design.md')
-rw-r--r--docs/superpowers/specs/2026-07-12-widget-refresh-button-design.md43
1 files changed, 43 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-07-12-widget-refresh-button-design.md b/docs/superpowers/specs/2026-07-12-widget-refresh-button-design.md
new file mode 100644
index 0000000..4a3ad34
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-12-widget-refresh-button-design.md
@@ -0,0 +1,43 @@
+# Widget Refresh Button — Design
+
+## Context
+
+First of five widget feature requests (refresh button, quick add, clickable-date reschedule, recurrence in the popup, overdue badge), being designed and shipped as separate small cycles rather than one batch. This spec covers only the refresh button.
+
+`RefreshWorker` already exists and does the right thing on completion: `fetchAndPersist()` then `DootWidget().updateAll()`. Today it only runs on a 15-minute periodic schedule (`RefreshWorker.schedule()`) or via `RefreshWorker.runOnce()`, which nothing currently calls. There is no manual trigger in the widget UI.
+
+## Goal
+
+A tappable refresh icon on the widget that triggers an immediate fetch, with visual feedback while the fetch is in flight.
+
+## Constraint
+
+Android AppWidgets render via `RemoteViews`, which does not support arbitrary View animation (no rotating-icon spinner). The realistic version of "spinner" is a static icon swap: refresh glyph ↔ a distinct "loading" glyph, swapped synchronously on tap and swapped back when the worker finishes. Confirmed acceptable with the user.
+
+## Design
+
+**Flow:**
+1. User taps the refresh icon.
+2. `RefreshTaskAction.onAction()` (new `ActionCallback`, mirrors the existing `CompleteTaskAction`) sets `Keys.IS_REFRESHING = true` in the widget's DataStore and calls `DootWidget().updateAll(context)` immediately — the icon flips to the loading glyph before any network call happens.
+3. The same action enqueues `RefreshWorker.runOnce(context)`.
+4. `RefreshWorker.doWork()` is extended so that, on **both** the success and failure branches, it clears `Keys.IS_REFRESHING` and calls `DootWidget().updateAll(context)` again — the icon always reverts, even if the fetch errors out and `Result.retry()` is returned (a retry is still "not actively refreshing" from the user's point of view between attempts).
+
+**Components:**
+
+- `data/DataStore.kt` — add `val IS_REFRESHING = booleanPreferencesKey("is_refreshing")` to `Keys`.
+- `res/drawable/ic_refresh.xml` — new 24dp vector icon, same style/stroke convention as the existing `ic_checkbox_empty.xml`.
+- `res/drawable/ic_refresh_loading.xml` — new 24dp vector icon, visually distinct (e.g. hourglass or three dots) from the idle refresh icon.
+- `widget/ui/Actions.kt` — new `RefreshTaskAction : ActionCallback`, no parameters needed.
+- `widget/ui/DootWidget.kt`:
+ - `provideGlance()` reads `prefs[Keys.IS_REFRESHING] ?: false` and passes it into `WidgetRoot(items, now, isRefreshing)`.
+ - `WidgetRoot`'s existing "TODAY" header `Row` gains a second child: a new `RefreshButton(isRefreshing: Boolean)` composable, right-aligned (the `Row` needs `horizontalArrangement`/a spacer or `defaultWeight()` on the "TODAY" text so the icon lands on the right edge).
+ - `RefreshButton` renders `ic_refresh_loading` if `isRefreshing`, else `ic_refresh`; both wrapped in a `Box` with `clickable(actionRunCallback<RefreshTaskAction>())`, sized to match the existing checkbox tap target (24dp box, matching `TaskRow`'s checkbox pattern).
+- `widget/work/RefreshWorker.kt` — `doWork()`'s `fold` branches both gain the clear-flag-and-update step before returning their `Result`.
+
+**Error handling:** No new error states. `RefreshWorker.doWork()` already returns `Result.retry()` on failure; WorkManager's existing backoff handles the retry timing. The icon simply reverts to idle between attempts rather than staying in a stuck loading state.
+
+**Testing:** No new unit-test surface — this is Glance/RemoteViews composition and WorkManager wiring, consistent with how the rest of `DootWidget.kt` and the `*Worker.kt` classes are (not) unit tested today. Verified by building the APK, installing on device, and confirming the tap → loading-icon → fetch → idle-icon cycle live, the same verification approach used for every other widget fix this session.
+
+## Out of scope
+
+The other four widget features (quick add, clickable-date reschedule, recurrence display, overdue badge) — each gets its own spec.