apteva 0.2.6 → 0.2.8
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/App.hzbfeg94.js +217 -0
- package/dist/index.html +3 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +386 -0
- package/src/auth/middleware.ts +183 -0
- package/src/binary.ts +19 -1
- package/src/db.ts +570 -32
- package/src/routes/api.ts +913 -38
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +60 -8
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +30 -41
- package/src/web/components/agents/AgentPanel.tsx +751 -11
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/auth/CreateAccountStep.tsx +176 -0
- package/src/web/components/auth/LoginPage.tsx +91 -0
- package/src/web/components/auth/index.ts +2 -0
- package/src/web/components/common/Icons.tsx +48 -0
- package/src/web/components/common/Modal.tsx +1 -1
- package/src/web/components/dashboard/Dashboard.tsx +91 -31
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +145 -15
- package/src/web/components/layout/Sidebar.tsx +81 -43
- package/src/web/components/mcp/McpPage.tsx +261 -32
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +404 -18
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/TelemetryContext.tsx +98 -76
- package/src/web/context/index.ts +5 -0
- package/src/web/hooks/useAgents.ts +18 -6
- package/src/web/hooks/useOnboarding.ts +20 -4
- package/src/web/hooks/useProviders.ts +15 -5
- package/src/web/icon.png +0 -0
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +6 -0
- package/dist/App.0mzj9cz9.js +0 -213
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
2
2
|
import { Select } from "../common/Select";
|
|
3
|
-
import { useTelemetryContext, type TelemetryEvent } from "../../context";
|
|
3
|
+
import { useTelemetryContext, useProjects, useAuth, type TelemetryEvent } from "../../context";
|
|
4
4
|
|
|
5
5
|
interface TelemetryStats {
|
|
6
6
|
total_events: number;
|
|
@@ -20,25 +20,58 @@ interface UsageByAgent {
|
|
|
20
20
|
errors: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Helper to extract stats from a single event
|
|
24
|
+
function extractEventStats(event: TelemetryEvent): {
|
|
25
|
+
llm_calls: number;
|
|
26
|
+
tool_calls: number;
|
|
27
|
+
errors: number;
|
|
28
|
+
input_tokens: number;
|
|
29
|
+
output_tokens: number;
|
|
30
|
+
} {
|
|
31
|
+
const isLlm = event.category === "LLM";
|
|
32
|
+
const isTool = event.category === "TOOL";
|
|
33
|
+
const isError = event.level === "error";
|
|
34
|
+
const inputTokens = (event.data?.input_tokens as number) || 0;
|
|
35
|
+
const outputTokens = (event.data?.output_tokens as number) || 0;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
llm_calls: isLlm ? 1 : 0,
|
|
39
|
+
tool_calls: isTool ? 1 : 0,
|
|
40
|
+
errors: isError ? 1 : 0,
|
|
41
|
+
input_tokens: inputTokens,
|
|
42
|
+
output_tokens: outputTokens,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
export function TelemetryPage() {
|
|
24
|
-
const {
|
|
25
|
-
const
|
|
47
|
+
const { events: realtimeEvents } = useTelemetryContext();
|
|
48
|
+
const { currentProjectId, currentProject } = useProjects();
|
|
49
|
+
const { authFetch } = useAuth();
|
|
50
|
+
const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
|
|
26
51
|
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
27
|
-
const [
|
|
52
|
+
const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
|
|
28
53
|
const [loading, setLoading] = useState(true);
|
|
29
54
|
const [filter, setFilter] = useState({
|
|
30
|
-
category: "",
|
|
31
55
|
level: "",
|
|
32
56
|
agent_id: "",
|
|
33
57
|
});
|
|
34
|
-
|
|
58
|
+
// Categories to hide (DATABASE hidden by default)
|
|
59
|
+
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set(["DATABASE"]));
|
|
60
|
+
const [agents, setAgents] = useState<Array<{ id: string; name: string; projectId: string | null }>>([]);
|
|
35
61
|
const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
|
|
36
62
|
|
|
63
|
+
// Track IDs that were in the fetched stats to avoid double-counting
|
|
64
|
+
const countedEventIdsRef = useRef<Set<string>>(new Set());
|
|
65
|
+
|
|
66
|
+
// Track which events are "new" (for animation) - stores event IDs with their arrival time
|
|
67
|
+
const [newEventIds, setNewEventIds] = useState<Set<string>>(new Set());
|
|
68
|
+
const seenEventIdsRef = useRef<Set<string>>(new Set());
|
|
69
|
+
|
|
37
70
|
// Fetch agents for dropdown
|
|
38
71
|
useEffect(() => {
|
|
39
72
|
const fetchAgents = async () => {
|
|
40
73
|
try {
|
|
41
|
-
const res = await
|
|
74
|
+
const res = await authFetch("/api/agents");
|
|
42
75
|
const data = await res.json();
|
|
43
76
|
setAgents(data.agents || []);
|
|
44
77
|
} catch (e) {
|
|
@@ -46,32 +79,54 @@ export function TelemetryPage() {
|
|
|
46
79
|
}
|
|
47
80
|
};
|
|
48
81
|
fetchAgents();
|
|
49
|
-
}, []);
|
|
82
|
+
}, [authFetch]);
|
|
83
|
+
|
|
84
|
+
// Filter agents by project
|
|
85
|
+
const filteredAgents = useMemo(() => {
|
|
86
|
+
if (currentProjectId === null) return agents;
|
|
87
|
+
if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
|
|
88
|
+
return agents.filter(a => a.projectId === currentProjectId);
|
|
89
|
+
}, [agents, currentProjectId]);
|
|
90
|
+
|
|
91
|
+
// Get agent IDs for the current project
|
|
92
|
+
const projectAgentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
|
|
50
93
|
|
|
51
94
|
// Fetch stats and historical data (less frequently now since we have real-time)
|
|
52
95
|
const fetchData = async () => {
|
|
53
96
|
setLoading(true);
|
|
54
97
|
try {
|
|
98
|
+
// Build project filter param
|
|
99
|
+
const projectParam = currentProjectId === "unassigned" ? "null" : currentProjectId || "";
|
|
100
|
+
|
|
55
101
|
// Fetch stats
|
|
56
|
-
const
|
|
102
|
+
const statsParams = new URLSearchParams();
|
|
103
|
+
if (projectParam) statsParams.set("project_id", projectParam);
|
|
104
|
+
const statsRes = await authFetch(`/api/telemetry/stats${statsParams.toString() ? `?${statsParams}` : ""}`);
|
|
57
105
|
const statsData = await statsRes.json();
|
|
58
|
-
|
|
106
|
+
setFetchedStats(statsData.stats);
|
|
59
107
|
|
|
60
108
|
// Fetch historical events with filters
|
|
61
109
|
const params = new URLSearchParams();
|
|
62
|
-
if (filter.category) params.set("category", filter.category);
|
|
63
110
|
if (filter.level) params.set("level", filter.level);
|
|
64
111
|
if (filter.agent_id) params.set("agent_id", filter.agent_id);
|
|
65
|
-
params.set("
|
|
112
|
+
if (projectParam) params.set("project_id", projectParam);
|
|
113
|
+
params.set("limit", "100"); // Fetch more since we filter client-side
|
|
66
114
|
|
|
67
|
-
const eventsRes = await
|
|
115
|
+
const eventsRes = await authFetch(`/api/telemetry/events?${params}`);
|
|
68
116
|
const eventsData = await eventsRes.json();
|
|
69
|
-
|
|
117
|
+
const events = eventsData.events || [];
|
|
118
|
+
setHistoricalEvents(events);
|
|
119
|
+
|
|
120
|
+
// Mark all fetched event IDs as counted (stats already include them)
|
|
121
|
+
countedEventIdsRef.current = new Set(events.map((e: TelemetryEvent) => e.id));
|
|
70
122
|
|
|
71
123
|
// Fetch usage by agent
|
|
72
|
-
const
|
|
124
|
+
const usageParams = new URLSearchParams();
|
|
125
|
+
usageParams.set("group_by", "agent");
|
|
126
|
+
if (projectParam) usageParams.set("project_id", projectParam);
|
|
127
|
+
const usageRes = await authFetch(`/api/telemetry/usage?${usageParams}`);
|
|
73
128
|
const usageData = await usageRes.json();
|
|
74
|
-
|
|
129
|
+
setFetchedUsage(usageData.usage || []);
|
|
75
130
|
} catch (e) {
|
|
76
131
|
console.error("Failed to fetch telemetry:", e);
|
|
77
132
|
}
|
|
@@ -83,26 +138,108 @@ export function TelemetryPage() {
|
|
|
83
138
|
// Refresh stats every 60 seconds (events come in real-time)
|
|
84
139
|
const interval = setInterval(fetchData, 60000);
|
|
85
140
|
return () => clearInterval(interval);
|
|
86
|
-
}, [filter]);
|
|
141
|
+
}, [filter, currentProjectId, authFetch]);
|
|
142
|
+
|
|
143
|
+
// Compute real-time stats from new events (not already counted in fetched stats)
|
|
144
|
+
const stats = useMemo(() => {
|
|
145
|
+
if (!fetchedStats) return null;
|
|
146
|
+
|
|
147
|
+
// Calculate deltas from real-time events not in fetched data
|
|
148
|
+
let deltaEvents = 0;
|
|
149
|
+
let deltaLlmCalls = 0;
|
|
150
|
+
let deltaToolCalls = 0;
|
|
151
|
+
let deltaErrors = 0;
|
|
152
|
+
let deltaInputTokens = 0;
|
|
153
|
+
let deltaOutputTokens = 0;
|
|
154
|
+
|
|
155
|
+
for (const event of realtimeEvents) {
|
|
156
|
+
if (!countedEventIdsRef.current.has(event.id)) {
|
|
157
|
+
deltaEvents++;
|
|
158
|
+
const eventStats = extractEventStats(event);
|
|
159
|
+
deltaLlmCalls += eventStats.llm_calls;
|
|
160
|
+
deltaToolCalls += eventStats.tool_calls;
|
|
161
|
+
deltaErrors += eventStats.errors;
|
|
162
|
+
deltaInputTokens += eventStats.input_tokens;
|
|
163
|
+
deltaOutputTokens += eventStats.output_tokens;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
total_events: fetchedStats.total_events + deltaEvents,
|
|
169
|
+
total_llm_calls: fetchedStats.total_llm_calls + deltaLlmCalls,
|
|
170
|
+
total_tool_calls: fetchedStats.total_tool_calls + deltaToolCalls,
|
|
171
|
+
total_errors: fetchedStats.total_errors + deltaErrors,
|
|
172
|
+
total_input_tokens: fetchedStats.total_input_tokens + deltaInputTokens,
|
|
173
|
+
total_output_tokens: fetchedStats.total_output_tokens + deltaOutputTokens,
|
|
174
|
+
};
|
|
175
|
+
}, [fetchedStats, realtimeEvents]);
|
|
176
|
+
|
|
177
|
+
// Compute real-time usage by agent
|
|
178
|
+
const usage = useMemo(() => {
|
|
179
|
+
// Start with a copy of fetched usage as a map
|
|
180
|
+
const usageMap = new Map<string, UsageByAgent>();
|
|
181
|
+
for (const u of fetchedUsage) {
|
|
182
|
+
usageMap.set(u.agent_id, { ...u });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Add deltas from real-time events
|
|
186
|
+
for (const event of realtimeEvents) {
|
|
187
|
+
if (!countedEventIdsRef.current.has(event.id)) {
|
|
188
|
+
const eventStats = extractEventStats(event);
|
|
189
|
+
const existing = usageMap.get(event.agent_id);
|
|
190
|
+
if (existing) {
|
|
191
|
+
existing.llm_calls += eventStats.llm_calls;
|
|
192
|
+
existing.tool_calls += eventStats.tool_calls;
|
|
193
|
+
existing.errors += eventStats.errors;
|
|
194
|
+
existing.input_tokens += eventStats.input_tokens;
|
|
195
|
+
existing.output_tokens += eventStats.output_tokens;
|
|
196
|
+
} else {
|
|
197
|
+
usageMap.set(event.agent_id, {
|
|
198
|
+
agent_id: event.agent_id,
|
|
199
|
+
llm_calls: eventStats.llm_calls,
|
|
200
|
+
tool_calls: eventStats.tool_calls,
|
|
201
|
+
errors: eventStats.errors,
|
|
202
|
+
input_tokens: eventStats.input_tokens,
|
|
203
|
+
output_tokens: eventStats.output_tokens,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return Array.from(usageMap.values());
|
|
210
|
+
}, [fetchedUsage, realtimeEvents]);
|
|
87
211
|
|
|
88
212
|
// Merge real-time events with historical, filtering and deduping
|
|
89
213
|
const allEvents = React.useMemo(() => {
|
|
90
214
|
// Apply filters to real-time events
|
|
91
215
|
let filtered = realtimeEvents;
|
|
216
|
+
|
|
217
|
+
// Filter by project (for real-time events)
|
|
218
|
+
if (currentProjectId !== null) {
|
|
219
|
+
filtered = filtered.filter(e => projectAgentIds.has(e.agent_id));
|
|
220
|
+
}
|
|
221
|
+
|
|
92
222
|
if (filter.agent_id) {
|
|
93
223
|
filtered = filtered.filter(e => e.agent_id === filter.agent_id);
|
|
94
224
|
}
|
|
95
|
-
|
|
96
|
-
|
|
225
|
+
// Filter out hidden categories
|
|
226
|
+
if (hiddenCategories.size > 0) {
|
|
227
|
+
filtered = filtered.filter(e => !hiddenCategories.has(e.category));
|
|
97
228
|
}
|
|
98
229
|
if (filter.level) {
|
|
99
230
|
filtered = filtered.filter(e => e.level === filter.level);
|
|
100
231
|
}
|
|
101
232
|
|
|
233
|
+
// Filter historical events too
|
|
234
|
+
let filteredHistorical = historicalEvents;
|
|
235
|
+
if (hiddenCategories.size > 0) {
|
|
236
|
+
filteredHistorical = filteredHistorical.filter(e => !hiddenCategories.has(e.category));
|
|
237
|
+
}
|
|
238
|
+
|
|
102
239
|
// Merge with historical, dedupe by ID
|
|
103
240
|
const seen = new Set(filtered.map(e => e.id));
|
|
104
241
|
const merged = [...filtered];
|
|
105
|
-
for (const evt of
|
|
242
|
+
for (const evt of filteredHistorical) {
|
|
106
243
|
if (!seen.has(evt.id)) {
|
|
107
244
|
merged.push(evt);
|
|
108
245
|
seen.add(evt.id);
|
|
@@ -113,7 +250,35 @@ export function TelemetryPage() {
|
|
|
113
250
|
merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
114
251
|
|
|
115
252
|
return merged.slice(0, 100);
|
|
116
|
-
}, [realtimeEvents, historicalEvents, filter]);
|
|
253
|
+
}, [realtimeEvents, historicalEvents, filter, hiddenCategories, currentProjectId, projectAgentIds]);
|
|
254
|
+
|
|
255
|
+
// Track new events for animation - mark events as "new" when they first appear
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
const newIds: string[] = [];
|
|
258
|
+
for (const event of realtimeEvents) {
|
|
259
|
+
if (!seenEventIdsRef.current.has(event.id)) {
|
|
260
|
+
seenEventIdsRef.current.add(event.id);
|
|
261
|
+
newIds.push(event.id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (newIds.length > 0) {
|
|
266
|
+
setNewEventIds(prev => {
|
|
267
|
+
const updated = new Set(prev);
|
|
268
|
+
newIds.forEach(id => updated.add(id));
|
|
269
|
+
return updated;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Remove "new" status after 5 seconds
|
|
273
|
+
setTimeout(() => {
|
|
274
|
+
setNewEventIds(prev => {
|
|
275
|
+
const updated = new Set(prev);
|
|
276
|
+
newIds.forEach(id => updated.delete(id));
|
|
277
|
+
return updated;
|
|
278
|
+
});
|
|
279
|
+
}, 5000);
|
|
280
|
+
}
|
|
281
|
+
}, [realtimeEvents]);
|
|
117
282
|
|
|
118
283
|
const getAgentName = (agentId: string) => {
|
|
119
284
|
const agent = agents.find(a => a.id === agentId);
|
|
@@ -142,23 +307,26 @@ export function TelemetryPage() {
|
|
|
142
307
|
TASK: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
|
|
143
308
|
MEMORY: "bg-cyan-500/20 text-cyan-400 border-cyan-500/30",
|
|
144
309
|
MCP: "bg-orange-500/20 text-orange-400 border-orange-500/30",
|
|
310
|
+
DATABASE: "bg-pink-500/20 text-pink-400 border-pink-500/30",
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const allCategories = ["LLM", "TOOL", "CHAT", "TASK", "MEMORY", "MCP", "SYSTEM", "DATABASE", "ERROR"];
|
|
314
|
+
|
|
315
|
+
const toggleCategory = (category: string) => {
|
|
316
|
+
setHiddenCategories(prev => {
|
|
317
|
+
const updated = new Set(prev);
|
|
318
|
+
if (updated.has(category)) {
|
|
319
|
+
updated.delete(category);
|
|
320
|
+
} else {
|
|
321
|
+
updated.add(category);
|
|
322
|
+
}
|
|
323
|
+
return updated;
|
|
324
|
+
});
|
|
145
325
|
};
|
|
146
326
|
|
|
147
327
|
const agentOptions = [
|
|
148
328
|
{ value: "", label: "All Agents" },
|
|
149
|
-
...
|
|
150
|
-
];
|
|
151
|
-
|
|
152
|
-
const categoryOptions = [
|
|
153
|
-
{ value: "", label: "All Categories" },
|
|
154
|
-
{ value: "LLM", label: "LLM" },
|
|
155
|
-
{ value: "TOOL", label: "Tool" },
|
|
156
|
-
{ value: "CHAT", label: "Chat" },
|
|
157
|
-
{ value: "TASK", label: "Task" },
|
|
158
|
-
{ value: "MEMORY", label: "Memory" },
|
|
159
|
-
{ value: "MCP", label: "MCP" },
|
|
160
|
-
{ value: "SYSTEM", label: "System" },
|
|
161
|
-
{ value: "ERROR", label: "Error" },
|
|
329
|
+
...filteredAgents.map(a => ({ value: a.id, label: a.name })),
|
|
162
330
|
];
|
|
163
331
|
|
|
164
332
|
const levelOptions = [
|
|
@@ -171,23 +339,27 @@ export function TelemetryPage() {
|
|
|
171
339
|
|
|
172
340
|
return (
|
|
173
341
|
<div className="flex-1 overflow-auto p-6">
|
|
174
|
-
<div
|
|
342
|
+
<div>
|
|
175
343
|
{/* Header */}
|
|
176
|
-
<div className="mb-6
|
|
177
|
-
<div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
344
|
+
<div className="mb-6">
|
|
345
|
+
<div className="flex items-center gap-3 mb-1">
|
|
346
|
+
{currentProject && (
|
|
347
|
+
<span
|
|
348
|
+
className="w-3 h-3 rounded-full"
|
|
349
|
+
style={{ backgroundColor: currentProject.color }}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
352
|
+
<h1 className="text-2xl font-semibold">
|
|
353
|
+
{currentProjectId === null
|
|
354
|
+
? "Telemetry"
|
|
355
|
+
: currentProjectId === "unassigned"
|
|
356
|
+
? "Telemetry - Unassigned"
|
|
357
|
+
: `Telemetry - ${currentProject?.name || ""}`}
|
|
358
|
+
</h1>
|
|
190
359
|
</div>
|
|
360
|
+
<p className="text-[#666]">
|
|
361
|
+
Monitor agent activity, token usage, and errors.
|
|
362
|
+
</p>
|
|
191
363
|
</div>
|
|
192
364
|
|
|
193
365
|
{/* Stats Cards */}
|
|
@@ -242,8 +414,8 @@ export function TelemetryPage() {
|
|
|
242
414
|
)}
|
|
243
415
|
|
|
244
416
|
{/* Filters */}
|
|
245
|
-
<div className="flex items-center gap-3 mb-4">
|
|
246
|
-
<div className="w-
|
|
417
|
+
<div className="flex flex-wrap items-center gap-3 mb-4">
|
|
418
|
+
<div className="w-44">
|
|
247
419
|
<Select
|
|
248
420
|
value={filter.agent_id}
|
|
249
421
|
options={agentOptions}
|
|
@@ -251,28 +423,42 @@ export function TelemetryPage() {
|
|
|
251
423
|
placeholder="All Agents"
|
|
252
424
|
/>
|
|
253
425
|
</div>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
426
|
+
{/* Category toggles */}
|
|
427
|
+
<div className="flex flex-wrap items-center gap-1.5 flex-1">
|
|
428
|
+
{allCategories.map((cat) => {
|
|
429
|
+
const isHidden = hiddenCategories.has(cat);
|
|
430
|
+
const colorClass = categoryColors[cat] || "bg-[#222] text-[#888] border-[#333]";
|
|
431
|
+
return (
|
|
432
|
+
<button
|
|
433
|
+
key={cat}
|
|
434
|
+
onClick={() => toggleCategory(cat)}
|
|
435
|
+
className={`px-2 py-0.5 rounded text-xs border transition-all ${
|
|
436
|
+
isHidden
|
|
437
|
+
? "bg-[#1a1a1a] text-[#555] border-[#333] opacity-50"
|
|
438
|
+
: colorClass
|
|
439
|
+
}`}
|
|
440
|
+
>
|
|
441
|
+
{cat}
|
|
442
|
+
</button>
|
|
443
|
+
);
|
|
444
|
+
})}
|
|
261
445
|
</div>
|
|
262
|
-
<div className="
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
446
|
+
<div className="flex items-center gap-2">
|
|
447
|
+
<div className="w-36">
|
|
448
|
+
<Select
|
|
449
|
+
value={filter.level}
|
|
450
|
+
options={levelOptions}
|
|
451
|
+
onChange={(value) => setFilter({ ...filter, level: value })}
|
|
452
|
+
placeholder="All Levels"
|
|
453
|
+
/>
|
|
454
|
+
</div>
|
|
455
|
+
<button
|
|
456
|
+
onClick={fetchData}
|
|
457
|
+
className="px-3 py-2 bg-[#1a1a1a] hover:bg-[#222] border border-[#333] rounded text-sm transition"
|
|
458
|
+
>
|
|
459
|
+
Refresh
|
|
460
|
+
</button>
|
|
269
461
|
</div>
|
|
270
|
-
<button
|
|
271
|
-
onClick={fetchData}
|
|
272
|
-
className="px-3 py-2 bg-[#1a1a1a] hover:bg-[#222] border border-[#333] rounded text-sm transition"
|
|
273
|
-
>
|
|
274
|
-
Refresh
|
|
275
|
-
</button>
|
|
276
462
|
</div>
|
|
277
463
|
|
|
278
464
|
{/* Events List */}
|
|
@@ -294,20 +480,22 @@ export function TelemetryPage() {
|
|
|
294
480
|
</div>
|
|
295
481
|
) : (
|
|
296
482
|
<div className="divide-y divide-[#1a1a1a]">
|
|
297
|
-
{allEvents.map((event
|
|
298
|
-
|
|
299
|
-
const isNew = index < 3 && realtimeEvents.some(e => e.id === event.id);
|
|
483
|
+
{allEvents.map((event) => {
|
|
484
|
+
const isNew = newEventIds.has(event.id);
|
|
300
485
|
|
|
301
486
|
return (
|
|
302
487
|
<div
|
|
303
488
|
key={event.id}
|
|
304
|
-
className={`p-3 hover:bg-[#0a0a0a]
|
|
305
|
-
isNew ? "bg-
|
|
489
|
+
className={`p-3 hover:bg-[#0a0a0a] cursor-pointer transition-all duration-500 ${
|
|
490
|
+
isNew ? "bg-green-500/5" : ""
|
|
306
491
|
}`}
|
|
492
|
+
style={{
|
|
493
|
+
animation: isNew ? "slideIn 0.3s ease-out" : undefined,
|
|
494
|
+
}}
|
|
307
495
|
onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
|
|
308
496
|
>
|
|
309
497
|
<div className="flex items-start gap-3">
|
|
310
|
-
<span className={`px-2 py-0.5 rounded text-xs border ${categoryColors[event.category] || "bg-[#222] text-[#888] border-[#333]"}`}>
|
|
498
|
+
<span className={`px-2 py-0.5 rounded text-xs border transition-colors duration-300 ${categoryColors[event.category] || "bg-[#222] text-[#888] border-[#333]"}`}>
|
|
311
499
|
{event.category}
|
|
312
500
|
</span>
|
|
313
501
|
<div className="flex-1 min-w-0">
|
|
@@ -319,9 +507,11 @@ export function TelemetryPage() {
|
|
|
319
507
|
{event.duration_ms && (
|
|
320
508
|
<span className="text-xs text-[#555]">{event.duration_ms}ms</span>
|
|
321
509
|
)}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
510
|
+
<span
|
|
511
|
+
className={`w-1.5 h-1.5 rounded-full bg-green-400 transition-opacity duration-1000 ${
|
|
512
|
+
isNew ? "opacity-100" : "opacity-0"
|
|
513
|
+
}`}
|
|
514
|
+
/>
|
|
325
515
|
</div>
|
|
326
516
|
<div className="text-xs text-[#555] mt-1">
|
|
327
517
|
{getAgentName(event.agent_id)} · {new Date(event.timestamp).toLocaleString()}
|