Skip to content

fix(dashboard): keep rows loading when the split view re-adds Home - #1097

Merged
everpcpc merged 1 commit into
kmworks:mainfrom
spawvn:fix/dashboard-section-loading
Sep 24, 2026
Merged

everpcpc merged 1 commit into
kmworks:mainfrom
spawvn:fix/dashboard-section-loading

Conversation

@spawvn

@spawvn spawvn commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Closes #1096

Problem

On iPad, switching the sidebar back to Home leaves the dashboard rows on stale cached content until pull-to-refresh. It's most visible after reading a book opened from another page. Cause: SwiftUI adds, removes (about 1 ms later), and re-adds the dashboard while keeping its state. Each row's view .task is cancelled mid-request, and its load-once flag (pinned rows: isLoading guard) then blocks the retry. Full trace in #1096.

Approach

Rows load through view models they own, following the same direction as #1069's OfflineView browse models:

  • DashboardSectionViewModel owns the row's pagination and its load task. The view's .task is gone: onAppear calls ensureLoaded(), which starts a first-page load only when none has completed or is running. A load in flight survives the view being removed and re-added.
  • Newest request wins. reload() cancels a load in flight and restarts from the first page instead of being dropped by an isLoading guard, and it returns once the newest load settles, so manual refreshes still acknowledge after content lands. Superseded loads end quietly (loadID check, no error alert). Loaded items stay on screen until the new first page replaces them, as before.
  • DashboardPinnedSectionViewModel does the same for pinned collections and read lists. They keep their current behavior of refreshing (local items, then sync) on every appear, but join a refresh that's already running for the same server, and a reload command restarts it.
  • DashboardRefreshCoordinator's register/acknowledge flow is unchanged.
  • AGENTS.md documents the rule: rows load through view models, never in a view .task, with no load-once flags or isLoading guards that drop a re-run.

Why this shape, not another

  • Robust rows rather than preventing the re-add: the add/remove/re-add comes from swapping the detail NavigationStack's root under NavigationSplitView, and views get removed and re-added in other cases too, like tab switches mid-load on iPhone. Loading shouldn't depend on a view task surviving.
  • No memory in the coordinator for reloads posted while the dashboard isn't mounted: a dashboard created when Home is selected loads current data anyway.
  • Row-owned models, not models owned by DashboardView: each row already reloads through the coordinator's commands, so nothing needs to reach the models from above. Moving them up (and replacing the acknowledgement tracking with direct awaits) is possible later without changing the rows' behavior.
  • DashboardSectionDetailView is untouched: it's pushed onto the stack rather than swapped in as the root, so this trigger doesn't reach it.

Validation

  • make build passes on iOS, macOS, and tvOS.
  • iPad A16 simulator against a local Komga, with temporary appear/disappear/task logging:
    • Before: Read Lists → read list → read a book → close → Home. onAppear → onDisappear 1 ms later → onAppear 19 ms later. Every row's request, plus the pinned read list/collection syncs, failed with Request cancelled. Keep Reading stayed without the book just read.
    • After: same steps. All row requests return 200, the pinned syncs complete (Synced readlists, Synced collections), and Keep Reading lists the book just read first, with no pull-to-refresh.
  • Pull-to-refresh still waits for every row before dismissing.
  • Closing a reader opened from the dashboard itself reloads the rows. The close refresh and the deferred flush arrived 70 ms apart, and the newer load won.

Manual test plan

  1. iPad: Read Lists → open a read list → read a few pages of a book → close → tap Home → Keep Reading shows that book without pull-to-refresh.
  2. Same with Series/Books as the starting page.
  3. Pull-to-refresh on the dashboard → the spinner stays until the rows have reloaded.
  4. Open a book from Keep Reading, finish it, close → Keep Reading and Recently Read update in place.
  5. iPhone: switch tabs while the dashboard is still loading → returning to Home shows loaded rows.

Note

#1095 also touches DashboardSectionView; I'll rebase whichever of the two lands second.

🤖 Generated with Claude Code

On iPad, switching the sidebar back to Home (for example after reading
a book opened from a read list) left the dashboard rows on stale cached
items until pull-to-refresh.

Selecting Home swaps the root of the split view's detail NavigationStack,
and SwiftUI adds, removes (about 1 ms later), and re-adds the dashboard
while keeping its state. Each row loaded in its view .task, which the
removal cancelled mid-request; on re-add the task ran again but the
row's load-once flag returned early, so the row kept its cache seed.
Pinned rows re-ran their .task(id:), but their isLoading guard dropped
the re-run while the cancelled one was unwinding, skipping their sync.

Rows now load through view models they own (DashboardSectionViewModel,
DashboardPinnedSectionViewModel). Loads run in tasks the model owns, so
view cancellation cannot interrupt them; appearing starts a load only
when none has completed or is running (pinned rows refresh on every
appear but join a running refresh), and a reload supersedes a load in
flight instead of being dropped by its guard.

The reader's close refresh still has no mounted dashboard to reach
while another sidebar page is showing; the dashboard created when Home
is selected loads current data, so it no longer matters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@everpcpc
everpcpc merged commit 88cff9b into kmworks:main Sep 24, 2026
3 checks passed
everpcpc added a commit that referenced this pull request Sep 25, 2026
…anup (#1101)

Follow-up nits from the review of #1097, no behavior change. DashboardPinnedSectionViewModel.refreshIfIdle is now refreshIfNeeded — when a refresh is running for a different server it cancels and restarts it, so the old name undersold the behavior. The pass-through computed properties in DashboardSectionView and DashboardPinnedSectionView are gone; the views read the view model directly. AGENTS.md's dashboard row rule now states that pinned rows join a running refresh for the same server, matching the guard the code implements.
spawvn added a commit to spawvn/KMReader that referenced this pull request Sep 25, 2026
Resolves the conflict with kmworks#1097, which moved dashboard row loading
into DashboardSectionViewModel: the On Deck read list merge now runs in
the view model's loadPage (after the fetch, re-checking the load id
after the merge) and the row view's snapshot observer calls
reloadIfReadListsOutdated, which supersedes through startReload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spawvn
spawvn deleted the fix/dashboard-section-loading branch September 25, 2026 12:03
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.

iPad: dashboard rows stay stale after returning to Home until pull-to-refresh

2 participants