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
+38 -10
View File
@@ -12,8 +12,9 @@
"@huggingface/transformers": "^4.2.0", "@huggingface/transformers": "^4.2.0",
"@imgly/background-removal": "^1.7.0", "@imgly/background-removal": "^1.7.0",
"@insignia/iios-kernel-client": "^0.1.6", "@insignia/iios-kernel-client": "^0.1.6",
"@insignia/iios-messaging-ui": "^0.1.17", "@insignia/iios-messaging-ui": "^0.2.0",
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk", "@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
"@tanstack/react-query": "^5.101.4",
"@tensorflow-models/coco-ssd": "^2.2.3", "@tensorflow-models/coco-ssd": "^2.2.3",
"@tensorflow/tfjs": "^4.22.0", "@tensorflow/tfjs": "^4.22.0",
"@vladmandic/face-api": "^1.7.15", "@vladmandic/face-api": "^1.7.15",
@@ -21,6 +22,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^1.21.0", "lucide-react": "^1.21.0",
"next": "16.2.9", "next": "16.2.9",
"onnxruntime-web": "^1.21.0",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
@@ -1110,11 +1112,12 @@
} }
}, },
"node_modules/@insignia/iios-messaging-ui": { "node_modules/@insignia/iios-messaging-ui": {
"version": "0.1.17", "version": "0.2.0",
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.17/iios-messaging-ui-0.1.17.tgz", "resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.2.0/iios-messaging-ui-0.2.0.tgz",
"integrity": "sha512-pob/ldsvloFZ15BUyDyVdZb34MYQrH6aFAV27tleTgaJhnOeDf4HlJe19ftpW8S/h2TNhFMRhv6eOnYRLzEtxQ==", "integrity": "sha512-eM3K9CMtPumTvpoL7FrX+RcdKylhIDgMDZ4/7GCGoe6P1lj+HcAMmUX38cyLx7qq21DaDMnUcq3RNMW69gsDNw==",
"peerDependencies": { "peerDependencies": {
"@insignia/iios-kernel-client": "*", "@insignia/iios-kernel-client": "*",
"@tanstack/react-query": ">=5",
"react": ">=18", "react": ">=18",
"react-dom": ">=18" "react-dom": ">=18"
}, },
@@ -1823,6 +1826,32 @@
"tailwindcss": "4.3.1" "tailwindcss": "4.3.1"
} }
}, },
"node_modules/@tanstack/query-core": {
"version": "5.101.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
"integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.101.4",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
"integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.101.4"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-virtual": { "node_modules/@tanstack/react-virtual": {
"version": "3.14.8", "version": "3.14.8",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.8.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.8.tgz",
@@ -2072,7 +2101,7 @@
"version": "19.2.17", "version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -3454,7 +3483,7 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
@@ -6639,7 +6668,7 @@
"version": "1.21.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0.tgz", "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0.tgz",
"integrity": "sha512-adzOe+7uI7lKz6pQNbAsLMQd2Fq5Jhmoxd8LZjJr8m3KvbFyiYyRxRiC57/XXD+jb18voppjeGAjoZmskXG+7A==", "integrity": "sha512-adzOe+7uI7lKz6pQNbAsLMQd2Fq5Jhmoxd8LZjJr8m3KvbFyiYyRxRiC57/XXD+jb18voppjeGAjoZmskXG+7A==",
"peer": true, "license": "MIT",
"dependencies": { "dependencies": {
"flatbuffers": "^25.1.24", "flatbuffers": "^25.1.24",
"guid-typescript": "^1.0.9", "guid-typescript": "^1.0.9",
@@ -6653,14 +6682,13 @@
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0", "license": "Apache-2.0"
"peer": true
}, },
"node_modules/onnxruntime-web/node_modules/onnxruntime-common": { "node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
"version": "1.21.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
"integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==",
"peer": true "license": "MIT"
}, },
"node_modules/opencollective-postinstall": { "node_modules/opencollective-postinstall": {
"version": "2.0.3", "version": "2.0.3",
+3 -1
View File
@@ -14,8 +14,9 @@
"@huggingface/transformers": "^4.2.0", "@huggingface/transformers": "^4.2.0",
"@imgly/background-removal": "^1.7.0", "@imgly/background-removal": "^1.7.0",
"@insignia/iios-kernel-client": "^0.1.6", "@insignia/iios-kernel-client": "^0.1.6",
"@insignia/iios-messaging-ui": "^0.1.17", "@insignia/iios-messaging-ui": "^0.2.0",
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk", "@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
"@tanstack/react-query": "^5.101.4",
"@tensorflow-models/coco-ssd": "^2.2.3", "@tensorflow-models/coco-ssd": "^2.2.3",
"@tensorflow/tfjs": "^4.22.0", "@tensorflow/tfjs": "^4.22.0",
"@vladmandic/face-api": "^1.7.15", "@vladmandic/face-api": "^1.7.15",
@@ -23,6 +24,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^1.21.0", "lucide-react": "^1.21.0",
"next": "16.2.9", "next": "16.2.9",
"onnxruntime-web": "^1.21.0",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
File diff suppressed because it is too large Load Diff
+25 -3
View File
@@ -21,6 +21,7 @@ import { Settings } from "./settings";
import { Projects } from "./projects"; import { Projects } from "./projects";
import { NotificationCenter } from "./notification-center"; import { NotificationCenter } from "./notification-center";
import { RealtimeProvider } from "@/lib/realtime"; import { RealtimeProvider } from "@/lib/realtime";
import { CrmQueryProvider } from "@/lib/query-client";
import { SmartGallery } from "./smart-gallery"; import { SmartGallery } from "./smart-gallery";
import { Leads } from "./leads"; import { Leads } from "./leads";
import { Verify } from "./verify"; import { Verify } from "./verify";
@@ -32,6 +33,16 @@ export function Dashboard() {
const [active, setActive] = useState("dashboard"); const [active, setActive] = useState("dashboard");
// Deep link from global search: which conversation to focus once we switch tabs. // Deep link from global search: which conversation to focus once we switch tabs.
const [deepLink, setDeepLink] = useState<{ surface: "messenger" | "inbox"; threadId: string } | null>(null); 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) { function navigateToConversation(surface: "messenger" | "inbox", threadId: string) {
setActive(surface); setActive(surface);
@@ -72,6 +83,7 @@ export function Dashboard() {
return ( return (
<div className="dash-root" data-theme={theme}> <div className="dash-root" data-theme={theme}>
<CrmQueryProvider>
<RealtimeProvider> <RealtimeProvider>
<Sidebar active={active} onSelect={setActive} /> <Sidebar active={active} onSelect={setActive} />
<div className="dash-main"> <div className="dash-main">
@@ -79,12 +91,21 @@ export function Dashboard() {
<div className="dash-content"> <div className="dash-content">
<ToastProvider> <ToastProvider>
<NotificationCenter active={active} onNavigate={navigateToConversation} /> <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 === "support" ? <Support />
: active === "rules" ? <Rules /> : active === "rules" ? <Rules />
: active === "ai" ? <AiAssistant /> : 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 === "settings" ? <Settings />
: active === "gallery" ? <SmartGallery theme={theme} /> : active === "gallery" ? <SmartGallery theme={theme} />
: active === "leads" ? <Leads /> : active === "leads" ? <Leads />
@@ -97,6 +118,7 @@ export function Dashboard() {
</div> </div>
</div> </div>
</RealtimeProvider> </RealtimeProvider>
</CrmQueryProvider>
</div> </div>
); );
} }
+3 -1
View File
@@ -138,7 +138,9 @@ export class CrmMessagingAdapter implements MessagingAdapter {
async history(threadId: string): Promise<Message[]> { async history(threadId: string): Promise<Message[]> {
if (this.socket) { if (this.socket) {
const res = await this.socket.openThread(threadId); // joins so live events flow // openThread both JOINS the room and returns history, so it is the only call needed on a
// cache miss. The SDK caches per thread, so this no longer fires on every selection.
const res = await this.socket.openThread(threadId);
this.joined.add(threadId); this.joined.add(threadId);
return Promise.all( return Promise.all(
res.history.map((m) => { res.history.map((m) => {
+33
View File
@@ -0,0 +1,33 @@
"use client";
// The CRM owns the single QueryClient for the whole dashboard.
//
// The messaging SDK takes React Query as a peerDependency and will fall back to its own client if
// it can't find one — but then its cache would be invisible to us: we couldn't invalidate a
// conversation list after a write, share GC policy, or persist it. Providing the client here keeps
// SDK and CRM data in one store.
import { useState, type ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export function CrmQueryProvider({ children }: { children: ReactNode }) {
// useState, not a module constant: a module-level client would be shared across SSR requests and
// leak one user's cache into another's render.
const [client] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// The socket already pushes live changes for anything the user has open, so refetching
// on every window focus is noise — it was one of the redundant-call sources.
refetchOnWindowFocus: false,
retry: 1,
// Keep a cache entry ~10 min after its last observer unmounts, so leaving a module and
// coming back is served from memory rather than the network.
gcTime: 10 * 60_000,
},
},
}),
);
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}