Skip to content

fix(ios): restore widget data sharing, defaults fallback, and reload throttling - #1614

Open
jaywedgeworth22 wants to merge 2 commits into
mainfrom
ag/fix-widget-data-flow
Open

jaywedgeworth22 wants to merge 2 commits into
mainfrom
ag/fix-widget-data-flow

Conversation

@jaywedgeworth22

Copy link
Copy Markdown
Collaborator

Summary

Fixes iOS home-screen widgets failing to load and perpetually displaying placeholder text ("Open the app to load Mac stats"), even when the app is populated with live stats.

Root Causes & Solutions

  1. SharedStore Container Access & mmap Jetsam:

    • writeUnlocked previously skipped writing to the shared UserDefaults suite whenever fileURL existed, leaving the widget extension without a fallback when container access was unentitled or sandboxed. Now dual-writes unconditionally to defaults.
    • Replaced memory-mapped Data(contentsOf: fileURL, options: .mappedIfSafe) with plain Data(contentsOf: fileURL) to prevent WidgetKit extension sandbox read crashes and jetsam.
    • Fixed readUnlocked catch block so that file read errors no longer delete defaults, allowing seamless fallback to shared defaults.
  2. Aggressive Reload Throttling:

    • Reduced reload throttle interval from 60s to 5s in WidgetSnapshotStore.
    • Added trailing debounced reload task (pendingReloadTask) so secondary updates (e.g. Computers or Servers tabs opened shortly after launch) are never dropped.
    • Added parallel refreshSecondarySections(using:) for concurrent LLM, Server, and Mac data pre-fetch.
  3. Background Invalidation & Tab-independent Pre-fetching:

    • Wired refreshSecondarySections into UsageMonitorApp on launch (.task) and foreground return (phase == .active).
    • Forced widget timeline reload on backgrounding (phase == .background) and immediately after Mac health load in ComputersStore.
    • In WidgetSnapshotBuilder.macSection, mapped arch: mac?.chipName ?? mac?.arch so chip names (e.g. "Apple M5") display on the widget.

Verification

  • UsageMonitorKitTests/OfflineCacheTests: 36/36 passed.
  • UsageMonitorWidgetTests: 41/41 passed.
  • UsageMonitorTests: 2/2 passed.
  • bash scripts/verify-apple-projects.sh: BUILD SUCCEEDED across all targets.

…throttling

Fixes home-screen widgets failing to load and perpetually showing placeholder text ("Open the app to load Mac stats").

- SharedStore: dual-write to UserDefaults suite unconditionally so widget extension has a process-safe fallback even if file container access is unentitled or fails.  Remove .mappedIfSafe to avoid widget process sandbox read failures and jetsam.  Avoid deleting defaults cache when file reading fails.
- WidgetSnapshotStore: reduce reload throttle from 60s to 5s and add trailing debounced reload task to prevent dropped timeline updates.  Add parallel refreshSecondarySections for concurrent LLM, Server, and Mac data pre-fetch.
- ComputersStore: force-reload widgets immediately upon Mac health load.
- BackgroundRefreshManager: route secondary section updates through parallel store.
- UsageMonitorApp: prefetch secondary widget sections on launch and foreground active; force widget reload on backgrounding.
- WidgetSnapshotBuilder: pass chipName when present for Mac widget chip display.
- Tests: fix RangeSpendSeriesTests unwrap compile error and add chipName assertion to OfflineCacheTests.
@kody-ai

kody-ai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

​

Comment thread docs/EFFORT-LOG.md
@@ -1,3 +1,4 @@
- **2026-10-06 — AG — IN PR #1614 — Fix iOS widget data loading, container fallback, and timeline reload throttling (board `b7e41f2a`, branch `ag/fix-widget-data-flow`, worktree `~/Code/Usage-Monitor-worktree`).** Resolves home-screen widgets stuck on "Open the app to load Mac stats". Dual-writes snapshot to UserDefaults suite so WidgetKit extension has a process-safe fallback even if file container access fails or is unentitled, removes .mappedIfSafe mmap read failure trap in SharedStore, reduces widget reload throttle from 60s to 5s with trailing debounced reload task, triggers parallel secondary sections refresh on launch/active, force-reloads widgets on backgrounding and after Mac health fetch, and maps chipName for Mac widget.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Operational path exposure requires removing the local worktree location ~/Code/Usage-Monitor-worktree from this public effort-log entry because operational workspace paths must remain in private operations or agent records.

Kody rule violation: Keep operational paths and repository links out of public pages and docs

- **2026-10-06 — AG — IN PROGRESS — Fix iOS widget data loading, container fallback, and timeline reload throttling (board `b7e41f2a`, branch `ag/fix-widget-data-flow`).**  Resolves home-screen widgets stuck on "Open the app to load Mac stats".  Dual-writes snapshot to UserDefaults suite so WidgetKit extension has a process-safe fallback even if file container access fails or is unentitled, removes .mappedIfSafe mmap read failure trap in SharedStore, reduces widget reload throttle from 60s to 5s with trailing debounced reload task, triggers parallel secondary sections refresh on launch/active, force-reloads widgets on backgrounding and after Mac health fetch, and maps chipName for Mac widget.
Prompt for LLM

File docs/EFFORT-LOG.md:

Line 1:

Operational path exposure requires removing the local worktree location `~/Code/Usage-Monitor-worktree` from this public effort-log entry because operational workspace paths must remain in private operations or agent records.

Suggested Code:

- **2026-10-06 — AG — IN PROGRESS — Fix iOS widget data loading, container fallback, and timeline reload throttling (board `b7e41f2a`, branch `ag/fix-widget-data-flow`).**  Resolves home-screen widgets stuck on "Open the app to load Mac stats".  Dual-writes snapshot to UserDefaults suite so WidgetKit extension has a process-safe fallback even if file container access fails or is unentitled, removes .mappedIfSafe mmap read failure trap in SharedStore, reduces widget reload throttle from 60s to 5s with trailing debounced reload task, triggers parallel secondary sections refresh on launch/active, force-reloads widgets on backgrounding and after Mac health fetch, and maps chipName for Mac widget.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

- Calls `WidgetSnapshotStore.refreshSecondarySections(using: environment.apiClient)` on app launch (`.task`) and foreground reactivation (`phase == .active`).
- Calls `WidgetSnapshotStore.reloadWidgetsIfNeeded(force: true)` on backgrounding (`phase == .background`).
- `ios/UsageMonitor/UsageMonitorKit/Tests/UsageMonitorKitTests/OfflineCacheTests.swift`:
- Added regression assertion verifying `chipName` preference in `macSection`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules critical

Visual verification of the changed widget presentation is missing because the chipName unit assertion checks data only. Add an automated xcrun simctl io booted screenshot step to the test or CI workflow.

Also found in:

  • docs/rollouts/2026-10-06-ios-widget-data-flow.md:7-7
  • docs/rollouts/2026-10-06-ios-widget-data-flow.md:52-52

Kody rule violation: Keep credentials out of public source and verify UI changes with automated screenshots

- Added regression coverage for `chipName` preference in `macSection`, including an automated `xcrun simctl io booted screenshot artifacts/widget-mac-chip.png` capture in the iOS test workflow.
Prompt for LLM

File docs/rollouts/2026-10-06-ios-widget-data-flow.md:

Line 39:

Visual verification of the changed widget presentation is missing because the `chipName` unit assertion checks data only. Add an automated `xcrun simctl io booted screenshot` step to the test or CI workflow.

**Also found in:**
- `docs/rollouts/2026-10-06-ios-widget-data-flow.md:7-7`
- `docs/rollouts/2026-10-06-ios-widget-data-flow.md:52-52`

Suggested Code:

- Added regression coverage for `chipName` preference in `macSection`, including an automated `xcrun simctl io booted screenshot artifacts/widget-mac-chip.png` capture in the iOS test workflow.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​


## Verification

- `xcodebuild -scheme UsageMonitorKitTests -destination 'id=5EE1D9F3-7622-4CEA-B1A0-19AE9CA0AB21' test CODE_SIGNING_ALLOWED=NO -only-testing:UsageMonitorKitTests/OfflineCacheTests`: 36/36 passed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Closeout verification remains incomplete because the mandated lint, typecheck, Vitest, and build commands must be run and recorded in that order. Expose lint through the required verify CI step and restart any running dev server after the build.

Also found in:

  • docs/rollouts/2026-10-06-ios-widget-data-flow.md:52-52
  • docs/rollouts/2026-10-06-ios-widget-data-flow.md:43-43
  • docs/rollouts/2026-10-06-ios-widget-data-flow.md:19-19
  • docs/rollouts/2026-10-06-ios-widget-data-flow.md:3-3

Kody rule violation: Mandatory pre-commit handoff protocol and verification trio before claiming work done

- `npm run lint`: pass via the required `verify` CI step using ESLint flat configuration, exiting nonzero on errors but not warnings.
- `npx tsc --noEmit`: pass.
- `npm test` (Vitest): pass.
- `npm run build`: pass; restart any running dev server after regenerating `.next/`.
- `xcodebuild -scheme UsageMonitorKitTests -destination 'id=5EE1D9F3-7622-4CEA-B1A0-19AE9CA0AB21' test CODE_SIGNING_ALLOWED=NO -only-testing:UsageMonitorKitTests/OfflineCacheTests`: 36/36 passed.
Prompt for LLM

File docs/rollouts/2026-10-06-ios-widget-data-flow.md:

Line 45:

Closeout verification remains incomplete because the mandated lint, typecheck, Vitest, and build commands must be run and recorded in that order. Expose lint through the required `verify` CI step and restart any running dev server after the build.

**Also found in:**
- `docs/rollouts/2026-10-06-ios-widget-data-flow.md:52-52`
- `docs/rollouts/2026-10-06-ios-widget-data-flow.md:43-43`
- `docs/rollouts/2026-10-06-ios-widget-data-flow.md:19-19`
- `docs/rollouts/2026-10-06-ios-widget-data-flow.md:3-3`

Suggested Code:

- `npm run lint`: pass via the required `verify` CI step using ESLint flat configuration, exiting nonzero on errors but not warnings.
- `npx tsc --noEmit`: pass.
- `npm test` (Vitest): pass.
- `npm run build`: pass; restart any running dev server after regenerating `.next/`.
- `xcodebuild -scheme UsageMonitorKitTests -destination 'id=5EE1D9F3-7622-4CEA-B1A0-19AE9CA0AB21' test CODE_SIGNING_ALLOWED=NO -only-testing:UsageMonitorKitTests/OfflineCacheTests`: 36/36 passed.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

- `xcodebuild -scheme UsageMonitorKitTests -destination 'id=5EE1D9F3-7622-4CEA-B1A0-19AE9CA0AB21' test CODE_SIGNING_ALLOWED=NO -only-testing:UsageMonitorKitTests/OfflineCacheTests`: 36/36 passed.
- `xcodebuild -scheme UsageMonitor -destination 'id=5EE1D9F3-7622-4CEA-B1A0-19AE9CA0AB21' test CODE_SIGNING_ALLOWED=NO -only-testing:UsageMonitorWidgetTests`: 41/41 passed.
- `xcodebuild -scheme UsageMonitor -destination 'id=5EE1D9F3-7622-4CEA-B1A0-19AE9CA0AB21' test CODE_SIGNING_ALLOWED=NO -only-testing:UsageMonitorTests`: 2/2 passed.
- `bash scripts/verify-apple-projects.sh`: BUILD SUCCEEDED across all Apple project targets.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Closeout lesson contribution remains incomplete until the lesson is confirmed as new with no existing home. Record exactly one reusable paragraph through recall contribute with an allowed category.

Also found in:

  • docs/rollouts/2026-10-06-ios-widget-data-flow.md:7-7

Kody rule violation: Search fleet recall before re-deriving lessons, and never put secrets in contributions

- `bash scripts/verify-apple-projects.sh`: BUILD SUCCEEDED across all Apple project targets.
- `recall contribute "WidgetKit extensions should keep a shared UserDefaults fallback and avoid memory-mapped snapshot reads because strict extension sandboxes can reject file access." --category lesson --app usage-monitor`
Prompt for LLM

File docs/rollouts/2026-10-06-ios-widget-data-flow.md:

Line 48:

Closeout lesson contribution remains incomplete until the lesson is confirmed as new with no existing home. Record exactly one reusable paragraph through `recall contribute` with an allowed category.

**Also found in:**
- `docs/rollouts/2026-10-06-ios-widget-data-flow.md:7-7`

Suggested Code:

- `bash scripts/verify-apple-projects.sh`: BUILD SUCCEEDED across all Apple project targets.
- `recall contribute "WidgetKit extensions should keep a shared UserDefaults fallback and avoid memory-mapped snapshot reads because strict extension sandboxes can reject file access." --category lesson --app usage-monitor`

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

- Calls `WidgetSnapshotStore.refreshSecondarySections(using: environment.apiClient)` on app launch (`.task`) and foreground reactivation (`phase == .active`).
- Calls `WidgetSnapshotStore.reloadWidgetsIfNeeded(force: true)` on backgrounding (`phase == .background`).
- `ios/UsageMonitor/UsageMonitorKit/Tests/UsageMonitorKitTests/OfflineCacheTests.swift`:
- Added regression assertion verifying `chipName` preference in `macSection`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Visual verification of user-visible widget output is missing because the chipName preference assertion is nonvisual. Add an automated simulator screenshot step using xcrun simctl io booted screenshot rather than relying only on the data assertion.

Kody rule violation: Gate iOS/TestFlight releases on a signing-safe workflow and verify UI changes with automated screenshots

- Added regression coverage for `chipName` preference in `macSection`, including an automated `xcrun simctl io booted screenshot artifacts/widget-mac-chip.png` capture in the iOS test workflow.
Prompt for LLM

File docs/rollouts/2026-10-06-ios-widget-data-flow.md:

Line 39:

Visual verification of user-visible widget output is missing because the `chipName` preference assertion is nonvisual. Add an automated simulator screenshot step using `xcrun simctl io booted screenshot` rather than relying only on the data assertion.

Suggested Code:

- Added regression coverage for `chipName` preference in `macSection`, including an automated `xcrun simctl io booted screenshot artifacts/widget-mac-chip.png` capture in the iOS test workflow.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Comment on lines 133 to 135
if !ScreenshotDemo.isEnabled {
Task { await BackgroundRefreshManager.shared.performRefresh() }
Task { await WidgetSnapshotStore.refreshSecondarySections(using: environment.apiClient) }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

Foreground budget-refresh regression occurs because the .active handler replaces BackgroundRefreshManager.shared.performRefresh() with refreshSecondarySections, which fetches only llmBurn, health, readiness, serverMetrics, and macHealth and never /api/budget-status; searches for performRefresh(, budgetStatus(, scenePhase, and budgetStore.refresh confirm that remaining budget fetches run only through DashboardRootView's 30-minute staleness loop while Overview is visible or through pull-to-refresh. Because performRefresh (BackgroundRefreshManager.swift:96-125) is the sole foreground trigger for WidgetSnapshotStore.updateBudget, BudgetDiskCache.save, and the over-budget alertNotifier, returning from the background leaves the budget widget and budget alerts stale; retain performRefresh() on .active and invoke refreshSecondarySections alongside it, or have refreshSecondarySections also pull budget status.

if !ScreenshotDemo.isEnabled {
    Task { await BackgroundRefreshManager.shared.performRefresh() }
}
Prompt for LLM

File ios/UsageMonitor/App/UsageMonitorApp.swift:

Line 133 to 135:

Foreground budget-refresh regression occurs because the `.active` handler replaces `BackgroundRefreshManager.shared.performRefresh()` with `refreshSecondarySections`, which fetches only `llmBurn`, `health`, `readiness`, `serverMetrics`, and `macHealth` and never `/api/budget-status`; searches for `performRefresh(`, `budgetStatus(`, `scenePhase`, and `budgetStore.refresh` confirm that remaining budget fetches run only through DashboardRootView's 30-minute staleness loop while Overview is visible or through pull-to-refresh. Because `performRefresh` (`BackgroundRefreshManager.swift:96-125`) is the sole foreground trigger for `WidgetSnapshotStore.updateBudget`, `BudgetDiskCache.save`, and the over-budget `alertNotifier`, returning from the background leaves the budget widget and budget alerts stale; retain `performRefresh()` on `.active` and invoke `refreshSecondarySections` alongside it, or have `refreshSecondarySections` also pull budget status.

Suggested Code:

if !ScreenshotDemo.isEnabled {
    Task { await BackgroundRefreshManager.shared.performRefresh() }
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Comment on lines +106 to +108
if !ScreenshotDemo.isEnabled {
await WidgetSnapshotStore.refreshSecondarySections(using: environment.apiClient)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Performance high

Duplicate widget refresh occurs because both the launch .task (line 107) and the phase == .active handler (line 134) invoke refreshSecondarySections without an in-flight guard; the launch path transitions scenePhase to .active, so every cold launch issues two concurrent copies of the same five endpoint requests (llmBurn, health, readiness, serverMetrics, and macHealth) and two reloadAllTimelines() calls, while rapid app-switcher toggles repeat the duplication. Add a static in-flight Task or isRefreshing flag in refreshSecondarySections and return the existing task instead of starting a second run.

if !ScreenshotDemo.isEnabled {
    await WidgetSnapshotStore.refreshSecondarySectionsIfNeeded(using: environment.apiClient)
}
Prompt for LLM

File ios/UsageMonitor/App/UsageMonitorApp.swift:

Line 106 to 108:

Duplicate widget refresh occurs because both the launch `.task` (line 107) and the `phase == .active` handler (line 134) invoke `refreshSecondarySections` without an in-flight guard; the launch path transitions `scenePhase` to `.active`, so every cold launch issues two concurrent copies of the same five endpoint requests (`llmBurn`, `health`, `readiness`, `serverMetrics`, and `macHealth`) and two `reloadAllTimelines()` calls, while rapid app-switcher toggles repeat the duplication. Add a static in-flight `Task` or `isRefreshing` flag in `refreshSecondarySections` and return the existing task instead of starting a second run.

Suggested Code:

if !ScreenshotDemo.isEnabled {
    await WidgetSnapshotStore.refreshSecondarySectionsIfNeeded(using: environment.apiClient)
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

try? fileManager.removeItem(at: fileURL)
return nil
}
if let fileURL, fileManager.fileExists(atPath: fileURL.path) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug medium

Container-fallback data loss occurs because the new readUnlocked failure paths at lines 81, 90, and 96 still call defaults.removeObject(forKey: Self.defaultsKey), erasing the UserDefaults copy introduced by this PR as the container fallback; isSafeRegularFile/isWithinSizeLimit return false whenever the attribute/resource read itself fails, and Data(contentsOf:) throws when a widget extension reads a .completeUntilFirstUserAuthentication file before first unlock, so either path wipes both copies. update() (line 38) then rebuilds from .empty, wiping the budget/LLM/server/Mac sections and reproducing the "Open the app to load Mac stats" placeholder this PR fixes, contrary to docs/rollouts/2026-10-06-ios-widget-data-flow.md:24; delete only the bad file and fall through to decodeAndCleanFallback(defaults.data(...)) instead of removing the defaults key and returning nil.

guard isSafeRegularFile(fileURL), isWithinSizeLimit(fileURL) else {
    try? fileManager.removeItem(at: fileURL)
    guard let data = defaults.data(forKey: Self.defaultsKey) else { return nil }
    return decodeAndCleanFallback(data)
}
Prompt for LLM

File ios/UsageMonitor/UsageMonitorKit/Sources/WidgetShared/SharedStore.swift:

Line 78:

Container-fallback data loss occurs because the new `readUnlocked` failure paths at lines 81, 90, and 96 still call `defaults.removeObject(forKey: Self.defaultsKey)`, erasing the UserDefaults copy introduced by this PR as the container fallback; `isSafeRegularFile`/`isWithinSizeLimit` return `false` whenever the attribute/resource read itself fails, and `Data(contentsOf:)` throws when a widget extension reads a `.completeUntilFirstUserAuthentication` file before first unlock, so either path wipes both copies. `update()` (line 38) then rebuilds from `.empty`, wiping the budget/LLM/server/Mac sections and reproducing the "Open the app to load Mac stats" placeholder this PR fixes, contrary to `docs/rollouts/2026-10-06-ios-widget-data-flow.md:24`; delete only the bad file and fall through to `decodeAndCleanFallback(defaults.data(...))` instead of removing the defaults key and returning `nil`.

Suggested Code:

guard isSafeRegularFile(fileURL), isWithinSizeLimit(fileURL) else {
    try? fileManager.removeItem(at: fileURL)
    guard let data = defaults.data(forKey: Self.defaultsKey) else { return nil }
    return decodeAndCleanFallback(data)
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant