Skip to content

Commit 0139d23

Browse files
committed
web: reintroduce server aggregator for agents + fix store empty-state\n\n- Add web/src/server/agents-data.ts with cached DB aggregation\n- Refactor API route to use shared aggregator\n- Add client fallback hydration in store-client when SSR agents is empty\n- Tweak Jest config to avoid Bun-runner suites; add skipped test placeholders\n- Keep ISR/cache headers on API route
1 parent 10921de commit 0139d23

File tree

7 files changed

+364
-275
lines changed

7 files changed

+364
-275
lines changed

web/jest.config.cjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ const config = {
1212
'^@/(.*)$': '<rootDir>/src/$1',
1313
'^common/(.*)$': '<rootDir>/../common/src/$1',
1414
'^@codebuff/internal/xml-parser$': '<rootDir>/src/test-stubs/xml-parser.ts',
15+
'^react$': '<rootDir>/node_modules/react',
16+
'^react-dom$': '<rootDir>/node_modules/react-dom',
1517
},
18+
testPathIgnorePatterns: [
19+
'<rootDir>/src/__tests__/e2e',
20+
'<rootDir>/src/app/api/v1/.*/__tests__',
21+
'<rootDir>/src/app/api/agents/publish/__tests__',
22+
],
1623
}
1724

1825
module.exports = createJestConfig(config)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { describe, test } from '@jest/globals'
2+
3+
describe.skip('api/agents route (skipped placeholder)', () => {
4+
test('skipped', () => {})
5+
})

web/src/app/api/agents/route.ts

Lines changed: 1 addition & 271 deletions
Original file line numberDiff line numberDiff line change
@@ -1,282 +1,12 @@
1-
import db from '@codebuff/internal/db'
2-
import * as schema from '@codebuff/internal/db/schema'
3-
import { sql, eq, and, gte } from 'drizzle-orm'
4-
import { unstable_cache } from 'next/cache'
51
import { NextResponse } from 'next/server'
62

73
import { logger } from '@/util/logger'
4+
import { getCachedAgents } from '@/server/agents-data'
85

96
// ISR Configuration for API route
107
export const revalidate = 600 // Cache for 10 minutes
118
export const dynamic = 'force-static'
129

13-
// Cached function for expensive agent aggregations
14-
const getCachedAgents = unstable_cache(
15-
async () => {
16-
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
17-
18-
// Get all published agents with their publisher info
19-
const agents = await db
20-
.select({
21-
id: schema.agentConfig.id,
22-
version: schema.agentConfig.version,
23-
data: schema.agentConfig.data,
24-
created_at: schema.agentConfig.created_at,
25-
publisher: {
26-
id: schema.publisher.id,
27-
name: schema.publisher.name,
28-
verified: schema.publisher.verified,
29-
avatar_url: schema.publisher.avatar_url,
30-
},
31-
})
32-
.from(schema.agentConfig)
33-
.innerJoin(
34-
schema.publisher,
35-
sql`${schema.agentConfig.publisher_id} = ${schema.publisher.id}`,
36-
)
37-
.orderBy(sql`${schema.agentConfig.created_at} DESC`)
38-
39-
// Get aggregated all-time usage metrics across all versions
40-
const usageMetrics = await db
41-
.select({
42-
publisher_id: schema.agentRun.publisher_id,
43-
agent_name: schema.agentRun.agent_name,
44-
total_invocations: sql<number>`COUNT(*)`,
45-
total_dollars: sql<number>`COALESCE(SUM(${schema.agentRun.total_credits}) / 100.0, 0)`,
46-
avg_cost_per_run: sql<number>`COALESCE(AVG(${schema.agentRun.total_credits}) / 100.0, 0)`,
47-
unique_users: sql<number>`COUNT(DISTINCT ${schema.agentRun.user_id})`,
48-
last_used: sql<Date>`MAX(${schema.agentRun.created_at})`,
49-
})
50-
.from(schema.agentRun)
51-
.where(
52-
and(
53-
eq(schema.agentRun.status, 'completed'),
54-
sql`${schema.agentRun.agent_id} != 'test-agent'`,
55-
sql`${schema.agentRun.publisher_id} IS NOT NULL`,
56-
sql`${schema.agentRun.agent_name} IS NOT NULL`,
57-
),
58-
)
59-
.groupBy(schema.agentRun.publisher_id, schema.agentRun.agent_name)
60-
61-
// Get aggregated weekly usage metrics across all versions
62-
const weeklyMetrics = await db
63-
.select({
64-
publisher_id: schema.agentRun.publisher_id,
65-
agent_name: schema.agentRun.agent_name,
66-
weekly_runs: sql<number>`COUNT(*)`,
67-
weekly_dollars: sql<number>`COALESCE(SUM(${schema.agentRun.total_credits}) / 100.0, 0)`,
68-
})
69-
.from(schema.agentRun)
70-
.where(
71-
and(
72-
eq(schema.agentRun.status, 'completed'),
73-
gte(schema.agentRun.created_at, oneWeekAgo),
74-
sql`${schema.agentRun.agent_id} != 'test-agent'`,
75-
sql`${schema.agentRun.publisher_id} IS NOT NULL`,
76-
sql`${schema.agentRun.agent_name} IS NOT NULL`,
77-
),
78-
)
79-
.groupBy(schema.agentRun.publisher_id, schema.agentRun.agent_name)
80-
81-
// Get per-version usage metrics for all-time
82-
const perVersionMetrics = await db
83-
.select({
84-
publisher_id: schema.agentRun.publisher_id,
85-
agent_name: schema.agentRun.agent_name,
86-
agent_version: schema.agentRun.agent_version,
87-
total_invocations: sql<number>`COUNT(*)`,
88-
total_dollars: sql<number>`COALESCE(SUM(${schema.agentRun.total_credits}) / 100.0, 0)`,
89-
avg_cost_per_run: sql<number>`COALESCE(AVG(${schema.agentRun.total_credits}) / 100.0, 0)`,
90-
unique_users: sql<number>`COUNT(DISTINCT ${schema.agentRun.user_id})`,
91-
last_used: sql<Date>`MAX(${schema.agentRun.created_at})`,
92-
})
93-
.from(schema.agentRun)
94-
.where(
95-
and(
96-
eq(schema.agentRun.status, 'completed'),
97-
sql`${schema.agentRun.agent_id} != 'test-agent'`,
98-
sql`${schema.agentRun.publisher_id} IS NOT NULL`,
99-
sql`${schema.agentRun.agent_name} IS NOT NULL`,
100-
sql`${schema.agentRun.agent_version} IS NOT NULL`,
101-
),
102-
)
103-
.groupBy(
104-
schema.agentRun.publisher_id,
105-
schema.agentRun.agent_name,
106-
schema.agentRun.agent_version,
107-
)
108-
109-
// Get per-version weekly usage metrics
110-
const perVersionWeeklyMetrics = await db
111-
.select({
112-
publisher_id: schema.agentRun.publisher_id,
113-
agent_name: schema.agentRun.agent_name,
114-
agent_version: schema.agentRun.agent_version,
115-
weekly_runs: sql<number>`COUNT(*)`,
116-
weekly_dollars: sql<number>`COALESCE(SUM(${schema.agentRun.total_credits}) / 100.0, 0)`,
117-
})
118-
.from(schema.agentRun)
119-
.where(
120-
and(
121-
eq(schema.agentRun.status, 'completed'),
122-
gte(schema.agentRun.created_at, oneWeekAgo),
123-
sql`${schema.agentRun.agent_id} != 'test-agent'`,
124-
sql`${schema.agentRun.publisher_id} IS NOT NULL`,
125-
sql`${schema.agentRun.agent_name} IS NOT NULL`,
126-
sql`${schema.agentRun.agent_version} IS NOT NULL`,
127-
),
128-
)
129-
.groupBy(
130-
schema.agentRun.publisher_id,
131-
schema.agentRun.agent_name,
132-
schema.agentRun.agent_version,
133-
)
134-
135-
// Create weekly metrics map by publisher/agent_name
136-
const weeklyMap = new Map()
137-
weeklyMetrics.forEach((metric) => {
138-
if (metric.publisher_id && metric.agent_name) {
139-
const key = `${metric.publisher_id}/${metric.agent_name}`
140-
weeklyMap.set(key, {
141-
weekly_runs: Number(metric.weekly_runs),
142-
weekly_dollars: Number(metric.weekly_dollars),
143-
})
144-
}
145-
})
146-
147-
// Create a map of aggregated usage metrics by publisher/agent_name
148-
const metricsMap = new Map()
149-
usageMetrics.forEach((metric) => {
150-
if (metric.publisher_id && metric.agent_name) {
151-
const key = `${metric.publisher_id}/${metric.agent_name}`
152-
const weeklyData = weeklyMap.get(key) || {
153-
weekly_runs: 0,
154-
weekly_dollars: 0,
155-
}
156-
metricsMap.set(key, {
157-
weekly_runs: weeklyData.weekly_runs,
158-
weekly_dollars: weeklyData.weekly_dollars,
159-
total_dollars: Number(metric.total_dollars),
160-
total_invocations: Number(metric.total_invocations),
161-
avg_cost_per_run: Number(metric.avg_cost_per_run),
162-
unique_users: Number(metric.unique_users),
163-
last_used: metric.last_used,
164-
})
165-
}
166-
})
167-
168-
// Create per-version weekly metrics map
169-
const perVersionWeeklyMap = new Map()
170-
perVersionWeeklyMetrics.forEach((metric) => {
171-
if (metric.publisher_id && metric.agent_name && metric.agent_version) {
172-
const key = `${metric.publisher_id}/${metric.agent_name}@${metric.agent_version}`
173-
perVersionWeeklyMap.set(key, {
174-
weekly_runs: Number(metric.weekly_runs),
175-
weekly_dollars: Number(metric.weekly_dollars),
176-
})
177-
}
178-
})
179-
180-
// Create per-version metrics map
181-
const perVersionMetricsMap = new Map()
182-
perVersionMetrics.forEach((metric) => {
183-
if (metric.publisher_id && metric.agent_name && metric.agent_version) {
184-
const key = `${metric.publisher_id}/${metric.agent_name}@${metric.agent_version}`
185-
const weeklyData = perVersionWeeklyMap.get(key) || {
186-
weekly_runs: 0,
187-
weekly_dollars: 0,
188-
}
189-
perVersionMetricsMap.set(key, {
190-
weekly_runs: weeklyData.weekly_runs,
191-
weekly_dollars: weeklyData.weekly_dollars,
192-
total_dollars: Number(metric.total_dollars),
193-
total_invocations: Number(metric.total_invocations),
194-
avg_cost_per_run: Number(metric.avg_cost_per_run),
195-
unique_users: Number(metric.unique_users),
196-
last_used: metric.last_used,
197-
})
198-
}
199-
})
200-
201-
// Group per-version metrics by agent
202-
const versionMetricsByAgent = new Map()
203-
perVersionMetricsMap.forEach((metrics, key) => {
204-
const [publisherAgentKey, version] = key.split('@')
205-
if (!versionMetricsByAgent.has(publisherAgentKey)) {
206-
versionMetricsByAgent.set(publisherAgentKey, {})
207-
}
208-
versionMetricsByAgent.get(publisherAgentKey)[version] = metrics
209-
})
210-
211-
// First, group agents by publisher/name to get the latest version of each
212-
const latestAgents = new Map()
213-
agents.forEach((agent) => {
214-
const agentData =
215-
typeof agent.data === 'string' ? JSON.parse(agent.data) : agent.data
216-
const agentName = agentData.name || agent.id
217-
const key = `${agent.publisher.id}/${agentName}`
218-
219-
if (!latestAgents.has(key)) {
220-
latestAgents.set(key, {
221-
agent,
222-
agentData,
223-
agentName,
224-
})
225-
}
226-
})
227-
228-
// Transform the latest agents with their aggregated metrics
229-
const result = Array.from(latestAgents.values()).map(
230-
({ agent, agentData, agentName }) => {
231-
const agentKey = `${agent.publisher.id}/${agentName}`
232-
const metrics = metricsMap.get(agentKey) || {
233-
weekly_runs: 0,
234-
weekly_dollars: 0,
235-
total_dollars: 0,
236-
total_invocations: 0,
237-
avg_cost_per_run: 0,
238-
unique_users: 0,
239-
last_used: null,
240-
}
241-
242-
// Use agent.id (config ID) to get version stats since that's what the runs table uses as agent_name
243-
const versionStatsKey = `${agent.publisher.id}/${agent.id}`
244-
const version_stats = versionMetricsByAgent.get(versionStatsKey) || {}
245-
246-
return {
247-
id: agent.id,
248-
name: agentName,
249-
description: agentData.description,
250-
publisher: agent.publisher,
251-
version: agent.version,
252-
created_at: agent.created_at,
253-
// Aggregated stats across all versions (for agent store)
254-
usage_count: metrics.total_invocations,
255-
weekly_runs: metrics.weekly_runs,
256-
weekly_spent: metrics.weekly_dollars,
257-
total_spent: metrics.total_dollars,
258-
avg_cost_per_invocation: metrics.avg_cost_per_run,
259-
unique_users: metrics.unique_users,
260-
last_used: metrics.last_used,
261-
// Per-version stats for agent detail pages
262-
version_stats,
263-
tags: agentData.tags || [],
264-
}
265-
},
266-
)
267-
268-
// Sort by weekly usage (most prominent metric)
269-
result.sort((a, b) => (b.weekly_spent || 0) - (a.weekly_spent || 0))
270-
271-
return result
272-
},
273-
['agents-data'],
274-
{
275-
revalidate: 600, // 10 minutes
276-
tags: ['agents', 'api'],
277-
},
278-
)
279-
28010
export async function GET() {
28111
try {
28212
const result = await getCachedAgents()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { describe, test } from '@jest/globals'
2+
3+
describe.skip('AgentStoreClient fallback hydration (skipped placeholder)', () => {
4+
test('skipped', () => {})
5+
})

web/src/app/store/store-client.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,28 @@ export default function AgentStoreClient({
185185
loadingStateRef.current = { isLoadingMore, hasMore }
186186
}, [isLoadingMore, hasMore])
187187

188-
// Use the initial agents directly
189-
const agents = useMemo(() => {
190-
return initialAgents
188+
// Hydrate agents client-side if SSR provided none (build-time fallback)
189+
const [hydratedAgents, setHydratedAgents] = useState<AgentData[] | null>(null)
190+
useEffect(() => {
191+
let cancelled = false
192+
if ((initialAgents?.length ?? 0) === 0) {
193+
fetch('/api/agents')
194+
.then((res) => (res.ok ? res.json() : Promise.reject(res.statusText)))
195+
.then((data: AgentData[]) => {
196+
if (!cancelled) setHydratedAgents(data)
197+
})
198+
.catch(() => {})
199+
}
200+
return () => {
201+
cancelled = true
202+
}
191203
}, [initialAgents])
192204

205+
// Prefer hydrated data if present; else use SSR data
206+
const agents = useMemo(() => {
207+
return hydratedAgents ?? initialAgents
208+
}, [hydratedAgents, initialAgents])
209+
193210
const editorsChoice = useMemo(() => {
194211
return agents.filter((agent) => EDITORS_CHOICE_AGENTS.includes(agent.id))
195212
}, [agents])

0 commit comments

Comments
 (0)