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.
Files changed (41) hide show
  1. package/dist/App.hzbfeg94.js +217 -0
  2. package/dist/index.html +3 -1
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +570 -32
  9. package/src/routes/api.ts +913 -38
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +60 -8
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +30 -41
  14. package/src/web/components/agents/AgentPanel.tsx +751 -11
  15. package/src/web/components/agents/AgentsView.tsx +81 -9
  16. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  17. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  18. package/src/web/components/auth/LoginPage.tsx +91 -0
  19. package/src/web/components/auth/index.ts +2 -0
  20. package/src/web/components/common/Icons.tsx +48 -0
  21. package/src/web/components/common/Modal.tsx +1 -1
  22. package/src/web/components/dashboard/Dashboard.tsx +91 -31
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +145 -15
  25. package/src/web/components/layout/Sidebar.tsx +81 -43
  26. package/src/web/components/mcp/McpPage.tsx +261 -32
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  28. package/src/web/components/settings/SettingsPage.tsx +404 -18
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/TelemetryContext.tsx +98 -76
  34. package/src/web/context/index.ts +5 -0
  35. package/src/web/hooks/useAgents.ts +18 -6
  36. package/src/web/hooks/useOnboarding.ts +20 -4
  37. package/src/web/hooks/useProviders.ts +15 -5
  38. package/src/web/icon.png +0 -0
  39. package/src/web/styles.css +12 -0
  40. package/src/web/types.ts +6 -0
  41. 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 { connected, events: realtimeEvents } = useTelemetryContext();
25
- const [stats, setStats] = useState<TelemetryStats | null>(null);
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 [usage, setUsage] = useState<UsageByAgent[]>([]);
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
- const [agents, setAgents] = useState<Array<{ id: string; name: string }>>([]);
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 fetch("/api/agents");
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 statsRes = await fetch("/api/telemetry/stats");
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
- setStats(statsData.stats);
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("limit", "50");
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 fetch(`/api/telemetry/events?${params}`);
115
+ const eventsRes = await authFetch(`/api/telemetry/events?${params}`);
68
116
  const eventsData = await eventsRes.json();
69
- setHistoricalEvents(eventsData.events || []);
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 usageRes = await fetch("/api/telemetry/usage?group_by=agent");
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
- setUsage(usageData.usage || []);
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
- if (filter.category) {
96
- filtered = filtered.filter(e => e.category === filter.category);
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 historicalEvents) {
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
- ...agents.map(a => ({ value: a.id, label: a.name })),
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 className="max-w-6xl">
342
+ <div>
175
343
  {/* Header */}
176
- <div className="mb-6 flex items-center justify-between">
177
- <div>
178
- <h1 className="text-2xl font-semibold mb-1">Telemetry</h1>
179
- <p className="text-[#666]">
180
- Monitor agent activity, token usage, and errors.
181
- </p>
182
- </div>
183
- <div className="flex items-center gap-2">
184
- <span
185
- className={`w-2 h-2 rounded-full ${connected ? "bg-green-400" : "bg-red-400"}`}
186
- />
187
- <span className="text-xs text-[#666]">
188
- {connected ? "Live" : "Reconnecting..."}
189
- </span>
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-56">
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
- <div className="w-48">
255
- <Select
256
- value={filter.category}
257
- options={categoryOptions}
258
- onChange={(value) => setFilter({ ...filter, category: value })}
259
- placeholder="All Categories"
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="w-40">
263
- <Select
264
- value={filter.level}
265
- options={levelOptions}
266
- onChange={(value) => setFilter({ ...filter, level: value })}
267
- placeholder="All Levels"
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, index) => {
298
- // Check if this is a new real-time event (in first few positions and recent)
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] transition cursor-pointer ${
305
- isNew ? "bg-[#0f1a0f]" : ""
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
- {isNew && (
323
- <span className="text-xs text-green-400">new</span>
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()}