Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .claude/skills/performance-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
name: performance-review
description: Review frontend code changes for performance anti-patterns and guideline compliance. Checks query hooks, stale times, bundle impact, rendering patterns, and Zustand selectors.
user_invocable: true
---

# Frontend Performance Review

**Reference:** `frontend/docs/performance.md`

---

## When to Invoke

- Before submitting a PR that adds or modifies query hooks, page components, Zustand stores, or real-time features
- When the user asks for a "performance review" or "perf check"
- As part of the component-development workflow for new pages

## Agent Instructions

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).

### Step 1: Identify Scope

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.

### Step 2: Query Hook Checks

1. Search for `useState` + `useEffect` + `api.` within 20 lines of each other in changed files. If found: **CRITICAL** — should be a TanStack Query hook.
2. Check that new `useQuery` calls reference a key from `queryKeys.ts`, not an inline array. If inline: **CRITICAL**.
3. Check that new query hook files are named `use<Domain>Queries.ts` and placed in `src/hooks/queries/`. If misplaced: **WARNING**.
4. Check that `useMutation` calls include `onSuccess` with `queryClient.invalidateQueries()`. If missing: **WARNING**.
5. Check for `skipToken` usage on conditional queries vs. `enabled: false`. Flag `enabled: false` without `skipToken` as **WARNING**.

### Step 3: Stale Time Audit

1. For each new `useQuery`, check if `staleTime` is set explicitly or inherits the 30s default appropriately.
2. Cross-reference the data type: if the query fetches components, templates, or providers — flag missing `staleTime: Infinity` as **CRITICAL**.
3. For execution queries, verify they use `executionQueryOptions.ts` factories rather than inline values. Flag inconsistency as **WARNING**.
4. For queries on terminal runs, verify `terminalStaleTime()` is used rather than a hardcoded number.

### Step 4: Bundle Impact

1. For each new page added to `App.tsx`: verify it uses `React.lazy(() => import(...))`. Static imports are **CRITICAL**.
2. Verify new sidebar pages are added to `routePrefetchMap` in `src/lib/prefetch-routes.ts`. Missing entry is **WARNING**.
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.

### Step 5: Rendering Checks

1. Search for `const [*, set*] = useState` followed by `set*` inside a `useEffect` that references query data — **CRITICAL** (should use `useMemo`).
2. Look for derived data calculations inline in JSX without `useMemo` involving array operations (filter, sort, reduce) — **WARNING**.
3. Check for `React.memo` usage on new components in `timeline/` or `workflow/` directories that re-render frequently — **INFO** if missing.

### Step 6: Zustand Store Checks

1. Search for `const store = use<X>Store()` or destructuring from `use<X>Store()` without a selector — full store subscription. **WARNING**.
2. For new stores using `persist`, verify `partialize` is present. Missing is **WARNING**.
3. For new `persist` stores, verify action functions are excluded from `partialize`.

### Step 7: Generate Report

Format the output as follows:

```
## Performance Review: [description of scope]

### Critical (must fix before merge)
- [file:line] Description of issue. See: performance.md § [section]

### Warnings (should fix)
- [file:line] Description of issue. See: performance.md § [section]

### Info (consider)
- [file:line] Description. See: performance.md § [section]

### Passed Checks
- [n] query hooks using TanStack Query correctly
- [n] page routes using React.lazy
- [n] Zustand selectors properly scoped
- staleTime: appropriate tiers applied

### Summary
[1-2 sentences on overall performance hygiene of the change]
```

## Rules

- This is a **review only** — do NOT make code changes unless the user explicitly asks
- Always cite the file and line number for each finding
- Always reference `frontend/docs/performance.md` for the relevant pattern section
- If no frontend files are in the diff, report "No frontend changes detected" and exit
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: load-audit
name: stress-test-frontend
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.
user_invocable: true
---
Expand All @@ -13,7 +13,7 @@ user_invocable: true

## Agent Instructions

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

### 1. Setup

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

### Frontend: Read Before Writing Code

Before writing ANY frontend code that fetches data or adds a page, you MUST read these files first:

1. `frontend/docs/state.md` — Decision guide: TanStack Query vs Zustand, hook patterns, anti-patterns
2. `frontend/docs/performance.md` — Stale time tiers, bundle splitting, prefetch patterns, query key architecture
3. `frontend/src/lib/queryKeys.ts` — Existing query key factories (add new keys here, never inline)
4. Browse `frontend/src/hooks/queries/` — Follow existing hook naming conventions (`use<Domain>Queries.ts`)

### Frontend Data Fetching (Mandatory)

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.
7. **Query keys** go in `frontend/src/lib/queryKeys.ts` (org-scoped, factory functions).
8. **After mutations**, invalidate the relevant query cache via `queryClient.invalidateQueries()` — do not manually update local state.
9. **Derive data** from query results using `useMemo`, not by copying into separate `useState`.
10. **Zustand stores** are for client-only UI state (canvas, timeline, auth). Never store API data in Zustand.

See `frontend/docs/state.md` for patterns, anti-patterns, and the full decision guide.

### Frontend Performance (Mandatory)

See `frontend/docs/performance.md` for the complete reference with code examples.

11. **Every new page must use `React.lazy()`** in `App.tsx`. Add the route to `routePrefetchMap` in `src/lib/prefetch-routes.ts`.
12. **Set `staleTime: Infinity` for static/reference data** (components, templates, providers). The 30s default is wrong for them.
13. **Use `skipToken` for conditional queries** instead of `enabled: false` alone. See `useRunQueries.ts`.
14. **Granular Zustand selectors**: `useStore((s) => s.field)`, never `const store = useStore()`.
15. **No N+1 queries**: never call a query hook inside `.map()`. Use a batched endpoint (see `useMcpGroupsWithServers`).

---

## Architecture
Expand Down Expand Up @@ -140,6 +169,16 @@ When tasks match a skill, load it: `cat .claude/skills/<name>/SKILL.md`
<description>Creating components (inline/docker). Dynamic ports, retry policies, PTY patterns, IsolatedContainerVolume.</description>
<location>project</location>
</skill>
<skill>
<name>performance-review</name>
<description>Review code changes for frontend performance anti-patterns. Checks stale times, bundle splitting, Zustand selectors, N+1 queries, and React rendering.</description>
<location>project</location>
</skill>
<skill>
<name>stress-test-frontend</name>
<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>
<location>project</location>
</skill>
</available_skills>

</skills_system>
9 changes: 8 additions & 1 deletion backend/src/mcp-groups/mcp-groups.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,16 @@ export class McpGroupsController {
@Get()
@ApiOperation({ summary: 'List all MCP groups' })
@ApiQuery({ name: 'enabled', required: false, type: Boolean })
@ApiQuery({ name: 'includeServers', required: false, type: Boolean })
@ApiOkResponse({ type: [McpGroupResponse] })
async listGroups(@Query('enabled') enabled?: string): Promise<McpGroupResponse[]> {
async listGroups(
@Query('enabled') enabled?: string,
@Query('includeServers') includeServers?: string,
): Promise<McpGroupResponse[]> {
const enabledOnly = enabled === 'true';
if (includeServers === 'true') {
return this.mcpGroupsService.listGroupsWithServers(enabledOnly);
}
return this.mcpGroupsService.listGroups(enabledOnly);
}

Expand Down
54 changes: 54 additions & 0 deletions backend/src/mcp-groups/mcp-groups.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,60 @@ export class McpGroupsRepository {

// Group-Server relationship methods

async findAllServersGrouped(): Promise<
Map<
string,
(McpServerRecord & { recommended: boolean; defaultSelected: boolean; toolCount: number })[]
>
> {
const query = sql`
SELECT
gs.group_id,
s.id,
s.name,
s.description,
s.transport_type,
s.endpoint,
s.command,
s.args,
s.headers,
s.enabled,
s.health_check_url,
s.last_health_check,
s.last_health_status,
s.group_id AS server_group_id,
s.organization_id,
s.created_by,
s.created_at,
s.updated_at,
gs.recommended,
gs.default_selected,
COALESCE(tc.tool_count, 0) as tool_count
FROM mcp_group_servers gs
INNER JOIN mcp_servers s ON gs.server_id = s.id
LEFT JOIN (
SELECT server_id, COUNT(id) as tool_count
FROM mcp_server_tools
GROUP BY server_id
) tc ON tc.server_id = s.id
ORDER BY gs.group_id, CASE WHEN gs.recommended THEN 0 ELSE 1 END ASC, s.name
`;

const result = await this.db.execute(query);
const grouped = new Map<
string,
(McpServerRecord & { recommended: boolean; defaultSelected: boolean; toolCount: number })[]
>();
for (const row of result.rows as any[]) {
const groupId = row.group_id;
if (!grouped.has(groupId)) {
grouped.set(groupId, []);
}
grouped.get(groupId)!.push(row);
}
return grouped;
}

async findServersByGroup(
groupId: string,
): Promise<
Expand Down
13 changes: 13 additions & 0 deletions backend/src/mcp-groups/mcp-groups.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ export class McpGroupsService implements OnModuleInit {
return groups.map((g) => this.mapGroupToResponse(g));
}

async listGroupsWithServers(
enabledOnly = false,
): Promise<(McpGroupResponse & { servers: McpGroupServerResponse[] })[]> {
const [groups, serversMap] = await Promise.all([
this.repository.findAll(enabledOnly ? { enabled: true } : {}),
this.repository.findAllServersGrouped(),
]);
return groups.map((g) => ({
...this.mapGroupToResponse(g),
servers: (serversMap.get(g.id) ?? []).map((s) => this.mapGroupServerToResponse(s)),
}));
}

listTemplates(): GroupTemplateDto[] {
return this.seedingService.getAllTemplates();
}
Expand Down
7 changes: 7 additions & 0 deletions backend/src/workflows/repository/workflow.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface WorkflowSummaryRecord {
description: string | null;
organizationId: string | null;
lastRun: Date | null;
latestRunStatus: string | null;
runCount: number;
nodeCount: number;
createdAt: Date;
Expand Down Expand Up @@ -152,6 +153,12 @@ export class WorkflowRepository {
description: workflowsTable.description,
organizationId: workflowsTable.organizationId,
lastRun: workflowsTable.lastRun,
latestRunStatus: sql<string | null>`(
SELECT wr.status FROM workflow_runs wr
WHERE wr.workflow_id = ${workflowsTable.id}
ORDER BY wr.created_at DESC
LIMIT 1
)`.as('latest_run_status'),
runCount: workflowsTable.runCount,
nodeCount: sql<number>`coalesce(jsonb_array_length(${workflowsTable.graph}->'nodes'), 0)`.as(
'node_count',
Expand Down
2 changes: 2 additions & 0 deletions backend/src/workflows/workflows.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface WorkflowSummaryResponse {
description: string | null;
organizationId: string | null;
lastRun: string | null;
latestRunStatus: string | null;
runCount: number;
nodeCount: number;
createdAt: string;
Expand Down Expand Up @@ -560,6 +561,7 @@ export class WorkflowsService {
return records.map((record) => ({
...record,
lastRun: record.lastRun?.toISOString() ?? null,
latestRunStatus: record.latestRunStatus ?? null,
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
}));
Expand Down
10 changes: 10 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ VITE_LOGO_DEV_PUBLIC_KEY=
# Leave empty to hide Dashboards navigation link
# For dev/prod: http://localhost/analytics (nginx in dev/prod)
VITE_OPENSEARCH_DASHBOARDS_URL=http://localhost/analytics

# Dev Tools
# Set to 'true' to hide TanStack Query devtools in development mode
# VITE_DISABLE_DEVTOOLS=true
Loading