Skip to content

Commit cf77e6d

Browse files
authored
feat(frontend): migrate server-state from Zustand to TanStack Query + perf optimizations | ENG-202 & ENG-204 (#298)
* feat(frontend): add TanStack Query foundation and query hooks for all domains - Install @tanstack/react-query, add QueryClient and QueryClientProvider - Create org-scoped query key factories in queryKeys.ts - Create queryOptions() factories for execution data in executionQueryOptions.ts - Add query hooks for all server-state domains: runs, workflows, executions, components, integrations, MCP groups/servers, schedules, secrets, webhooks, API keys, artifacts, and human input - Export TerminalChunkResponse from api.ts for queryOptions type inference - Add mcpGroupsApi service for N+1 batch endpoints Signed-off-by: Aseem Shrey <LuD1161@users.noreply.github.com> * refactor(frontend): migrate all consumers to TanStack Query and delete Zustand stores - Migrate all pages, components, and features from Zustand stores to query hooks - Route execution/timeline Zustand stores through queryOptions() factories - Delete 11 Zustand stores (apiKey, artifact, component, integration, mcpGroup, mcpServer, run, schedule, secret, webhook) and their tests - Update remaining tests to work with TanStack Query patterns - Fix N+1 query patterns on WorkflowList and MCP Library pages (backend batch endpoints) Signed-off-by: Aseem Shrey <LuD1161@users.noreply.github.com> * perf(frontend): add idle-time prefetching, bundle optimization, and backend batch endpoints - Add idle-time prefetching for components and workflows via requestIdleCallback - Optimize bundle with manual chunks, font preloading, and lazy route loading - Add backend batch endpoints for workflow summaries and MCP group server counts - Gate prefetch on auth state and align cache shapes to prevent mismatches - Add VITE_DISABLE_DEVTOOLS env var Signed-off-by: Aseem Shrey <LuD1161@users.noreply.github.com> * docs(frontend): add performance guidelines, state management docs, and load testing audit - Add frontend/docs/performance.md with query hook and rendering guidelines - Update frontend/docs/state.md with TanStack Query migration patterns - Add load testing audit report (2026-02-17) - Add performance-review skill and rename load-audit to stress-test-frontend - Update AGENTS.md with TanStack Query enforcement rules - Update READMEs for hooks and stores Signed-off-by: Aseem Shrey <LuD1161@users.noreply.github.com> --------- Signed-off-by: Aseem Shrey <LuD1161@users.noreply.github.com> Co-authored-by: Aseem Shrey <LuD1161@users.noreply.github.com>
1 parent 213e769 commit cf77e6d

105 files changed

Lines changed: 4504 additions & 3990 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
name: performance-review
3+
description: Review frontend code changes for performance anti-patterns and guideline compliance. Checks query hooks, stale times, bundle impact, rendering patterns, and Zustand selectors.
4+
user_invocable: true
5+
---
6+
7+
# Frontend Performance Review
8+
9+
**Reference:** `frontend/docs/performance.md`
10+
11+
---
12+
13+
## When to Invoke
14+
15+
- Before submitting a PR that adds or modifies query hooks, page components, Zustand stores, or real-time features
16+
- When the user asks for a "performance review" or "perf check"
17+
- As part of the component-development workflow for new pages
18+
19+
## Agent Instructions
20+
21+
When invoked, perform the following checks in order. Report PASS/FAIL/WARN for each. Group failures by severity: **CRITICAL** (must fix before merge), **WARNING** (should fix), **INFO** (consider).
22+
23+
### Step 1: Identify Scope
24+
25+
Read the diff or the files specified by the user. If no files specified, use `git diff --name-only` to find changed frontend files. Identify which categories apply: query hooks, page routes, stores, real-time, bundle.
26+
27+
### Step 2: Query Hook Checks
28+
29+
1. Search for `useState` + `useEffect` + `api.` within 20 lines of each other in changed files. If found: **CRITICAL** — should be a TanStack Query hook.
30+
2. Check that new `useQuery` calls reference a key from `queryKeys.ts`, not an inline array. If inline: **CRITICAL**.
31+
3. Check that new query hook files are named `use<Domain>Queries.ts` and placed in `src/hooks/queries/`. If misplaced: **WARNING**.
32+
4. Check that `useMutation` calls include `onSuccess` with `queryClient.invalidateQueries()`. If missing: **WARNING**.
33+
5. Check for `skipToken` usage on conditional queries vs. `enabled: false`. Flag `enabled: false` without `skipToken` as **WARNING**.
34+
35+
### Step 3: Stale Time Audit
36+
37+
1. For each new `useQuery`, check if `staleTime` is set explicitly or inherits the 30s default appropriately.
38+
2. Cross-reference the data type: if the query fetches components, templates, or providers — flag missing `staleTime: Infinity` as **CRITICAL**.
39+
3. For execution queries, verify they use `executionQueryOptions.ts` factories rather than inline values. Flag inconsistency as **WARNING**.
40+
4. For queries on terminal runs, verify `terminalStaleTime()` is used rather than a hardcoded number.
41+
42+
### Step 4: Bundle Impact
43+
44+
1. For each new page added to `App.tsx`: verify it uses `React.lazy(() => import(...))`. Static imports are **CRITICAL**.
45+
2. Verify new sidebar pages are added to `routePrefetchMap` in `src/lib/prefetch-routes.ts`. Missing entry is **WARNING**.
46+
3. If a new conditionally-visible component imports a library > 100KB (check for `@xterm`, `posthog-js`, `lucide-react` barrel imports): flag as **WARNING** — consider deferred load pattern.
47+
48+
### Step 5: Rendering Checks
49+
50+
1. Search for `const [*, set*] = useState` followed by `set*` inside a `useEffect` that references query data — **CRITICAL** (should use `useMemo`).
51+
2. Look for derived data calculations inline in JSX without `useMemo` involving array operations (filter, sort, reduce) — **WARNING**.
52+
3. Check for `React.memo` usage on new components in `timeline/` or `workflow/` directories that re-render frequently — **INFO** if missing.
53+
54+
### Step 6: Zustand Store Checks
55+
56+
1. Search for `const store = use<X>Store()` or destructuring from `use<X>Store()` without a selector — full store subscription. **WARNING**.
57+
2. For new stores using `persist`, verify `partialize` is present. Missing is **WARNING**.
58+
3. For new `persist` stores, verify action functions are excluded from `partialize`.
59+
60+
### Step 7: Generate Report
61+
62+
Format the output as follows:
63+
64+
```
65+
## Performance Review: [description of scope]
66+
67+
### Critical (must fix before merge)
68+
- [file:line] Description of issue. See: performance.md § [section]
69+
70+
### Warnings (should fix)
71+
- [file:line] Description of issue. See: performance.md § [section]
72+
73+
### Info (consider)
74+
- [file:line] Description. See: performance.md § [section]
75+
76+
### Passed Checks
77+
- [n] query hooks using TanStack Query correctly
78+
- [n] page routes using React.lazy
79+
- [n] Zustand selectors properly scoped
80+
- staleTime: appropriate tiers applied
81+
82+
### Summary
83+
[1-2 sentences on overall performance hygiene of the change]
84+
```
85+
86+
## Rules
87+
88+
- This is a **review only** — do NOT make code changes unless the user explicitly asks
89+
- Always cite the file and line number for each finding
90+
- Always reference `frontend/docs/performance.md` for the relevant pattern section
91+
- If no frontend files are in the diff, report "No frontend changes detected" and exit

.claude/skills/load-audit/SKILL.md renamed to .claude/skills/stress-test-frontend/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
name: load-audit
2+
name: stress-test-frontend
33
description: Run a frontend load testing audit. Seeds data, tests all pages via Chrome DevTools MCP, records network calls, TanStack queries, DOM sizes, and generates a timestamped report.
44
user_invocable: true
55
---
@@ -13,7 +13,7 @@ user_invocable: true
1313

1414
## Agent Instructions
1515

16-
When the user invokes `/load-audit`, perform a full frontend load testing audit:
16+
When the user invokes `/stress-test-frontend`, perform a full frontend load testing audit:
1717

1818
### 1. Setup
1919

AGENTS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,35 @@ bun --cwd backend run db:studio # View data
9898
4. **E2E Tests**: Mandatory for significant features. Place in `e2e-tests/` folder.
9999
5. **GitHub CLI**: Use `gh` for all GitHub operations (issues, PRs, actions, releases). Never use browser automation for GitHub tasks.
100100

101+
### Frontend: Read Before Writing Code
102+
103+
Before writing ANY frontend code that fetches data or adds a page, you MUST read these files first:
104+
105+
1. `frontend/docs/state.md` — Decision guide: TanStack Query vs Zustand, hook patterns, anti-patterns
106+
2. `frontend/docs/performance.md` — Stale time tiers, bundle splitting, prefetch patterns, query key architecture
107+
3. `frontend/src/lib/queryKeys.ts` — Existing query key factories (add new keys here, never inline)
108+
4. Browse `frontend/src/hooks/queries/` — Follow existing hook naming conventions (`use<Domain>Queries.ts`)
109+
110+
### Frontend Data Fetching (Mandatory)
111+
112+
6. **All API data must use TanStack Query hooks** in `frontend/src/hooks/queries/`. Never use `useState` + `useEffect` to fetch backend data — this is the single most important frontend rule.
113+
7. **Query keys** go in `frontend/src/lib/queryKeys.ts` (org-scoped, factory functions).
114+
8. **After mutations**, invalidate the relevant query cache via `queryClient.invalidateQueries()` — do not manually update local state.
115+
9. **Derive data** from query results using `useMemo`, not by copying into separate `useState`.
116+
10. **Zustand stores** are for client-only UI state (canvas, timeline, auth). Never store API data in Zustand.
117+
118+
See `frontend/docs/state.md` for patterns, anti-patterns, and the full decision guide.
119+
120+
### Frontend Performance (Mandatory)
121+
122+
See `frontend/docs/performance.md` for the complete reference with code examples.
123+
124+
11. **Every new page must use `React.lazy()`** in `App.tsx`. Add the route to `routePrefetchMap` in `src/lib/prefetch-routes.ts`.
125+
12. **Set `staleTime: Infinity` for static/reference data** (components, templates, providers). The 30s default is wrong for them.
126+
13. **Use `skipToken` for conditional queries** instead of `enabled: false` alone. See `useRunQueries.ts`.
127+
14. **Granular Zustand selectors**: `useStore((s) => s.field)`, never `const store = useStore()`.
128+
15. **No N+1 queries**: never call a query hook inside `.map()`. Use a batched endpoint (see `useMcpGroupsWithServers`).
129+
101130
---
102131

103132
## Architecture
@@ -140,6 +169,16 @@ When tasks match a skill, load it: `cat .claude/skills/<name>/SKILL.md`
140169
<description>Creating components (inline/docker). Dynamic ports, retry policies, PTY patterns, IsolatedContainerVolume.</description>
141170
<location>project</location>
142171
</skill>
172+
<skill>
173+
<name>performance-review</name>
174+
<description>Review code changes for frontend performance anti-patterns. Checks stale times, bundle splitting, Zustand selectors, N+1 queries, and React rendering.</description>
175+
<location>project</location>
176+
</skill>
177+
<skill>
178+
<name>stress-test-frontend</name>
179+
<description>Run a frontend load testing audit. Seeds data, tests all pages via Chrome DevTools MCP, records network calls, TanStack queries, DOM sizes, and generates a timestamped report.</description>
180+
<location>project</location>
181+
</skill>
143182
</available_skills>
144183

145184
</skills_system>

backend/src/mcp-groups/mcp-groups.controller.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,16 @@ export class McpGroupsController {
4545
@Get()
4646
@ApiOperation({ summary: 'List all MCP groups' })
4747
@ApiQuery({ name: 'enabled', required: false, type: Boolean })
48+
@ApiQuery({ name: 'includeServers', required: false, type: Boolean })
4849
@ApiOkResponse({ type: [McpGroupResponse] })
49-
async listGroups(@Query('enabled') enabled?: string): Promise<McpGroupResponse[]> {
50+
async listGroups(
51+
@Query('enabled') enabled?: string,
52+
@Query('includeServers') includeServers?: string,
53+
): Promise<McpGroupResponse[]> {
5054
const enabledOnly = enabled === 'true';
55+
if (includeServers === 'true') {
56+
return this.mcpGroupsService.listGroupsWithServers(enabledOnly);
57+
}
5158
return this.mcpGroupsService.listGroups(enabledOnly);
5259
}
5360

backend/src/mcp-groups/mcp-groups.repository.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,60 @@ export class McpGroupsRepository {
123123

124124
// Group-Server relationship methods
125125

126+
async findAllServersGrouped(): Promise<
127+
Map<
128+
string,
129+
(McpServerRecord & { recommended: boolean; defaultSelected: boolean; toolCount: number })[]
130+
>
131+
> {
132+
const query = sql`
133+
SELECT
134+
gs.group_id,
135+
s.id,
136+
s.name,
137+
s.description,
138+
s.transport_type,
139+
s.endpoint,
140+
s.command,
141+
s.args,
142+
s.headers,
143+
s.enabled,
144+
s.health_check_url,
145+
s.last_health_check,
146+
s.last_health_status,
147+
s.group_id AS server_group_id,
148+
s.organization_id,
149+
s.created_by,
150+
s.created_at,
151+
s.updated_at,
152+
gs.recommended,
153+
gs.default_selected,
154+
COALESCE(tc.tool_count, 0) as tool_count
155+
FROM mcp_group_servers gs
156+
INNER JOIN mcp_servers s ON gs.server_id = s.id
157+
LEFT JOIN (
158+
SELECT server_id, COUNT(id) as tool_count
159+
FROM mcp_server_tools
160+
GROUP BY server_id
161+
) tc ON tc.server_id = s.id
162+
ORDER BY gs.group_id, CASE WHEN gs.recommended THEN 0 ELSE 1 END ASC, s.name
163+
`;
164+
165+
const result = await this.db.execute(query);
166+
const grouped = new Map<
167+
string,
168+
(McpServerRecord & { recommended: boolean; defaultSelected: boolean; toolCount: number })[]
169+
>();
170+
for (const row of result.rows as any[]) {
171+
const groupId = row.group_id;
172+
if (!grouped.has(groupId)) {
173+
grouped.set(groupId, []);
174+
}
175+
grouped.get(groupId)!.push(row);
176+
}
177+
return grouped;
178+
}
179+
126180
async findServersByGroup(
127181
groupId: string,
128182
): Promise<

backend/src/mcp-groups/mcp-groups.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ export class McpGroupsService implements OnModuleInit {
106106
return groups.map((g) => this.mapGroupToResponse(g));
107107
}
108108

109+
async listGroupsWithServers(
110+
enabledOnly = false,
111+
): Promise<(McpGroupResponse & { servers: McpGroupServerResponse[] })[]> {
112+
const [groups, serversMap] = await Promise.all([
113+
this.repository.findAll(enabledOnly ? { enabled: true } : {}),
114+
this.repository.findAllServersGrouped(),
115+
]);
116+
return groups.map((g) => ({
117+
...this.mapGroupToResponse(g),
118+
servers: (serversMap.get(g.id) ?? []).map((s) => this.mapGroupServerToResponse(s)),
119+
}));
120+
}
121+
109122
listTemplates(): GroupTemplateDto[] {
110123
return this.seedingService.getAllTemplates();
111124
}

backend/src/workflows/repository/workflow.repository.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface WorkflowSummaryRecord {
1616
description: string | null;
1717
organizationId: string | null;
1818
lastRun: Date | null;
19+
latestRunStatus: string | null;
1920
runCount: number;
2021
nodeCount: number;
2122
createdAt: Date;
@@ -152,6 +153,12 @@ export class WorkflowRepository {
152153
description: workflowsTable.description,
153154
organizationId: workflowsTable.organizationId,
154155
lastRun: workflowsTable.lastRun,
156+
latestRunStatus: sql<string | null>`(
157+
SELECT wr.status FROM workflow_runs wr
158+
WHERE wr.workflow_id = ${workflowsTable.id}
159+
ORDER BY wr.created_at DESC
160+
LIMIT 1
161+
)`.as('latest_run_status'),
155162
runCount: workflowsTable.runCount,
156163
nodeCount: sql<number>`coalesce(jsonb_array_length(${workflowsTable.graph}->'nodes'), 0)`.as(
157164
'node_count',

backend/src/workflows/workflows.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface WorkflowSummaryResponse {
5454
description: string | null;
5555
organizationId: string | null;
5656
lastRun: string | null;
57+
latestRunStatus: string | null;
5758
runCount: number;
5859
nodeCount: number;
5960
createdAt: string;
@@ -560,6 +561,7 @@ export class WorkflowsService {
560561
return records.map((record) => ({
561562
...record,
562563
lastRun: record.lastRun?.toISOString() ?? null,
564+
latestRunStatus: record.latestRunStatus ?? null,
563565
createdAt: record.createdAt.toISOString(),
564566
updatedAt: record.updatedAt.toISOString(),
565567
}));

bun.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ VITE_LOGO_DEV_PUBLIC_KEY=
2626
# Leave empty to hide Dashboards navigation link
2727
# For dev/prod: http://localhost/analytics (nginx in dev/prod)
2828
VITE_OPENSEARCH_DASHBOARDS_URL=http://localhost/analytics
29+
30+
# Dev Tools
31+
# Set to 'true' to hide TanStack Query devtools in development mode
32+
# VITE_DISABLE_DEVTOOLS=true

0 commit comments

Comments
 (0)