From f1e81357b616ef9b218fcce827e341b5194c571a Mon Sep 17 00:00:00 2001 From: jelveh Date: Sat, 18 Jul 2026 15:04:19 -0700 Subject: [PATCH] fix: guard Dashboard Apps loads against stale overwrites and drag/error clobbering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadApps only checked for an in-progress drag before its await, and its catch wiped whatever was on screen. Three concurrency issues followed: - A slow, older load resolving after a newer one could overwrite the newer app list (and clobber a reorder the user saved while the stale fetch was in flight). Tag each load with an increasing id and skip applying one only when a strictly newer load has already applied — gating on "already applied" (not "latest started") so the first load to resolve still populates the list for the pager's ResizeObserver. - A drag that began while a load was awaiting could have the grid rebuilt out from under it; re-check the drag after the await. - A transient re-fetch error replaced a working grid with "Failed to load apps"; only show that placeholder when nothing has loaded yet. Verified live: a slow older load no longer clobbers a newer one, and the grid still renders normally on activation. --- src/gui/src/UI/Dashboard/TabApps.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index 4f028a55b..3146a8cd8 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -905,6 +905,15 @@ const TabApps = { this._endDrag(false); } + // Give each load a monotonically increasing id. Concurrent loads are + // routine (init + initial-route onActivate both fire on open); an + // older/slower response must not clobber a newer one that already + // applied — or a reorder the user saved while a stale fetch was in + // flight. We gate on "already applied", not "latest started", so the + // first load to resolve still populates _apps (the pager's + // ResizeObserver needs _apps set as soon as any load resolves). + const loadSeq = (this._loadSeq = (this._loadSeq || 0) + 1); + const $container = $el_window.find('.myapps-container'); try { @@ -988,13 +997,24 @@ const TabApps = { } catch ( _e ) { orderedNames = null; } + // Skip only if a strictly newer load already applied its result, or + // a drag began while we were awaiting (rendering would yank the grid + // out from under it). + if ( loadSeq < (this._appliedSeq || 0) ) return; + if ( this._drag?.started ) return; + this._appliedSeq = loadSeq; + this._hasCustomOrder = Array.isArray(orderedNames) && orderedNames.length > 0; this._apps = reconcileAppOrder(merged, orderedNames); this.renderApps($el_window); } catch (e) { console.error('Failed to load installed apps:', e); - $container.html('

Failed to load apps

'); + // Only show the failure placeholder when nothing has loaded yet; a + // transient re-fetch error must not wipe a grid already on screen. + if ( ! this._apps ) { + $container.html('

Failed to load apps

'); + } } },