diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index 2b47615c..c28c51e8 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -255,7 +255,9 @@ const AppShell: React.FC = () => { dispatch(setPendingBrowserUrl(url)); const lastId = (window as any).__openswarm_last_dashboard_id as string | undefined; const firstDashboard = dashboardList[0]; - const targetId = lastId || firstDashboard?.id; + // Only navigate to lastId if it's a REAL dashboard: a stale localStorage id for a deleted dashboard used to route to /dashboard/, which 404s and re-fires the layout wipe (drops your cards / breaks a drag). + const lastIsReal = !!lastId && dashboardList.some((d) => d.id === lastId); + const targetId = (lastIsReal ? lastId : undefined) || firstDashboard?.id; if (targetId) { navigate(`/dashboard/${targetId}`); } else { diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts index bc4f2f55..d2d65512 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts @@ -113,13 +113,16 @@ export function useDashboardLifecycle({ useEffect(() => { if (!dashboardId) return; - hasFittedRef.current = false; - restoredExpandedRef.current = false; - setOutputsRefetched(false); - dispatch(resetLayout({ keepBrowserIds: getKeepAliveBrowserIds() })); - // CRITICAL path: these populate the cards the user expects to see on first paint. Don't defer. - dispatch(fetchSessions({ dashboardId })); - dispatch(fetchLayout({ dashboardId })); + // Never wipe+reload the layout while a card drag or marquee is in flight: a spurious mid-gesture nav (e.g. a phantom-dashboard round-trip) would unmount the card under the cursor and the drag silently dies. You can't switch dashboards while holding a drag, so any reset firing now is spurious. The shield class is up for exactly that window. Handlers below still install. + if (!document.body.classList.contains('dashboard-marquee-active')) { + hasFittedRef.current = false; + restoredExpandedRef.current = false; + setOutputsRefetched(false); + dispatch(resetLayout({ keepBrowserIds: getKeepAliveBrowserIds() })); + // CRITICAL path: these populate the cards the user expects to see on first paint. Don't defer. + dispatch(fetchSessions({ dashboardId })); + dispatch(fetchLayout({ dashboardId })); + } const cleanupBrowserHandler = initBrowserCommandHandler(); // Global broadcasts (spawned browser cards) skip the replay log, so a socket gap loses them; a reconnect refetch is the only way they return. const unsubReconnect = dashboardWs.on('dashboard:reconnected', () => {