apteva 0.4.44 → 0.4.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ActivityPage.c48n83h2.js → ActivityPage.sw9p594m.js} +1 -1
- package/dist/{ApiDocsPage.yzcxx5ax.js → ApiDocsPage.90e03bz7.js} +1 -1
- package/dist/App.3vnrera5.js +4 -0
- package/dist/App.94x6mh7f.js +20 -0
- package/dist/{App.qzbx5wtj.js → App.9sryp183.js} +1 -1
- package/dist/App.9t1zc5r7.js +53 -0
- package/dist/{App.r5serxkt.js → App.jhb45d7r.js} +1 -1
- package/dist/App.k4nmqgek.js +221 -0
- package/dist/App.p7jjw1zf.js +4 -0
- package/dist/App.pfbdzrhh.js +4 -0
- package/dist/App.stgng5bx.js +13 -0
- package/dist/{App.152mbs1r.js → App.tm3k7h4b.js} +1 -1
- package/dist/App.vkg121c6.js +4 -0
- package/dist/App.vza4fxg0.js +4 -0
- package/dist/App.wghtdzsk.js +1 -0
- package/dist/App.xva0tfzh.js +4 -0
- package/dist/App.ysxy7akk.js +61 -0
- package/dist/App.yzkh4gq2.js +4 -0
- package/dist/ConnectionsPage.q5f9fd37.js +3 -0
- package/dist/McpPage.f3ccrezb.js +3 -0
- package/dist/SettingsPage.q1pqcc93.js +3 -0
- package/dist/SkillsPage.whxnez67.js +3 -0
- package/dist/TasksPage.zp4jfevw.js +3 -0
- package/dist/TelemetryPage.an0ky78c.js +3 -0
- package/dist/TestsPage.18krj0d1.js +3 -0
- package/dist/ThreadsPage.nnphgy98.js +3 -0
- package/dist/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +10 -9
- package/src/db.ts +60 -22
- package/src/providers.ts +14 -9
- package/src/routes/api/agent-utils.ts +25 -3
- package/src/routes/api/telemetry.ts +21 -2
- package/src/server.ts +53 -1
- package/src/web/App.tsx +2 -2
- package/src/web/components/agents/AgentCard.tsx +9 -7
- package/src/web/components/agents/AgentPanel.tsx +205 -44
- package/src/web/components/agents/CreateAgentModal.tsx +5 -5
- package/src/web/components/auth/LoginPage.tsx +2 -2
- package/src/web/components/common/LoadingSpinner.tsx +1 -1
- package/src/web/components/common/Modal.tsx +6 -6
- package/src/web/components/common/Select.tsx +2 -2
- package/src/web/components/connections/ConnectionsPage.tsx +1 -1
- package/src/web/components/connections/IntegrationsTab.tsx +3 -3
- package/src/web/components/connections/OverviewTab.tsx +3 -3
- package/src/web/components/connections/TriggersTab.tsx +8 -8
- package/src/web/components/dashboard/Dashboard.tsx +4 -4
- package/src/web/components/layout/Header.tsx +3 -3
- package/src/web/components/layout/Sidebar.tsx +6 -5
- package/src/web/components/mcp/McpPage.tsx +13 -13
- package/src/web/components/onboarding/OnboardingWizard.tsx +2 -2
- package/src/web/components/settings/SettingsPage.tsx +59 -26
- package/src/web/components/skills/SkillsPage.tsx +7 -7
- package/src/web/components/tasks/TasksPage.tsx +212 -36
- package/src/web/components/telemetry/TelemetryPage.tsx +414 -94
- package/src/web/components/tests/TestsPage.tsx +2 -2
- package/src/web/components/threads/ThreadsPage.tsx +2 -2
- package/src/web/context/TelemetryContext.tsx +1 -0
- package/src/web/context/ThemeContext.tsx +31 -10
- package/src/web/index.html +1 -6
- package/src/web/styles.css +47 -0
- package/src/web/themes.ts +68 -5
- package/src/web/types.ts +1 -1
- package/dist/App.09yb8t0b.js +0 -1
- package/dist/App.3a67nx9w.js +0 -4
- package/dist/App.9epx6785.js +0 -4
- package/dist/App.d8955awp.js +0 -4
- package/dist/App.drwb57jq.js +0 -4
- package/dist/App.gssbmajb.js +0 -4
- package/dist/App.qw70pc29.js +0 -53
- package/dist/App.tpmp9020.js +0 -20
- package/dist/App.v2wb4d7d.js +0 -61
- package/dist/App.vxmaaj0m.js +0 -13
- package/dist/App.w4p2tda9.js +0 -4
- package/dist/App.wv2ng55q.js +0 -221
- package/dist/App.yncnrn0f.js +0 -4
- package/dist/ConnectionsPage.k6cspyqq.js +0 -3
- package/dist/McpPage.cdxm48xj.js +0 -3
- package/dist/SettingsPage.evpv7c2y.js +0 -3
- package/dist/SkillsPage.pvzp6c1a.js +0 -3
- package/dist/TasksPage.6jnvbpsy.js +0 -3
- package/dist/TelemetryPage.t7vk24zc.js +0 -3
- package/dist/TestsPage.5x6658aa.js +0 -3
- package/dist/ThreadsPage.3fvhtevh.js +0 -3
|
@@ -13,6 +13,9 @@ interface TelemetryStats {
|
|
|
13
13
|
total_errors: number;
|
|
14
14
|
total_input_tokens: number;
|
|
15
15
|
total_output_tokens: number;
|
|
16
|
+
total_cache_creation_tokens: number;
|
|
17
|
+
total_cache_read_tokens: number;
|
|
18
|
+
total_reasoning_tokens: number;
|
|
16
19
|
total_cost: number;
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -20,6 +23,22 @@ interface UsageByAgent {
|
|
|
20
23
|
agent_id: string;
|
|
21
24
|
input_tokens: number;
|
|
22
25
|
output_tokens: number;
|
|
26
|
+
cache_creation_tokens: number;
|
|
27
|
+
cache_read_tokens: number;
|
|
28
|
+
reasoning_tokens: number;
|
|
29
|
+
llm_calls: number;
|
|
30
|
+
tool_calls: number;
|
|
31
|
+
errors: number;
|
|
32
|
+
cost: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface UsageByProject {
|
|
36
|
+
project_id: string | null;
|
|
37
|
+
input_tokens: number;
|
|
38
|
+
output_tokens: number;
|
|
39
|
+
cache_creation_tokens: number;
|
|
40
|
+
cache_read_tokens: number;
|
|
41
|
+
reasoning_tokens: number;
|
|
23
42
|
llm_calls: number;
|
|
24
43
|
tool_calls: number;
|
|
25
44
|
errors: number;
|
|
@@ -30,9 +49,13 @@ interface DailyUsage {
|
|
|
30
49
|
date: string;
|
|
31
50
|
input_tokens: number;
|
|
32
51
|
output_tokens: number;
|
|
52
|
+
cache_creation_tokens: number;
|
|
53
|
+
cache_read_tokens: number;
|
|
54
|
+
reasoning_tokens: number;
|
|
33
55
|
llm_calls: number;
|
|
34
56
|
tool_calls: number;
|
|
35
57
|
errors: number;
|
|
58
|
+
cost: number;
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
// Helper to extract stats from a single event
|
|
@@ -42,12 +65,20 @@ function extractEventStats(event: TelemetryEvent): {
|
|
|
42
65
|
errors: number;
|
|
43
66
|
input_tokens: number;
|
|
44
67
|
output_tokens: number;
|
|
68
|
+
cache_creation_tokens: number;
|
|
69
|
+
cache_read_tokens: number;
|
|
70
|
+
reasoning_tokens: number;
|
|
71
|
+
cost: number;
|
|
45
72
|
} {
|
|
46
73
|
const isLlm = event.category === "LLM";
|
|
47
74
|
const isTool = event.category === "TOOL";
|
|
48
75
|
const isError = event.level === "error";
|
|
49
76
|
const inputTokens = (event.data?.input_tokens as number) || 0;
|
|
50
77
|
const outputTokens = (event.data?.output_tokens as number) || 0;
|
|
78
|
+
const cacheCreationTokens = (event.data?.cache_creation_tokens as number) || 0;
|
|
79
|
+
const cacheReadTokens = (event.data?.cache_read_tokens as number) || 0;
|
|
80
|
+
const reasoningTokens = (event.data?.reasoning_tokens as number) || 0;
|
|
81
|
+
const cost = (event.cost as number) || 0;
|
|
51
82
|
|
|
52
83
|
return {
|
|
53
84
|
llm_calls: isLlm ? 1 : 0,
|
|
@@ -55,17 +86,22 @@ function extractEventStats(event: TelemetryEvent): {
|
|
|
55
86
|
errors: isError ? 1 : 0,
|
|
56
87
|
input_tokens: inputTokens,
|
|
57
88
|
output_tokens: outputTokens,
|
|
89
|
+
cache_creation_tokens: cacheCreationTokens,
|
|
90
|
+
cache_read_tokens: cacheReadTokens,
|
|
91
|
+
reasoning_tokens: reasoningTokens,
|
|
92
|
+
cost,
|
|
58
93
|
};
|
|
59
94
|
}
|
|
60
95
|
|
|
61
96
|
export function TelemetryPage() {
|
|
62
97
|
const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
|
|
63
|
-
const { currentProjectId, currentProject, costTrackingEnabled } = useProjects();
|
|
98
|
+
const { currentProjectId, currentProject, costTrackingEnabled, projectsEnabled, projects } = useProjects();
|
|
64
99
|
const { authFetch } = useAuth();
|
|
65
100
|
const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
|
|
66
101
|
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
67
102
|
const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
|
|
68
103
|
const [dailyUsage, setDailyUsage] = useState<DailyUsage[]>([]);
|
|
104
|
+
const [projectUsage, setProjectUsage] = useState<UsageByProject[]>([]);
|
|
69
105
|
const [loading, setLoading] = useState(true);
|
|
70
106
|
const [filter, setFilter] = useState({
|
|
71
107
|
level: "",
|
|
@@ -76,8 +112,11 @@ export function TelemetryPage() {
|
|
|
76
112
|
const [agents, setAgents] = useState<Array<{ id: string; name: string; projectId: string | null }>>([]);
|
|
77
113
|
const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
|
|
78
114
|
|
|
115
|
+
// Time range filter
|
|
116
|
+
const [timeRange, setTimeRange] = useState<string>("all");
|
|
117
|
+
|
|
79
118
|
// Sort state for usage table
|
|
80
|
-
type SortKey = "agent" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "errors" | "cost";
|
|
119
|
+
type SortKey = "agent" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
|
|
81
120
|
const [sortKey, setSortKey] = useState<SortKey>("cost");
|
|
82
121
|
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
|
|
83
122
|
|
|
@@ -90,6 +129,20 @@ export function TelemetryPage() {
|
|
|
90
129
|
}
|
|
91
130
|
};
|
|
92
131
|
|
|
132
|
+
// Sort state for project usage table
|
|
133
|
+
type ProjectSortKey = "project" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
|
|
134
|
+
const [projectSortKey, setProjectSortKey] = useState<ProjectSortKey>("cost");
|
|
135
|
+
const [projectSortDir, setProjectSortDir] = useState<"asc" | "desc">("desc");
|
|
136
|
+
|
|
137
|
+
const handleProjectSort = (key: ProjectSortKey) => {
|
|
138
|
+
if (projectSortKey === key) {
|
|
139
|
+
setProjectSortDir(d => d === "asc" ? "desc" : "asc");
|
|
140
|
+
} else {
|
|
141
|
+
setProjectSortKey(key);
|
|
142
|
+
setProjectSortDir("desc");
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
93
146
|
// Track IDs that were in the fetched stats to avoid double-counting
|
|
94
147
|
const countedEventIdsRef = useRef<Set<string>>(new Set());
|
|
95
148
|
|
|
@@ -121,16 +174,35 @@ export function TelemetryPage() {
|
|
|
121
174
|
// Get agent IDs for the current project
|
|
122
175
|
const projectAgentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
|
|
123
176
|
|
|
177
|
+
// Compute `since` ISO string from selected time range
|
|
178
|
+
const getSince = useCallback((): string | undefined => {
|
|
179
|
+
if (timeRange === "all") return undefined;
|
|
180
|
+
const now = new Date();
|
|
181
|
+
const ms: Record<string, number> = {
|
|
182
|
+
"1h": 3600000,
|
|
183
|
+
"6h": 6 * 3600000,
|
|
184
|
+
"24h": 24 * 3600000,
|
|
185
|
+
"7d": 7 * 24 * 3600000,
|
|
186
|
+
"30d": 30 * 24 * 3600000,
|
|
187
|
+
};
|
|
188
|
+
if (ms[timeRange]) {
|
|
189
|
+
return new Date(now.getTime() - ms[timeRange]).toISOString();
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}, [timeRange]);
|
|
193
|
+
|
|
124
194
|
// Fetch stats and historical data (less frequently now since we have real-time)
|
|
125
195
|
const fetchData = async () => {
|
|
126
196
|
setLoading(true);
|
|
127
197
|
try {
|
|
128
198
|
// Build project filter param
|
|
129
199
|
const projectParam = currentProjectId === "unassigned" ? "null" : currentProjectId || "";
|
|
200
|
+
const since = getSince();
|
|
130
201
|
|
|
131
202
|
// Fetch stats
|
|
132
203
|
const statsParams = new URLSearchParams();
|
|
133
204
|
if (projectParam) statsParams.set("project_id", projectParam);
|
|
205
|
+
if (since) statsParams.set("since", since);
|
|
134
206
|
const statsRes = await authFetch(`/api/telemetry/stats${statsParams.toString() ? `?${statsParams}` : ""}`);
|
|
135
207
|
const statsData = await statsRes.json();
|
|
136
208
|
setFetchedStats(statsData.stats);
|
|
@@ -140,6 +212,7 @@ export function TelemetryPage() {
|
|
|
140
212
|
if (filter.level) params.set("level", filter.level);
|
|
141
213
|
if (filter.agent_id) params.set("agent_id", filter.agent_id);
|
|
142
214
|
if (projectParam) params.set("project_id", projectParam);
|
|
215
|
+
if (since) params.set("since", since);
|
|
143
216
|
params.set("limit", "100"); // Fetch more since we filter client-side
|
|
144
217
|
|
|
145
218
|
const eventsRes = await authFetch(`/api/telemetry/events?${params}`);
|
|
@@ -154,6 +227,7 @@ export function TelemetryPage() {
|
|
|
154
227
|
const usageParams = new URLSearchParams();
|
|
155
228
|
usageParams.set("group_by", "agent");
|
|
156
229
|
if (projectParam) usageParams.set("project_id", projectParam);
|
|
230
|
+
if (since) usageParams.set("since", since);
|
|
157
231
|
const usageRes = await authFetch(`/api/telemetry/usage?${usageParams}`);
|
|
158
232
|
const usageData = await usageRes.json();
|
|
159
233
|
setFetchedUsage(usageData.usage || []);
|
|
@@ -162,6 +236,7 @@ export function TelemetryPage() {
|
|
|
162
236
|
const dailyParams = new URLSearchParams();
|
|
163
237
|
dailyParams.set("group_by", "day");
|
|
164
238
|
if (projectParam) dailyParams.set("project_id", projectParam);
|
|
239
|
+
if (since) dailyParams.set("since", since);
|
|
165
240
|
const dailyRes = await authFetch(`/api/telemetry/usage?${dailyParams}`);
|
|
166
241
|
const dailyData = await dailyRes.json();
|
|
167
242
|
// Sort by date ascending for charts
|
|
@@ -169,6 +244,18 @@ export function TelemetryPage() {
|
|
|
169
244
|
a.date.localeCompare(b.date)
|
|
170
245
|
);
|
|
171
246
|
setDailyUsage(sorted);
|
|
247
|
+
|
|
248
|
+
// Fetch usage by project (only when viewing all projects and feature is enabled)
|
|
249
|
+
if (projectsEnabled && currentProjectId === null) {
|
|
250
|
+
const projParams = new URLSearchParams();
|
|
251
|
+
projParams.set("group_by", "project");
|
|
252
|
+
if (since) projParams.set("since", since);
|
|
253
|
+
const projRes = await authFetch(`/api/telemetry/usage?${projParams}`);
|
|
254
|
+
const projData = await projRes.json();
|
|
255
|
+
setProjectUsage(projData.usage || []);
|
|
256
|
+
} else {
|
|
257
|
+
setProjectUsage([]);
|
|
258
|
+
}
|
|
172
259
|
} catch (e) {
|
|
173
260
|
console.error("Failed to fetch telemetry:", e);
|
|
174
261
|
}
|
|
@@ -177,7 +264,7 @@ export function TelemetryPage() {
|
|
|
177
264
|
|
|
178
265
|
useEffect(() => {
|
|
179
266
|
fetchData();
|
|
180
|
-
}, [filter, currentProjectId, authFetch, statusChangeCounter]);
|
|
267
|
+
}, [filter, timeRange, currentProjectId, authFetch, statusChangeCounter]);
|
|
181
268
|
|
|
182
269
|
// Compute real-time stats from new events (not already counted in fetched stats)
|
|
183
270
|
const stats = useMemo(() => {
|
|
@@ -190,6 +277,10 @@ export function TelemetryPage() {
|
|
|
190
277
|
let deltaErrors = 0;
|
|
191
278
|
let deltaInputTokens = 0;
|
|
192
279
|
let deltaOutputTokens = 0;
|
|
280
|
+
let deltaCacheCreationTokens = 0;
|
|
281
|
+
let deltaCacheReadTokens = 0;
|
|
282
|
+
let deltaReasoningTokens = 0;
|
|
283
|
+
let deltaCost = 0;
|
|
193
284
|
|
|
194
285
|
for (const event of realtimeEvents) {
|
|
195
286
|
if (!countedEventIdsRef.current.has(event.id)) {
|
|
@@ -200,6 +291,10 @@ export function TelemetryPage() {
|
|
|
200
291
|
deltaErrors += eventStats.errors;
|
|
201
292
|
deltaInputTokens += eventStats.input_tokens;
|
|
202
293
|
deltaOutputTokens += eventStats.output_tokens;
|
|
294
|
+
deltaCacheCreationTokens += eventStats.cache_creation_tokens;
|
|
295
|
+
deltaCacheReadTokens += eventStats.cache_read_tokens;
|
|
296
|
+
deltaReasoningTokens += eventStats.reasoning_tokens;
|
|
297
|
+
deltaCost += eventStats.cost;
|
|
203
298
|
}
|
|
204
299
|
}
|
|
205
300
|
|
|
@@ -210,7 +305,10 @@ export function TelemetryPage() {
|
|
|
210
305
|
total_errors: fetchedStats.total_errors + deltaErrors,
|
|
211
306
|
total_input_tokens: fetchedStats.total_input_tokens + deltaInputTokens,
|
|
212
307
|
total_output_tokens: fetchedStats.total_output_tokens + deltaOutputTokens,
|
|
213
|
-
|
|
308
|
+
total_cache_creation_tokens: (fetchedStats.total_cache_creation_tokens || 0) + deltaCacheCreationTokens,
|
|
309
|
+
total_cache_read_tokens: (fetchedStats.total_cache_read_tokens || 0) + deltaCacheReadTokens,
|
|
310
|
+
total_reasoning_tokens: (fetchedStats.total_reasoning_tokens || 0) + deltaReasoningTokens,
|
|
311
|
+
total_cost: (fetchedStats.total_cost || 0) + deltaCost,
|
|
214
312
|
};
|
|
215
313
|
}, [fetchedStats, realtimeEvents]);
|
|
216
314
|
|
|
@@ -233,6 +331,10 @@ export function TelemetryPage() {
|
|
|
233
331
|
existing.errors += eventStats.errors;
|
|
234
332
|
existing.input_tokens += eventStats.input_tokens;
|
|
235
333
|
existing.output_tokens += eventStats.output_tokens;
|
|
334
|
+
existing.cache_creation_tokens += eventStats.cache_creation_tokens;
|
|
335
|
+
existing.cache_read_tokens += eventStats.cache_read_tokens;
|
|
336
|
+
existing.reasoning_tokens += eventStats.reasoning_tokens;
|
|
337
|
+
existing.cost += eventStats.cost;
|
|
236
338
|
} else {
|
|
237
339
|
usageMap.set(event.agent_id, {
|
|
238
340
|
agent_id: event.agent_id,
|
|
@@ -241,7 +343,10 @@ export function TelemetryPage() {
|
|
|
241
343
|
errors: eventStats.errors,
|
|
242
344
|
input_tokens: eventStats.input_tokens,
|
|
243
345
|
output_tokens: eventStats.output_tokens,
|
|
244
|
-
|
|
346
|
+
cache_creation_tokens: eventStats.cache_creation_tokens,
|
|
347
|
+
cache_read_tokens: eventStats.cache_read_tokens,
|
|
348
|
+
reasoning_tokens: eventStats.reasoning_tokens,
|
|
349
|
+
cost: eventStats.cost,
|
|
245
350
|
});
|
|
246
351
|
}
|
|
247
352
|
}
|
|
@@ -266,6 +371,70 @@ export function TelemetryPage() {
|
|
|
266
371
|
return sorted;
|
|
267
372
|
}, [usage, sortKey, sortDir, agents]);
|
|
268
373
|
|
|
374
|
+
// Sorted project usage for the table
|
|
375
|
+
const sortedProjectUsage = useMemo(() => {
|
|
376
|
+
const sorted = [...projectUsage];
|
|
377
|
+
sorted.sort((a, b) => {
|
|
378
|
+
if (projectSortKey === "project") {
|
|
379
|
+
const aName = (a.project_id ? projects.find(p => p.id === a.project_id)?.name || a.project_id : "Unassigned").toLowerCase();
|
|
380
|
+
const bName = (b.project_id ? projects.find(p => p.id === b.project_id)?.name || b.project_id : "Unassigned").toLowerCase();
|
|
381
|
+
return projectSortDir === "asc" ? (aName < bName ? -1 : 1) : (aName > bName ? -1 : 1);
|
|
382
|
+
}
|
|
383
|
+
const aVal = a[projectSortKey] as number;
|
|
384
|
+
const bVal = b[projectSortKey] as number;
|
|
385
|
+
return projectSortDir === "asc" ? aVal - bVal : bVal - aVal;
|
|
386
|
+
});
|
|
387
|
+
return sorted;
|
|
388
|
+
}, [projectUsage, projectSortKey, projectSortDir, projects]);
|
|
389
|
+
|
|
390
|
+
// Compute real-time chart data by merging fetched daily usage with SSE deltas
|
|
391
|
+
const chartData = useMemo(() => {
|
|
392
|
+
const buckets = new Map<string, DailyUsage>();
|
|
393
|
+
const useDaily = dailyUsage.length > 1;
|
|
394
|
+
|
|
395
|
+
// Seed with fetched daily data
|
|
396
|
+
for (const d of dailyUsage) {
|
|
397
|
+
buckets.set(d.date, { ...d });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Add deltas from real-time events not already counted
|
|
401
|
+
for (const event of realtimeEvents) {
|
|
402
|
+
if (!countedEventIdsRef.current.has(event.id)) {
|
|
403
|
+
const ts = new Date(event.timestamp);
|
|
404
|
+
const key = useDaily
|
|
405
|
+
? `${ts.getFullYear()}-${String(ts.getMonth() + 1).padStart(2, "0")}-${String(ts.getDate()).padStart(2, "0")}`
|
|
406
|
+
: `${ts.getFullYear()}-${String(ts.getMonth() + 1).padStart(2, "0")}-${String(ts.getDate()).padStart(2, "0")} ${String(ts.getHours()).padStart(2, "0")}:00`;
|
|
407
|
+
if (!buckets.has(key)) {
|
|
408
|
+
buckets.set(key, { date: key, llm_calls: 0, tool_calls: 0, errors: 0, input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, reasoning_tokens: 0, cost: 0 });
|
|
409
|
+
}
|
|
410
|
+
const b = buckets.get(key)!;
|
|
411
|
+
const s = extractEventStats(event);
|
|
412
|
+
b.llm_calls += s.llm_calls;
|
|
413
|
+
b.tool_calls += s.tool_calls;
|
|
414
|
+
b.errors += s.errors;
|
|
415
|
+
b.input_tokens += s.input_tokens;
|
|
416
|
+
b.output_tokens += s.output_tokens;
|
|
417
|
+
b.cache_creation_tokens += s.cache_creation_tokens;
|
|
418
|
+
b.cache_read_tokens += s.cache_read_tokens;
|
|
419
|
+
b.reasoning_tokens += s.reasoning_tokens;
|
|
420
|
+
b.cost += s.cost;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
425
|
+
}, [dailyUsage, realtimeEvents]);
|
|
426
|
+
|
|
427
|
+
const getProjectName = (projectId: string | null) => {
|
|
428
|
+
if (!projectId) return "Unassigned";
|
|
429
|
+
const project = projects.find(p => p.id === projectId);
|
|
430
|
+
return project?.name || projectId;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const getProjectColor = (projectId: string | null) => {
|
|
434
|
+
if (!projectId) return undefined;
|
|
435
|
+
return projects.find(p => p.id === projectId)?.color;
|
|
436
|
+
};
|
|
437
|
+
|
|
269
438
|
// Merge real-time events with historical, filtering and deduping
|
|
270
439
|
const allEvents = React.useMemo(() => {
|
|
271
440
|
// Apply filters to real-time events
|
|
@@ -408,26 +577,59 @@ export function TelemetryPage() {
|
|
|
408
577
|
)}
|
|
409
578
|
<h1 className="text-2xl font-semibold">
|
|
410
579
|
{currentProjectId === null
|
|
411
|
-
? "
|
|
580
|
+
? "Analytics"
|
|
412
581
|
: currentProjectId === "unassigned"
|
|
413
|
-
? "
|
|
414
|
-
: `
|
|
582
|
+
? "Analytics - Unassigned"
|
|
583
|
+
: `Analytics - ${currentProject?.name || ""}`}
|
|
415
584
|
</h1>
|
|
416
585
|
</div>
|
|
417
586
|
<p className="text-[var(--color-text-muted)]">
|
|
418
|
-
Monitor agent activity,
|
|
587
|
+
Monitor agent activity, usage, costs, and performance.
|
|
419
588
|
</p>
|
|
420
589
|
</div>
|
|
421
590
|
|
|
591
|
+
{/* Time Range Selector */}
|
|
592
|
+
<div className="flex gap-2 mb-6">
|
|
593
|
+
{[
|
|
594
|
+
{ value: "1h", label: "Last hour" },
|
|
595
|
+
{ value: "6h", label: "Last 6h" },
|
|
596
|
+
{ value: "24h", label: "Last 24h" },
|
|
597
|
+
{ value: "7d", label: "Last 7 days" },
|
|
598
|
+
{ value: "30d", label: "Last 30 days" },
|
|
599
|
+
{ value: "all", label: "All time" },
|
|
600
|
+
].map(opt => (
|
|
601
|
+
<button
|
|
602
|
+
key={opt.value}
|
|
603
|
+
onClick={() => setTimeRange(opt.value)}
|
|
604
|
+
className={`px-3 py-1.5 rounded text-sm transition ${
|
|
605
|
+
timeRange === opt.value
|
|
606
|
+
? "bg-[var(--color-accent)] text-black"
|
|
607
|
+
: "bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-[var(--color-text-secondary)]"
|
|
608
|
+
}`}
|
|
609
|
+
>
|
|
610
|
+
{opt.label}
|
|
611
|
+
</button>
|
|
612
|
+
))}
|
|
613
|
+
</div>
|
|
614
|
+
|
|
422
615
|
{/* Stats Cards */}
|
|
423
616
|
{stats && (
|
|
424
|
-
<div className=
|
|
617
|
+
<div className="flex flex-wrap gap-4 mb-6">
|
|
425
618
|
<StatCard label="Events" value={formatNumber(stats.total_events)} />
|
|
426
619
|
<StatCard label="LLM Calls" value={formatNumber(stats.total_llm_calls)} />
|
|
427
620
|
<StatCard label="Tool Calls" value={formatNumber(stats.total_tool_calls)} />
|
|
428
621
|
<StatCard label="Errors" value={formatNumber(stats.total_errors)} color="red" />
|
|
429
622
|
<StatCard label="Input Tokens" value={formatNumber(stats.total_input_tokens)} />
|
|
430
623
|
<StatCard label="Output Tokens" value={formatNumber(stats.total_output_tokens)} />
|
|
624
|
+
{(stats.total_cache_creation_tokens > 0 || stats.total_cache_read_tokens > 0) && (
|
|
625
|
+
<>
|
|
626
|
+
<StatCard label="Cache Write" value={formatNumber(stats.total_cache_creation_tokens)} />
|
|
627
|
+
<StatCard label="Cache Read" value={formatNumber(stats.total_cache_read_tokens)} />
|
|
628
|
+
</>
|
|
629
|
+
)}
|
|
630
|
+
{stats.total_reasoning_tokens > 0 && (
|
|
631
|
+
<StatCard label="Reasoning" value={formatNumber(stats.total_reasoning_tokens)} />
|
|
632
|
+
)}
|
|
431
633
|
{costTrackingEnabled && (
|
|
432
634
|
<StatCard label="Total Cost" value={`$${stats.total_cost.toFixed(4)}`} color="orange" />
|
|
433
635
|
)}
|
|
@@ -436,27 +638,7 @@ export function TelemetryPage() {
|
|
|
436
638
|
|
|
437
639
|
{/* Charts */}
|
|
438
640
|
{(() => {
|
|
439
|
-
// Use daily data if we have multiple days, otherwise aggregate events by hour
|
|
440
641
|
const useDaily = dailyUsage.length > 1;
|
|
441
|
-
const chartData = useDaily ? dailyUsage : (() => {
|
|
442
|
-
// Aggregate all visible events by hour
|
|
443
|
-
const buckets = new Map<string, { date: string; llm_calls: number; tool_calls: number; errors: number; input_tokens: number; output_tokens: number }>();
|
|
444
|
-
for (const event of allEvents) {
|
|
445
|
-
const d = new Date(event.timestamp);
|
|
446
|
-
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:00`;
|
|
447
|
-
if (!buckets.has(key)) {
|
|
448
|
-
buckets.set(key, { date: key, llm_calls: 0, tool_calls: 0, errors: 0, input_tokens: 0, output_tokens: 0 });
|
|
449
|
-
}
|
|
450
|
-
const b = buckets.get(key)!;
|
|
451
|
-
const s = extractEventStats(event);
|
|
452
|
-
b.llm_calls += s.llm_calls;
|
|
453
|
-
b.tool_calls += s.tool_calls;
|
|
454
|
-
b.errors += s.errors;
|
|
455
|
-
b.input_tokens += s.input_tokens;
|
|
456
|
-
b.output_tokens += s.output_tokens;
|
|
457
|
-
}
|
|
458
|
-
return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
459
|
-
})();
|
|
460
642
|
const chartLabel = useDaily ? "Daily" : "Hourly";
|
|
461
643
|
|
|
462
644
|
if (chartData.length === 0) return null;
|
|
@@ -464,7 +646,7 @@ export function TelemetryPage() {
|
|
|
464
646
|
return (
|
|
465
647
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
|
466
648
|
{/* Activity Chart */}
|
|
467
|
-
<div className="bg-[var(--color-surface)]
|
|
649
|
+
<div className="bg-[var(--color-surface)] card p-4">
|
|
468
650
|
<h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Activity</h3>
|
|
469
651
|
<ResponsiveContainer width="100%" height={200}>
|
|
470
652
|
<AreaChart data={chartData}>
|
|
@@ -506,6 +688,7 @@ export function TelemetryPage() {
|
|
|
506
688
|
fill="var(--color-accent)"
|
|
507
689
|
fillOpacity={0.15}
|
|
508
690
|
strokeWidth={1.5}
|
|
691
|
+
|
|
509
692
|
/>
|
|
510
693
|
<Area
|
|
511
694
|
type="monotone"
|
|
@@ -515,6 +698,7 @@ export function TelemetryPage() {
|
|
|
515
698
|
fill="var(--color-accent-hover)"
|
|
516
699
|
fillOpacity={0.08}
|
|
517
700
|
strokeWidth={1.5}
|
|
701
|
+
|
|
518
702
|
/>
|
|
519
703
|
<Area
|
|
520
704
|
type="monotone"
|
|
@@ -524,70 +708,97 @@ export function TelemetryPage() {
|
|
|
524
708
|
fill="#ef4444"
|
|
525
709
|
fillOpacity={0.1}
|
|
526
710
|
strokeWidth={1.5}
|
|
711
|
+
|
|
527
712
|
/>
|
|
528
713
|
</AreaChart>
|
|
529
714
|
</ResponsiveContainer>
|
|
530
715
|
</div>
|
|
531
716
|
|
|
532
|
-
{/* Token Usage Chart */}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
fontSize:
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
717
|
+
{/* Token Usage Chart - stacked bars: Input (cache read / cache write / regular) and Output (reasoning / regular) */}
|
|
718
|
+
{(() => {
|
|
719
|
+
// Compute stacked segments for each bucket
|
|
720
|
+
const stackedData = chartData.map(d => {
|
|
721
|
+
const cacheRead = d.cache_read_tokens || 0;
|
|
722
|
+
const cacheWrite = d.cache_creation_tokens || 0;
|
|
723
|
+
const regularInput = Math.max(0, d.input_tokens - cacheRead - cacheWrite);
|
|
724
|
+
const reasoning = d.reasoning_tokens || 0;
|
|
725
|
+
const regularOutput = Math.max(0, d.output_tokens - reasoning);
|
|
726
|
+
return {
|
|
727
|
+
date: d.date,
|
|
728
|
+
// Input stack (bottom to top: cache read, cache write, regular)
|
|
729
|
+
input_cache_read: cacheRead,
|
|
730
|
+
input_cache_write: cacheWrite,
|
|
731
|
+
input_regular: regularInput,
|
|
732
|
+
// Output stack (bottom to top: reasoning, regular)
|
|
733
|
+
output_reasoning: reasoning,
|
|
734
|
+
output_regular: regularOutput,
|
|
735
|
+
};
|
|
736
|
+
});
|
|
737
|
+
const hasCache = stackedData.some(d => d.input_cache_read > 0 || d.input_cache_write > 0);
|
|
738
|
+
const hasReasoning = stackedData.some(d => d.output_reasoning > 0);
|
|
739
|
+
|
|
740
|
+
return (
|
|
741
|
+
<div className="bg-[var(--color-surface)] card p-4">
|
|
742
|
+
<h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Token Usage</h3>
|
|
743
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
744
|
+
<BarChart data={stackedData}>
|
|
745
|
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
|
|
746
|
+
<XAxis
|
|
747
|
+
dataKey="date"
|
|
748
|
+
stroke="var(--color-border-light)"
|
|
749
|
+
tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
|
|
750
|
+
tickFormatter={(v) => {
|
|
751
|
+
if (!useDaily && v.includes(" ")) {
|
|
752
|
+
return v.split(" ")[1];
|
|
753
|
+
}
|
|
754
|
+
const d = new Date(v + "T00:00:00");
|
|
755
|
+
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
756
|
+
}}
|
|
757
|
+
/>
|
|
758
|
+
<YAxis
|
|
759
|
+
stroke="var(--color-border-light)"
|
|
760
|
+
tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
|
|
761
|
+
tickFormatter={(v) => {
|
|
762
|
+
if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
|
|
763
|
+
if (v >= 1000) return `${(v / 1000).toFixed(0)}K`;
|
|
764
|
+
return v;
|
|
765
|
+
}}
|
|
766
|
+
/>
|
|
767
|
+
<Tooltip
|
|
768
|
+
contentStyle={{
|
|
769
|
+
backgroundColor: "var(--color-surface)",
|
|
770
|
+
border: "1px solid var(--color-border-light)",
|
|
771
|
+
borderRadius: "8px",
|
|
772
|
+
fontSize: 12,
|
|
773
|
+
}}
|
|
774
|
+
labelStyle={{ color: "var(--color-text-secondary)" }}
|
|
775
|
+
cursor={{ fill: "rgba(255,255,255,0.03)" }}
|
|
776
|
+
labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
|
|
777
|
+
formatter={(value: number, name: string) => [value.toLocaleString(), name]}
|
|
778
|
+
/>
|
|
779
|
+
<Legend
|
|
780
|
+
wrapperStyle={{ fontSize: 11 }}
|
|
781
|
+
iconType="circle"
|
|
782
|
+
iconSize={8}
|
|
783
|
+
/>
|
|
784
|
+
{/* Input stack */}
|
|
785
|
+
{hasCache && (
|
|
786
|
+
<Bar dataKey="input_cache_read" name="Input (Cache Read)" stackId="input" fill="#fdba74" />
|
|
787
|
+
)}
|
|
788
|
+
{hasCache && (
|
|
789
|
+
<Bar dataKey="input_cache_write" name="Input (Cache Write)" stackId="input" fill="#fb923c" />
|
|
790
|
+
)}
|
|
791
|
+
<Bar dataKey="input_regular" name={hasCache ? "Input (Regular)" : "Input Tokens"} stackId="input" fill="#f97316" radius={[2, 2, 0, 0]} />
|
|
792
|
+
{/* Output stack */}
|
|
793
|
+
{hasReasoning && (
|
|
794
|
+
<Bar dataKey="output_reasoning" name="Output (Reasoning)" stackId="output" fill="#c4b5fd" />
|
|
795
|
+
)}
|
|
796
|
+
<Bar dataKey="output_regular" name={hasReasoning ? "Output (Regular)" : "Output Tokens"} stackId="output" fill="#a78bfa" radius={[2, 2, 0, 0]} />
|
|
797
|
+
</BarChart>
|
|
798
|
+
</ResponsiveContainer>
|
|
799
|
+
</div>
|
|
800
|
+
);
|
|
801
|
+
})()}
|
|
591
802
|
</div>
|
|
592
803
|
);
|
|
593
804
|
})()}
|
|
@@ -595,6 +806,8 @@ export function TelemetryPage() {
|
|
|
595
806
|
{/* Usage by Agent */}
|
|
596
807
|
{usage.length > 0 && (() => {
|
|
597
808
|
const maxCost = Math.max(...sortedUsage.map(u => u.cost), 0.0001);
|
|
809
|
+
const hasCacheTokens = sortedUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
|
|
810
|
+
const hasReasoningTokens = sortedUsage.some(u => (u.reasoning_tokens || 0) > 0);
|
|
598
811
|
const SortHeader = ({ label, field, align = "right" }: { label: string; field: SortKey; align?: string }) => (
|
|
599
812
|
<th
|
|
600
813
|
className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
|
|
@@ -615,7 +828,7 @@ export function TelemetryPage() {
|
|
|
615
828
|
return (
|
|
616
829
|
<div className="mb-6">
|
|
617
830
|
<h2 className="text-lg font-medium mb-3">Usage by Agent</h2>
|
|
618
|
-
<div className="bg-[var(--color-surface)]
|
|
831
|
+
<div className="bg-[var(--color-surface)] card overflow-hidden">
|
|
619
832
|
<table className="w-full text-sm">
|
|
620
833
|
<thead>
|
|
621
834
|
<tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
|
|
@@ -624,6 +837,9 @@ export function TelemetryPage() {
|
|
|
624
837
|
<SortHeader label="Tool Calls" field="tool_calls" />
|
|
625
838
|
<SortHeader label="Input Tokens" field="input_tokens" />
|
|
626
839
|
<SortHeader label="Output Tokens" field="output_tokens" />
|
|
840
|
+
{hasCacheTokens && <SortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
841
|
+
{hasCacheTokens && <SortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
842
|
+
{hasReasoningTokens && <SortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
627
843
|
<SortHeader label="Errors" field="errors" />
|
|
628
844
|
{costTrackingEnabled && <SortHeader label="Est. Cost" field="cost" />}
|
|
629
845
|
</tr>
|
|
@@ -636,6 +852,15 @@ export function TelemetryPage() {
|
|
|
636
852
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
|
|
637
853
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
|
|
638
854
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
|
|
855
|
+
{hasCacheTokens && (
|
|
856
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
|
|
857
|
+
)}
|
|
858
|
+
{hasCacheTokens && (
|
|
859
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
|
|
860
|
+
)}
|
|
861
|
+
{hasReasoningTokens && (
|
|
862
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
|
|
863
|
+
)}
|
|
639
864
|
<td className="p-3 text-right">
|
|
640
865
|
{u.errors > 0 ? (
|
|
641
866
|
<span className="text-red-400">{u.errors}</span>
|
|
@@ -665,6 +890,101 @@ export function TelemetryPage() {
|
|
|
665
890
|
);
|
|
666
891
|
})()}
|
|
667
892
|
|
|
893
|
+
{/* Usage by Project */}
|
|
894
|
+
{projectsEnabled && currentProjectId === null && sortedProjectUsage.length > 0 && (() => {
|
|
895
|
+
const maxCost = Math.max(...sortedProjectUsage.map(u => u.cost), 0.0001);
|
|
896
|
+
const hasProjCacheTokens = sortedProjectUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
|
|
897
|
+
const hasProjReasoningTokens = sortedProjectUsage.some(u => (u.reasoning_tokens || 0) > 0);
|
|
898
|
+
const PSortHeader = ({ label, field, align = "right" }: { label: string; field: ProjectSortKey; align?: string }) => (
|
|
899
|
+
<th
|
|
900
|
+
className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
|
|
901
|
+
onClick={() => handleProjectSort(field)}
|
|
902
|
+
>
|
|
903
|
+
<span className="inline-flex items-center gap-1">
|
|
904
|
+
{align === "right" && projectSortKey === field && (
|
|
905
|
+
<span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
|
|
906
|
+
)}
|
|
907
|
+
{label}
|
|
908
|
+
{align === "left" && projectSortKey === field && (
|
|
909
|
+
<span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
|
|
910
|
+
)}
|
|
911
|
+
</span>
|
|
912
|
+
</th>
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
return (
|
|
916
|
+
<div className="mb-6">
|
|
917
|
+
<h2 className="text-lg font-medium mb-3">Usage by Project</h2>
|
|
918
|
+
<div className="bg-[var(--color-surface)] card overflow-hidden">
|
|
919
|
+
<table className="w-full text-sm">
|
|
920
|
+
<thead>
|
|
921
|
+
<tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
|
|
922
|
+
<PSortHeader label="Project" field="project" align="left" />
|
|
923
|
+
<PSortHeader label="LLM Calls" field="llm_calls" />
|
|
924
|
+
<PSortHeader label="Tool Calls" field="tool_calls" />
|
|
925
|
+
<PSortHeader label="Input Tokens" field="input_tokens" />
|
|
926
|
+
<PSortHeader label="Output Tokens" field="output_tokens" />
|
|
927
|
+
{hasProjCacheTokens && <PSortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
928
|
+
{hasProjCacheTokens && <PSortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
929
|
+
{hasProjReasoningTokens && <PSortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
930
|
+
<PSortHeader label="Errors" field="errors" />
|
|
931
|
+
{costTrackingEnabled && <PSortHeader label="Est. Cost" field="cost" />}
|
|
932
|
+
</tr>
|
|
933
|
+
</thead>
|
|
934
|
+
<tbody>
|
|
935
|
+
{sortedProjectUsage.map((u) => {
|
|
936
|
+
const color = getProjectColor(u.project_id);
|
|
937
|
+
return (
|
|
938
|
+
<tr key={u.project_id || "_unassigned"} className="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg)]">
|
|
939
|
+
<td className="p-3 font-medium">
|
|
940
|
+
<span className="inline-flex items-center gap-2">
|
|
941
|
+
{color && <span className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} />}
|
|
942
|
+
{getProjectName(u.project_id)}
|
|
943
|
+
</span>
|
|
944
|
+
</td>
|
|
945
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.llm_calls)}</td>
|
|
946
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
|
|
947
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
|
|
948
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
|
|
949
|
+
{hasProjCacheTokens && (
|
|
950
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
|
|
951
|
+
)}
|
|
952
|
+
{hasProjCacheTokens && (
|
|
953
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
|
|
954
|
+
)}
|
|
955
|
+
{hasProjReasoningTokens && (
|
|
956
|
+
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
|
|
957
|
+
)}
|
|
958
|
+
<td className="p-3 text-right">
|
|
959
|
+
{u.errors > 0 ? (
|
|
960
|
+
<span className="text-red-400">{u.errors}</span>
|
|
961
|
+
) : (
|
|
962
|
+
<span className="text-[var(--color-text-faint)]">0</span>
|
|
963
|
+
)}
|
|
964
|
+
</td>
|
|
965
|
+
{costTrackingEnabled && (
|
|
966
|
+
<td className="p-3 text-right">
|
|
967
|
+
<div className="flex items-center justify-end gap-2">
|
|
968
|
+
<div className="w-16 h-1.5 bg-[var(--color-surface-raised)] rounded-full overflow-hidden">
|
|
969
|
+
<div
|
|
970
|
+
className="h-full bg-orange-500 rounded-full"
|
|
971
|
+
style={{ width: `${(u.cost / maxCost) * 100}%` }}
|
|
972
|
+
/>
|
|
973
|
+
</div>
|
|
974
|
+
<span className="text-[var(--color-text-secondary)] min-w-[60px] text-right">${u.cost.toFixed(4)}</span>
|
|
975
|
+
</div>
|
|
976
|
+
</td>
|
|
977
|
+
)}
|
|
978
|
+
</tr>
|
|
979
|
+
);
|
|
980
|
+
})}
|
|
981
|
+
</tbody>
|
|
982
|
+
</table>
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
);
|
|
986
|
+
})()}
|
|
987
|
+
|
|
668
988
|
{/* Filters */}
|
|
669
989
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
|
670
990
|
<div className="w-44">
|
|
@@ -714,7 +1034,7 @@ export function TelemetryPage() {
|
|
|
714
1034
|
</div>
|
|
715
1035
|
|
|
716
1036
|
{/* Events List */}
|
|
717
|
-
<div className="bg-[var(--color-surface)]
|
|
1037
|
+
<div className="bg-[var(--color-surface)] card">
|
|
718
1038
|
<div className="p-3 border-b border-[var(--color-border)] flex items-center justify-between">
|
|
719
1039
|
<h2 className="font-medium">Recent Events</h2>
|
|
720
1040
|
{realtimeEvents.length > 0 && (
|
|
@@ -728,7 +1048,7 @@ export function TelemetryPage() {
|
|
|
728
1048
|
<div className="p-8 text-center text-[var(--color-text-muted)]">Loading...</div>
|
|
729
1049
|
) : allEvents.length === 0 ? (
|
|
730
1050
|
<div className="p-8 text-center text-[var(--color-text-muted)]">
|
|
731
|
-
No
|
|
1051
|
+
No events yet. Events will appear here in real-time once agents start sending data.
|
|
732
1052
|
</div>
|
|
733
1053
|
) : (
|
|
734
1054
|
<div className="divide-y divide-[var(--color-border)]">
|
|
@@ -791,7 +1111,7 @@ export function TelemetryPage() {
|
|
|
791
1111
|
|
|
792
1112
|
function StatCard({ label, value, color }: { label: string; value: string; color?: string }) {
|
|
793
1113
|
return (
|
|
794
|
-
<div className="bg-[var(--color-surface)]
|
|
1114
|
+
<div className="bg-[var(--color-surface)] card p-4 flex-1 min-w-[120px]">
|
|
795
1115
|
<div className="text-[var(--color-text-muted)] text-xs mb-1">{label}</div>
|
|
796
1116
|
<div className={`text-2xl font-semibold ${color === "red" ? "text-red-400" : color === "orange" ? "text-orange-400" : ""}`}>
|
|
797
1117
|
{value}
|