feat(widget): per-residence widget configuration — closes #6 #10

Merged
admin merged 2 commits from feat/6-widget-residence-picker into master 2026-05-11 13:39:06 -05:00
Owner

Closes #6.

Users with multiple residences can now pick which one a given home-screen widget shows tasks for, on both iOS and Android. Pinning two widgets — one per house — lets each surface tasks for only that residence.

Single-home users and anyone who leaves the picker untouched continue to see tasks across every residence (the prior default), so this is non-breaking.

iOS

Component Change
ConfigurationAppIntent Add optional @Parameter of type WidgetResidenceEntity
WidgetResidenceEntity + WidgetResidenceEntityQuery New AppEntity reading from the App Group sidecar
WidgetDataManager saveResidences(from:) writes widget_residences.json; clearCache() clears it on sign-out; pure filterTasks(_:forResidenceId:) helper
WidgetDataManager.WidgetTask + CacheManager.CustomTask Optional residence_id Codable key — legacy JSON still decodes
DataManagerObservable Mirror every myResidences update into WidgetDataManager.saveResidences(from:)
CacheManager.getUpcomingTasks(forResidenceId:) + Provider.timeline/snapshot Honor configuration.residence?.intId

The picker is the standard AppIntents configuration sheet (long-press the widget → Edit Widget → Residence).

Android

Component Change
WidgetConfigActivity New Compose ComponentActivity hosting the residence-picker UI
WidgetDataStore widget_residences_json key + per-instance widget_residence_id_<appWidgetId> keys
WidgetDataRepository saveResidences/loadResidences, saveResidenceIdFor/loadResidenceIdFor/clearResidenceIdFor, loadTasksForResidence, loadTasksForWidget(appWidgetId), computeStatsFromTasks(_), Filter.filterTasksForResidence(_, _)
WidgetTaskDto already carried residenceId
WidgetResidenceDto new (id + name) — JSON-persisted via the sidecar
WidgetRefreshWorker / DefaultWidgetRefreshDataSource Pull myResidences alongside tasks/tier on each refresh, write the sidecar (best-effort; non-fatal on failure)
HoneyDue{Small,Medium,Large}Widget.provideGlance Resolve appWidgetId via GlanceAppWidgetManager(context).getAppWidgetId(id) and call loadTasksForWidget(appWidgetId); large widget uses computeStatsFromTasks(tasks) so its stat tiles reflect the scoped list
HoneyDue{Small,Medium,Large}WidgetReceiver.onDeleted Purge the per-instance residence scope when the tile is removed
AndroidManifest.xml Register WidgetConfigActivity with the APPWIDGET_CONFIGURE action
honeydue_{small,medium,large}_widget_info.xml Declare android:configure="com.tt.honeyDue.widget.WidgetConfigActivity"

The picker launches automatically when the user pins a new tile and is reachable again via "Edit Widget" on any pinned tile.

Migration / safety

  • residence_id field is optional on both platforms' Codable surfaces. Older app builds wrote widget cache without it; those tasks pass through the filter for unscoped widgets (preserves the legacy "all residences" view) and are hidden in scoped ones (we'd rather omit than misattribute).
  • Tiles that haven't been through the picker have no scope persisted → they default to "All residences" (legacy behaviour). Existing widgets keep working without the user touching anything.
  • The picker handles an empty residences list (signed-out / first install before background refresh) with an explicit helper message pointing at the main app.
  • Android clearAll() sweeps widget_residence_id_<n> keys by prefix on logout. onDeleted purges the key for a single removed tile.
  • iOS WidgetTask.init and CustomTask.init gained custom inits with residenceId: Int? = nil default so existing #Preview literals and TaskMetricsTests continue to compile.

Tests

iOS — new iosApp/HoneyDueTests/WidgetResidenceFilterTests.swift (5 cases): nil-passthrough, matching-id, no-match, missing-residence-on-task in scoped widget, order preservation.

Android — new composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/widget/WidgetResidenceFilterTest.kt (9 cases): the same 5 filter contracts, plus DataStore round-trip for the per-appWidgetId key, end-to-end loadTasksForWidget with multiple configured tiles, clearResidenceIdFor on tile removal, and saveResidences/loadResidences round-trip.

$ xcodebuild test -only-testing:HoneyDueTests/WidgetResidenceFilterTests
** TEST SUCCEEDED **

$ ./gradlew :composeApp:testDebugUnitTest --tests "com.tt.honeyDue.widget.WidgetResidenceFilterTest"
BUILD SUCCESSFUL

$ ./gradlew :composeApp:assembleDebug
BUILD SUCCESSFUL

$ xcodebuild build -scheme HoneyDue
** BUILD SUCCEEDED **
Closes #6. Users with multiple residences can now pick which one a given home-screen widget shows tasks for, on both iOS and Android. Pinning two widgets — one per house — lets each surface tasks for only that residence. Single-home users and anyone who leaves the picker untouched continue to see tasks across every residence (the prior default), so this is non-breaking. ## iOS | Component | Change | |---|---| | `ConfigurationAppIntent` | Add optional `@Parameter` of type `WidgetResidenceEntity` | | `WidgetResidenceEntity` + `WidgetResidenceEntityQuery` | New `AppEntity` reading from the App Group sidecar | | `WidgetDataManager` | `saveResidences(from:)` writes `widget_residences.json`; `clearCache()` clears it on sign-out; pure `filterTasks(_:forResidenceId:)` helper | | `WidgetDataManager.WidgetTask` + `CacheManager.CustomTask` | Optional `residence_id` Codable key — legacy JSON still decodes | | `DataManagerObservable` | Mirror every `myResidences` update into `WidgetDataManager.saveResidences(from:)` | | `CacheManager.getUpcomingTasks(forResidenceId:)` + `Provider.timeline/snapshot` | Honor `configuration.residence?.intId` | The picker is the standard `AppIntents` configuration sheet (long-press the widget → Edit Widget → Residence). ## Android | Component | Change | |---|---| | `WidgetConfigActivity` | New Compose `ComponentActivity` hosting the residence-picker UI | | `WidgetDataStore` | `widget_residences_json` key + per-instance `widget_residence_id_<appWidgetId>` keys | | `WidgetDataRepository` | `saveResidences/loadResidences`, `saveResidenceIdFor/loadResidenceIdFor/clearResidenceIdFor`, `loadTasksForResidence`, `loadTasksForWidget(appWidgetId)`, `computeStatsFromTasks(_)`, `Filter.filterTasksForResidence(_, _)` | | `WidgetTaskDto` | already carried `residenceId` | | `WidgetResidenceDto` | new (id + name) — JSON-persisted via the sidecar | | `WidgetRefreshWorker` / `DefaultWidgetRefreshDataSource` | Pull `myResidences` alongside tasks/tier on each refresh, write the sidecar (best-effort; non-fatal on failure) | | `HoneyDue{Small,Medium,Large}Widget.provideGlance` | Resolve `appWidgetId` via `GlanceAppWidgetManager(context).getAppWidgetId(id)` and call `loadTasksForWidget(appWidgetId)`; large widget uses `computeStatsFromTasks(tasks)` so its stat tiles reflect the scoped list | | `HoneyDue{Small,Medium,Large}WidgetReceiver.onDeleted` | Purge the per-instance residence scope when the tile is removed | | `AndroidManifest.xml` | Register `WidgetConfigActivity` with the `APPWIDGET_CONFIGURE` action | | `honeydue_{small,medium,large}_widget_info.xml` | Declare `android:configure="com.tt.honeyDue.widget.WidgetConfigActivity"` | The picker launches automatically when the user pins a new tile and is reachable again via "Edit Widget" on any pinned tile. ## Migration / safety - `residence_id` field is **optional** on both platforms' Codable surfaces. Older app builds wrote widget cache without it; those tasks pass through the filter for unscoped widgets (preserves the legacy "all residences" view) and are **hidden** in scoped ones (we'd rather omit than misattribute). - Tiles that haven't been through the picker have no scope persisted → they default to "All residences" (legacy behaviour). Existing widgets keep working without the user touching anything. - The picker handles an empty residences list (signed-out / first install before background refresh) with an explicit helper message pointing at the main app. - Android `clearAll()` sweeps `widget_residence_id_<n>` keys by prefix on logout. `onDeleted` purges the key for a single removed tile. - iOS `WidgetTask.init` and `CustomTask.init` gained custom inits with `residenceId: Int? = nil` default so existing `#Preview` literals and `TaskMetricsTests` continue to compile. ## Tests **iOS** — new `iosApp/HoneyDueTests/WidgetResidenceFilterTests.swift` (5 cases): nil-passthrough, matching-id, no-match, missing-residence-on-task in scoped widget, order preservation. **Android** — new `composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/widget/WidgetResidenceFilterTest.kt` (9 cases): the same 5 filter contracts, plus DataStore round-trip for the per-`appWidgetId` key, end-to-end `loadTasksForWidget` with multiple configured tiles, `clearResidenceIdFor` on tile removal, and `saveResidences/loadResidences` round-trip. ``` $ xcodebuild test -only-testing:HoneyDueTests/WidgetResidenceFilterTests ** TEST SUCCEEDED ** $ ./gradlew :composeApp:testDebugUnitTest --tests "com.tt.honeyDue.widget.WidgetResidenceFilterTest" BUILD SUCCESSFUL $ ./gradlew :composeApp:assembleDebug BUILD SUCCESSFUL $ xcodebuild build -scheme HoneyDue ** BUILD SUCCEEDED ** ```
admin added 1 commit 2026-05-11 13:15:25 -05:00
feat(widget): per-residence widget configuration (iOS, gitea#6)
Android UI Tests / ui-tests (pull_request) Has been cancelled
498e6b8064
Users with multiple residences can now pick which one a given home-
screen widget shows tasks for. Pinning two widgets — one per house —
lets each surface tasks for only that residence; users who keep the
configuration untouched continue to see all residences (the previous
default), so single-home users see no behavioural change.

Implementation (iOS only — Android Glance follow-up is scoped in the
issue):

* `ConfigurationAppIntent` (HoneyDue widget extension) gains an
  optional `@Parameter` of type `WidgetResidenceEntity`. `AppIntents`
  renders it as a residence picker in the widget edit sheet.
* `WidgetResidenceEntity` + `WidgetResidenceEntityQuery` resolve the
  user's residences from a new `widget_residences.json` sidecar in the
  App Group container (avoids a network call at config time).
* `WidgetDataManager.saveResidences(from:)` writes that sidecar from
  the main app whenever `DataManagerObservable.myResidences` updates.
  Logout clears it along with the rest of the widget cache.
* `WidgetDataManager.WidgetTask` + the widget extension's
  `CacheManager.CustomTask` both gain an optional `residence_id`
  field. Optional so older app builds that wrote pre-#6 widget cache
  continue to decode — those tasks pass through the filter for
  unscoped widgets and are hidden from scoped ones (safer than
  guessing).
* `CacheManager.getUpcomingTasks(forResidenceId:)` and the pure
  helper `WidgetDataManager.filterTasks(_:forResidenceId:)` apply the
  filter. `Provider.timeline` / `snapshot` read
  `configuration.residence?.intId` and pass it through.

Tests: new `WidgetResidenceFilterTests` (HoneyDueTests target, 5
cases) cover nil-passthrough, matching-id, no-match, missing-residence
on a task, and order preservation. All five green.

No Android changes in this commit — Glance widgets need a separate
configuration activity and an actionStartActivity wiring that's
non-trivial; tracking as a follow-up in the same issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
admin added 1 commit 2026-05-11 13:31:48 -05:00
feat(widget): per-residence widget configuration (Android, gitea#6)
Android UI Tests / ui-tests (pull_request) Has been cancelled
9c9e6009c7
Mirrors the iOS implementation. Adds a Glance configuration activity
that launches when the user pins a new honeyDue widget tile and again
on "Edit Widget", lets them pick one of their residences (or "All
residences"), and persists the choice per-`appWidgetId`. Each tile's
`provideGlance` resolves its own scope and filters tasks (and stats,
on the large widget) accordingly.

Pieces:

- `WidgetConfigActivity` — Compose `ComponentActivity` hosting the
  residence-picker UI; reads the persisted residences sidecar, reads
  any prior scope for the current `appWidgetId`, writes the new
  selection on Save, and re-renders every widget tile.
- `WidgetDataStore` — new `widget_residences_json` key + a per-instance
  `widget_residence_id_<appWidgetId>` key. `clearAll()` sweeps the
  per-instance keys by prefix so logout doesn't leave dangling state.
- `WidgetDataRepository`:
  * `saveResidences(_)` / `loadResidences()` for the picker.
  * `saveResidenceIdFor(appWidgetId, residenceId)` /
    `loadResidenceIdFor(appWidgetId)` /
    `clearResidenceIdFor(appWidgetId)` for per-tile scope.
  * `loadTasksForResidence(residenceId)` and the
    `appWidgetId`-driven `loadTasksForWidget(appWidgetId)`.
  * `computeStatsFromTasks(tasks)` so the large widget's tiles
    reflect only the scoped task list (instead of the whole cache).
  * Pure `Filter.filterTasksForResidence(_, _)` on the companion
    object — easy to exercise from unit tests.
- `WidgetTaskDto` already carries `residenceId`. New `WidgetResidenceDto`
  added (id + name) — JSON-persisted via the sidecar.
- `WidgetRefreshWorker` / `DefaultWidgetRefreshDataSource` — pull
  `myResidences` alongside tasks/tier on each refresh and write the
  sidecar (best-effort; non-fatal if the call fails).
- `HoneyDue{Small,Medium,Large}Widget.provideGlance` — resolve
  `appWidgetId` via `GlanceAppWidgetManager(context).getAppWidgetId(id)`
  and call `loadTasksForWidget(appWidgetId)`.
- `HoneyDue{Small,Medium,Large}WidgetReceiver.onDeleted` — purge the
  per-instance residence scope key when the tile is removed.
- Manifest: register the configure activity with the
  `APPWIDGET_CONFIGURE` action.
- `honeydue_{small,medium,large}_widget_info.xml` — declare
  `android:configure="com.tt.honeyDue.widget.WidgetConfigActivity"`.

Migration / safety:
- A tile that's never been through the picker has no residence id
  saved → `loadTasksForWidget` returns every task (legacy "All
  residences" behaviour). Existing tiles keep working without the
  user touching anything.
- The picker handles an empty residences list (signed-out / first
  install before background refresh) with an explicit helper message
  pointing at the main app.

Tests: new `WidgetResidenceFilterTest` (commonTest-style under
`androidUnitTest`, 9 cases). All green.

  $ ./gradlew :composeApp:testDebugUnitTest \\
      --tests "com.tt.honeyDue.widget.WidgetResidenceFilterTest"
  BUILD SUCCESSFUL

  $ ./gradlew :composeApp:assembleDebug
  BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
admin changed title from feat(widget): per-residence widget configuration (iOS) — #6 to feat(widget): per-residence widget configuration — closes #6 2026-05-11 13:32:09 -05:00
admin merged commit 3a5e33af93 into master 2026-05-11 13:39:06 -05:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/honeyDueKMP#10