[eric] dashboard: don't wipe layout mid-drag + never route to a phantom (deleted) dashboard id (dragging cards was silently broken by a spurious layout reset)

This commit is contained in:
ciregenz
2026-07-15 18:07:38 -07:00
parent c50947edad
commit d4db6e7c9d
2 changed files with 13 additions and 8 deletions
@@ -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/<phantom>, 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 {
@@ -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', () => {