perf(dashboard): stop refetching on every tab switch; own the QueryClient

Two changes, together removing nearly all the redundant conversation/history
calls visible in the network panel.

Keep-alive modules: Messenger and Inbox were rendered from the tab ternary, so
every tab change UNMOUNTED them — all their state died and returning re-hit the
API. They now mount on first visit and stay mounted, hidden with CSS. Deliberately
mount-on-first-visit rather than eager-mount-both, so opening the dashboard does
not fire two modules' initial loads for a user who never opens either.

QueryClient ownership: the SDK (0.2.0) takes React Query as a peer and falls back
to its own client if it can't find one — but then its cache would be invisible to
the CRM. Providing it here keeps SDK and CRM data in one store, so a write can
invalidate a conversation list. Client is created in useState, not at module
scope, so an SSR render can't leak one user's cache into another's.
refetchOnWindowFocus is off: the socket already pushes live changes, so focus
refetching was pure noise.

Also declares onnxruntime-web@1.21.0, a PINNED peerDependency of
@imgly/background-removal that npm prunes on any re-resolve. The committed lock
had it, so `npm ci` worked and `npm install` silently broke the gallery build —
declaring it directly makes it unprunable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 13:41:16 +05:30
parent 4b86e0d3cc
commit 7b806da14c
6 changed files with 9726 additions and 1711 deletions
+25 -3
View File
@@ -21,6 +21,7 @@ import { Settings } from "./settings";
import { Projects } from "./projects";
import { NotificationCenter } from "./notification-center";
import { RealtimeProvider } from "@/lib/realtime";
import { CrmQueryProvider } from "@/lib/query-client";
import { SmartGallery } from "./smart-gallery";
import { Leads } from "./leads";
import { Verify } from "./verify";
@@ -32,6 +33,16 @@ export function Dashboard() {
const [active, setActive] = useState("dashboard");
// Deep link from global search: which conversation to focus once we switch tabs.
const [deepLink, setDeepLink] = useState<{ surface: "messenger" | "inbox"; threadId: string } | null>(null);
// Messenger and Inbox are KEPT MOUNTED once opened, and hidden with CSS when you switch tabs.
// Rendering them from the ternary below unmounted them on every tab change, which threw away
// their sockets' worth of state and re-hit the API on return. Mount-on-first-visit rather than
// eager-mount-both, so the dashboard doesn't fire two modules' initial loads at boot.
const [everOpened, setEverOpened] = useState<Set<string>>(new Set());
useEffect(() => {
if (active === "messenger" || active === "inbox") {
setEverOpened((prev) => (prev.has(active) ? prev : new Set(prev).add(active)));
}
}, [active]);
function navigateToConversation(surface: "messenger" | "inbox", threadId: string) {
setActive(surface);
@@ -72,6 +83,7 @@ export function Dashboard() {
return (
<div className="dash-root" data-theme={theme}>
<CrmQueryProvider>
<RealtimeProvider>
<Sidebar active={active} onSelect={setActive} />
<div className="dash-main">
@@ -79,12 +91,21 @@ export function Dashboard() {
<div className="dash-content">
<ToastProvider>
<NotificationCenter active={active} onNavigate={navigateToConversation} />
{active === "profile" ? <Profile />
{everOpened.has("messenger") ? (
<div hidden={active !== "messenger"} className="dash-keepalive">
<MessengerSdk focusThreadId={deepLink?.surface === "messenger" ? deepLink.threadId : null} />
</div>
) : null}
{everOpened.has("inbox") ? (
<div hidden={active !== "inbox"} className="dash-keepalive">
<InboxSdk focusThreadId={deepLink?.surface === "inbox" ? deepLink.threadId : null} />
</div>
) : null}
{active === "messenger" || active === "inbox" ? null
: active === "profile" ? <Profile />
: active === "support" ? <Support />
: active === "rules" ? <Rules />
: active === "ai" ? <AiAssistant />
: active === "messenger" ? <MessengerSdk focusThreadId={deepLink?.surface === "messenger" ? deepLink.threadId : null} />
: active === "inbox" ? <InboxSdk focusThreadId={deepLink?.surface === "inbox" ? deepLink.threadId : null} />
: active === "settings" ? <Settings />
: active === "gallery" ? <SmartGallery theme={theme} />
: active === "leads" ? <Leads />
@@ -97,6 +118,7 @@ export function Dashboard() {
</div>
</div>
</RealtimeProvider>
</CrmQueryProvider>
</div>
);
}