diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index cb34a6a..4ecb7eb 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -73,7 +73,16 @@ export function Sidebar() {
const { hasPermission } = usePermission();
const user = useAuthStore((state) => state.user);
const { isModuleVisible } = useWorkspace();
- const moduleMenuOrder = useWorkspaceStore(s => s.moduleMenuOrder());
+ // NOTE: never select object-factory calls in a Zustand selector —
+ // `s.moduleMenuOrder()` creates a NEW Map on every getSnapshot call,
+ // which destabilizes useSyncExternalStore and causes a synchronous
+ // re-render loop that starves pending Suspense (lazy route) commits
+ // ("Dashboard loads forever" root cause). Select stable state and
+ // derive with useMemo instead.
+ const workspaceContext = useWorkspaceStore(s => s.context);
+ const moduleMenuOrder = useMemo(() => new Map(
+ (workspaceContext?.modules ?? []).map(m => [m.module_key, m.menu_order] as const),
+ ), [workspaceContext]);
// Use hasPermission directly — permissions are loaded via useUserPermissions hook
const canAccess = (perm?: string): boolean => {
diff --git a/frontend/src/hooks/useWorkspace.ts b/frontend/src/hooks/useWorkspace.ts
index 9907f3e..27bd781 100644
--- a/frontend/src/hooks/useWorkspace.ts
+++ b/frontend/src/hooks/useWorkspace.ts
@@ -1,4 +1,4 @@
-import { useEffect, useCallback } from 'react';
+import { useEffect, useCallback, useMemo } from 'react';
import { useMyWorkspaces, useWorkspaceContext } from '@/api/hooks/workspaces';
import { useWorkspaceStore, type WorkspaceInfo, type WorkspaceContextState } from '@/store/workspaceStore';
import { useAuthStore } from '@/store/authStore';
@@ -62,7 +62,13 @@ export function useWorkspace() {
return isModuleVisibleFromStore(moduleKey, isSystemAdmin);
}, [isModuleVisibleFromStore, isSystemAdmin]);
- const visibleModuleKeys = useWorkspaceStore(s => s.visibleModuleKeys());
+ // NOTE: derive from the stable `context` reference via useMemo —
+ // `s.visibleModuleKeys()` would create a NEW Set on every getSnapshot
+ // call, destabilizing useSyncExternalStore (same root cause as the
+ // Sidebar moduleMenuOrder infinite re-render loop).
+ const visibleModuleKeys = useMemo(() => new Set(
+ (context?.modules ?? []).filter(m => m.is_visible !== false).map(m => m.module_key),
+ ), [context]);
return {
activeWorkspaceId,
diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
index 57663f2..a2c510a 100644
--- a/frontend/src/routes/index.tsx
+++ b/frontend/src/routes/index.tsx
@@ -94,8 +94,6 @@ const router = createBrowserRouter([
path: '/login',
element: ,
},
- {
- },
{
path: '/dms-standalone',
element: {withSuspense()},