apteva 0.4.12 → 0.4.14

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.
@@ -3,6 +3,7 @@ import { Chat } from "@apteva/apteva-kit";
3
3
  import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon } from "../common/Icons";
4
4
  import { Select } from "../common/Select";
5
5
  import { useConfirm } from "../common/Modal";
6
+ import { useTelemetry } from "../../context";
6
7
  import { useAuth } from "../../context";
7
8
  import type { Agent, Provider, AgentFeatures, McpServer, SkillSummary, AgentMode, MultiAgentConfig } from "../../types";
8
9
  import { getMultiAgentConfig } from "../../types";
@@ -386,6 +387,7 @@ function TasksTab({ agent }: { agent: Agent }) {
386
387
  const [loading, setLoading] = useState(true);
387
388
  const [error, setError] = useState<string | null>(null);
388
389
  const [filter, setFilter] = useState<"all" | "pending" | "running" | "completed">("all");
390
+ const { events } = useTelemetry({ agent_id: agent.id, category: "task" });
389
391
 
390
392
  // Reset state when agent changes
391
393
  useEffect(() => {
@@ -413,17 +415,11 @@ function TasksTab({ agent }: { agent: Agent }) {
413
415
  }
414
416
  };
415
417
 
418
+ // Refetch when agent changes, filter changes, or task telemetry arrives
416
419
  useEffect(() => {
417
420
  setLoading(true);
418
421
  fetchTasks();
419
- }, [agent.id, agent.status, filter]);
420
-
421
- // Auto-refresh every 5 seconds when agent is running
422
- useEffect(() => {
423
- if (agent.status !== "running") return;
424
- const interval = setInterval(fetchTasks, 5000);
425
- return () => clearInterval(interval);
426
- }, [agent.id, agent.status, filter]);
422
+ }, [agent.id, agent.status, filter, events.length]);
427
423
 
428
424
  const getStatusColor = (status: Task["status"]) => {
429
425
  switch (status) {
@@ -174,6 +174,14 @@ export function FolderIcon({ className = "w-4 h-4" }: IconProps) {
174
174
  );
175
175
  }
176
176
 
177
+ export function TestsIcon({ className = "w-4 h-4" }: IconProps) {
178
+ return (
179
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
180
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
181
+ </svg>
182
+ );
183
+ }
184
+
177
185
  export function PlusIcon({ className = "w-4 h-4" }: IconProps) {
178
186
  return (
179
187
  <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -22,7 +22,7 @@ export function Dashboard({
22
22
  }: DashboardProps) {
23
23
  const { authFetch } = useAuth();
24
24
  const { currentProjectId } = useProjects();
25
- const { events: realtimeEvents } = useTelemetryContext();
25
+ const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
26
26
  const [stats, setStats] = useState<DashboardStats | null>(null);
27
27
  const [recentTasks, setRecentTasks] = useState<Task[]>([]);
28
28
  const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
@@ -72,9 +72,7 @@ export function Dashboard({
72
72
 
73
73
  useEffect(() => {
74
74
  fetchDashboardData();
75
- const interval = setInterval(fetchDashboardData, 10000);
76
- return () => clearInterval(interval);
77
- }, [fetchDashboardData]);
75
+ }, [fetchDashboardData, statusChangeCounter]);
78
76
 
79
77
  // Filter tasks by project agents
80
78
  const filteredTasks = useMemo(() => {
@@ -15,4 +15,5 @@ export { Dashboard } from "./dashboard";
15
15
  export { TasksPage } from "./tasks";
16
16
  export { McpPage } from "./mcp";
17
17
  export { SkillsPage } from "./skills/SkillsPage";
18
+ export { TestsPage } from "./tests/TestsPage";
18
19
  export { TelemetryPage } from "./telemetry/TelemetryPage";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { DashboardIcon, AgentsIcon, TasksIcon, McpIcon, SkillsIcon, TelemetryIcon, ApiIcon, SettingsIcon, CloseIcon } from "../common/Icons";
2
+ import { DashboardIcon, AgentsIcon, TasksIcon, McpIcon, SkillsIcon, TestsIcon, TelemetryIcon, ApiIcon, SettingsIcon, CloseIcon } from "../common/Icons";
3
3
  import type { Route } from "../../types";
4
4
 
5
5
  interface SidebarProps {
@@ -82,6 +82,12 @@ export function Sidebar({ route, agentCount, taskCount, onNavigate, isOpen, onCl
82
82
  active={route === "skills"}
83
83
  onClick={() => handleNavigate("skills")}
84
84
  />
85
+ <NavButton
86
+ icon={<TestsIcon />}
87
+ label="Tests"
88
+ active={route === "tests"}
89
+ onClick={() => handleNavigate("tests")}
90
+ />
85
91
  <NavButton
86
92
  icon={<TelemetryIcon />}
87
93
  label="Telemetry"
@@ -5,7 +5,7 @@ import { Select } from "../common/Select";
5
5
  import { useProjects, useAuth, type Project } from "../../context";
6
6
  import type { Provider } from "../../types";
7
7
 
8
- type SettingsTab = "providers" | "projects" | "account" | "updates" | "data";
8
+ type SettingsTab = "providers" | "projects" | "api-keys" | "account" | "updates" | "data";
9
9
 
10
10
  export function SettingsPage() {
11
11
  const { projectsEnabled } = useProjects();
@@ -14,6 +14,7 @@ export function SettingsPage() {
14
14
  const tabs: { key: SettingsTab; label: string }[] = [
15
15
  { key: "providers", label: "Providers" },
16
16
  ...(projectsEnabled ? [{ key: "projects" as SettingsTab, label: "Projects" }] : []),
17
+ { key: "api-keys", label: "API Keys" },
17
18
  { key: "account", label: "Account" },
18
19
  { key: "updates", label: "Updates" },
19
20
  { key: "data", label: "Data" },
@@ -59,6 +60,7 @@ export function SettingsPage() {
59
60
  <div className="flex-1 overflow-auto p-4 md:p-6">
60
61
  {activeTab === "providers" && <ProvidersSettings />}
61
62
  {activeTab === "projects" && projectsEnabled && <ProjectsSettings />}
63
+ {activeTab === "api-keys" && <ApiKeysSettings />}
62
64
  {activeTab === "account" && <AccountSettings />}
63
65
  {activeTab === "updates" && <UpdatesSettings />}
64
66
  {activeTab === "data" && <DataSettings />}
@@ -974,6 +976,8 @@ function IntegrationKeyCard({
974
976
  const [keys, setKeys] = useState<IntegrationKey[]>([]);
975
977
  const [selectedProjectId, setSelectedProjectId] = useState<string>("");
976
978
  const [expanded, setExpanded] = useState(false);
979
+ const [localError, setLocalError] = useState<string | null>(null);
980
+ const [localSaving, setLocalSaving] = useState(false);
977
981
  const { confirm, ConfirmDialog } = useConfirm();
978
982
 
979
983
  // Fetch all keys for this provider
@@ -993,9 +997,19 @@ function IntegrationKeyCard({
993
997
  }
994
998
  }, [provider.id, projectsEnabled]);
995
999
 
1000
+ // Clear local error when starting to edit
1001
+ useEffect(() => {
1002
+ if (isEditing) {
1003
+ setLocalError(null);
1004
+ }
1005
+ }, [isEditing]);
1006
+
996
1007
  const handleSaveWithProject = async () => {
997
1008
  if (!apiKey) return;
998
1009
 
1010
+ setLocalSaving(true);
1011
+ setLocalError(null);
1012
+
999
1013
  try {
1000
1014
  const res = await authFetch(`/api/keys/${provider.id}`, {
1001
1015
  method: "POST",
@@ -1006,16 +1020,22 @@ function IntegrationKeyCard({
1006
1020
  }),
1007
1021
  });
1008
1022
 
1023
+ const data = await res.json();
1024
+
1009
1025
  if (res.ok) {
1010
1026
  onApiKeyChange("");
1011
1027
  setSelectedProjectId("");
1012
1028
  onCancelEdit();
1013
1029
  fetchKeys();
1014
1030
  onRefresh();
1031
+ } else {
1032
+ setLocalError(data.error || "Failed to save key");
1015
1033
  }
1016
1034
  } catch (e) {
1017
1035
  console.error("Failed to save key:", e);
1036
+ setLocalError("Failed to save key");
1018
1037
  }
1038
+ setLocalSaving(false);
1019
1039
  };
1020
1040
 
1021
1041
  const handleDeleteKey = async (keyId: string, keyName: string | null) => {
@@ -1237,14 +1257,14 @@ function IntegrationKeyCard({
1237
1257
  ]}
1238
1258
  />
1239
1259
 
1240
- {error && <p className="text-red-400 text-sm">{error}</p>}
1241
- {success && <p className="text-green-400 text-sm">{success}</p>}
1260
+ {localError && <p className="text-red-400 text-sm">{localError}</p>}
1242
1261
 
1243
1262
  <div className="flex gap-2">
1244
1263
  <button
1245
1264
  onClick={() => {
1246
1265
  onCancelEdit();
1247
1266
  setSelectedProjectId("");
1267
+ setLocalError(null);
1248
1268
  }}
1249
1269
  className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
1250
1270
  >
@@ -1252,10 +1272,10 @@ function IntegrationKeyCard({
1252
1272
  </button>
1253
1273
  <button
1254
1274
  onClick={handleSaveWithProject}
1255
- disabled={!apiKey || saving}
1275
+ disabled={!apiKey || localSaving}
1256
1276
  className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
1257
1277
  >
1258
- {saving ? "Saving..." : "Save"}
1278
+ {localSaving ? "Saving..." : "Save"}
1259
1279
  </button>
1260
1280
  </div>
1261
1281
  </div>
@@ -1283,6 +1303,269 @@ function IntegrationKeyCard({
1283
1303
  );
1284
1304
  }
1285
1305
 
1306
+ interface ApiKeyItem {
1307
+ id: string;
1308
+ name: string;
1309
+ prefix: string;
1310
+ is_active: boolean;
1311
+ expires_at: string | null;
1312
+ last_used_at: string | null;
1313
+ created_at: string;
1314
+ }
1315
+
1316
+ function ApiKeysSettings() {
1317
+ const { authFetch } = useAuth();
1318
+ const [keys, setKeys] = useState<ApiKeyItem[]>([]);
1319
+ const [showCreate, setShowCreate] = useState(false);
1320
+ const [name, setName] = useState("");
1321
+ const [expiresInDays, setExpiresInDays] = useState<string>("90");
1322
+ const [creating, setCreating] = useState(false);
1323
+ const [error, setError] = useState<string | null>(null);
1324
+ const [newKey, setNewKey] = useState<string | null>(null);
1325
+ const [copied, setCopied] = useState(false);
1326
+ const { confirm, ConfirmDialog } = useConfirm();
1327
+
1328
+ const fetchKeys = async () => {
1329
+ try {
1330
+ const res = await authFetch("/api/keys/personal");
1331
+ const data = await res.json();
1332
+ setKeys(data.keys || []);
1333
+ } catch {
1334
+ // ignore
1335
+ }
1336
+ };
1337
+
1338
+ useEffect(() => {
1339
+ fetchKeys();
1340
+ }, []);
1341
+
1342
+ const handleCreate = async () => {
1343
+ if (!name.trim()) {
1344
+ setError("Name is required");
1345
+ return;
1346
+ }
1347
+ setCreating(true);
1348
+ setError(null);
1349
+
1350
+ try {
1351
+ const res = await authFetch("/api/keys/personal", {
1352
+ method: "POST",
1353
+ headers: { "Content-Type": "application/json" },
1354
+ body: JSON.stringify({
1355
+ name: name.trim(),
1356
+ expires_in_days: expiresInDays ? parseInt(expiresInDays) : null,
1357
+ }),
1358
+ });
1359
+
1360
+ const data = await res.json();
1361
+ if (!res.ok) {
1362
+ setError(data.error || "Failed to create key");
1363
+ } else {
1364
+ setNewKey(data.key);
1365
+ setName("");
1366
+ setExpiresInDays("90");
1367
+ fetchKeys();
1368
+ }
1369
+ } catch {
1370
+ setError("Failed to create key");
1371
+ }
1372
+ setCreating(false);
1373
+ };
1374
+
1375
+ const handleDelete = async (id: string, keyName: string) => {
1376
+ const confirmed = await confirm(`Delete API key "${keyName}"? This cannot be undone.`, { confirmText: "Delete", title: "Delete API Key" });
1377
+ if (!confirmed) return;
1378
+
1379
+ try {
1380
+ await authFetch(`/api/keys/personal/${id}`, { method: "DELETE" });
1381
+ fetchKeys();
1382
+ } catch {
1383
+ // ignore
1384
+ }
1385
+ };
1386
+
1387
+ const copyKey = () => {
1388
+ if (newKey) {
1389
+ navigator.clipboard.writeText(newKey);
1390
+ setCopied(true);
1391
+ setTimeout(() => setCopied(false), 2000);
1392
+ }
1393
+ };
1394
+
1395
+ const formatDate = (dateStr: string | null) => {
1396
+ if (!dateStr) return "Never";
1397
+ const d = new Date(dateStr);
1398
+ return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
1399
+ };
1400
+
1401
+ const isExpired = (expiresAt: string | null) => {
1402
+ if (!expiresAt) return false;
1403
+ return new Date(expiresAt) < new Date();
1404
+ };
1405
+
1406
+ return (
1407
+ <>
1408
+ {ConfirmDialog}
1409
+ <div className="max-w-4xl w-full">
1410
+ <div className="mb-6 flex items-center justify-between gap-4">
1411
+ <div>
1412
+ <h1 className="text-2xl font-semibold mb-1">API Keys</h1>
1413
+ <p className="text-[#666]">
1414
+ Create personal API keys for programmatic access. Use them with the <code className="text-[#888] bg-[#1a1a1a] px-1 rounded text-xs">X-API-Key</code> header.
1415
+ </p>
1416
+ </div>
1417
+ {!showCreate && !newKey && (
1418
+ <button
1419
+ onClick={() => { setShowCreate(true); setError(null); }}
1420
+ className="flex items-center gap-2 bg-[#f97316] hover:bg-[#fb923c] text-black px-4 py-2 rounded font-medium transition flex-shrink-0"
1421
+ >
1422
+ <PlusIcon className="w-4 h-4" />
1423
+ New Key
1424
+ </button>
1425
+ )}
1426
+ </div>
1427
+
1428
+ {/* Newly created key - show once */}
1429
+ {newKey && (
1430
+ <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 mb-6">
1431
+ <div className="flex items-center gap-2 text-green-400 mb-2">
1432
+ <CheckIcon className="w-5 h-5" />
1433
+ <span className="font-medium">API key created</span>
1434
+ </div>
1435
+ <p className="text-sm text-[#888] mb-3">
1436
+ Copy this key now. You won't be able to see it again.
1437
+ </p>
1438
+ <div className="flex items-center gap-2">
1439
+ <code className="flex-1 bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#e0e0e0] break-all select-all">
1440
+ {newKey}
1441
+ </code>
1442
+ <button
1443
+ onClick={copyKey}
1444
+ className="px-3 py-2 bg-[#1a1a1a] hover:bg-[#222] rounded text-sm flex-shrink-0"
1445
+ >
1446
+ {copied ? "Copied!" : "Copy"}
1447
+ </button>
1448
+ </div>
1449
+ <button
1450
+ onClick={() => { setNewKey(null); setShowCreate(false); }}
1451
+ className="mt-3 text-sm text-[#666] hover:text-[#888]"
1452
+ >
1453
+ Done
1454
+ </button>
1455
+ </div>
1456
+ )}
1457
+
1458
+ {/* Create Form */}
1459
+ {showCreate && !newKey && (
1460
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 mb-6">
1461
+ <h3 className="font-medium mb-4">Create new API key</h3>
1462
+ <div className="space-y-4 max-w-md">
1463
+ <div>
1464
+ <label className="block text-sm text-[#666] mb-1">Name</label>
1465
+ <input
1466
+ type="text"
1467
+ value={name}
1468
+ onChange={e => setName(e.target.value)}
1469
+ placeholder="e.g. CI Pipeline, My Script"
1470
+ autoFocus
1471
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1472
+ />
1473
+ </div>
1474
+ <div>
1475
+ <label className="block text-sm text-[#666] mb-1">Expiration</label>
1476
+ <select
1477
+ value={expiresInDays}
1478
+ onChange={e => setExpiresInDays(e.target.value)}
1479
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1480
+ >
1481
+ <option value="30">30 days</option>
1482
+ <option value="90">90 days</option>
1483
+ <option value="180">180 days</option>
1484
+ <option value="365">1 year</option>
1485
+ <option value="">No expiration</option>
1486
+ </select>
1487
+ </div>
1488
+
1489
+ {error && <p className="text-red-400 text-sm">{error}</p>}
1490
+
1491
+ <div className="flex gap-2">
1492
+ <button
1493
+ onClick={() => { setShowCreate(false); setError(null); setName(""); }}
1494
+ className="flex-1 px-3 py-2 border border-[#333] rounded text-sm hover:border-[#666]"
1495
+ >
1496
+ Cancel
1497
+ </button>
1498
+ <button
1499
+ onClick={handleCreate}
1500
+ disabled={creating || !name.trim()}
1501
+ className="flex-1 px-3 py-2 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
1502
+ >
1503
+ {creating ? "Creating..." : "Create Key"}
1504
+ </button>
1505
+ </div>
1506
+ </div>
1507
+ </div>
1508
+ )}
1509
+
1510
+ {/* Keys List */}
1511
+ {keys.length === 0 ? (
1512
+ <div className="text-center py-12 text-[#666]">
1513
+ <p className="text-lg mb-2">No API keys yet</p>
1514
+ <p className="text-sm">Create an API key to access apteva programmatically.</p>
1515
+ </div>
1516
+ ) : (
1517
+ <div className="space-y-3">
1518
+ {keys.map(key => (
1519
+ <div
1520
+ key={key.id}
1521
+ className={`bg-[#111] border rounded-lg p-4 flex items-center gap-4 ${
1522
+ !key.is_active || isExpired(key.expires_at) ? "border-[#1a1a1a] opacity-60" : "border-[#1a1a1a]"
1523
+ }`}
1524
+ >
1525
+ <div className="flex-1 min-w-0">
1526
+ <div className="flex items-center gap-2 mb-1">
1527
+ <h3 className="font-medium">{key.name}</h3>
1528
+ {!key.is_active && (
1529
+ <span className="text-xs text-red-400 bg-red-500/10 px-2 py-0.5 rounded">Revoked</span>
1530
+ )}
1531
+ {key.is_active && isExpired(key.expires_at) && (
1532
+ <span className="text-xs text-yellow-400 bg-yellow-500/10 px-2 py-0.5 rounded">Expired</span>
1533
+ )}
1534
+ </div>
1535
+ <div className="flex items-center gap-3 text-sm text-[#666]">
1536
+ <code className="font-mono text-xs bg-[#0a0a0a] px-2 py-0.5 rounded">{key.prefix}...</code>
1537
+ <span>Created {formatDate(key.created_at)}</span>
1538
+ {key.expires_at && <span>Expires {formatDate(key.expires_at)}</span>}
1539
+ {key.last_used_at && <span>Last used {formatDate(key.last_used_at)}</span>}
1540
+ </div>
1541
+ </div>
1542
+ {key.is_active && (
1543
+ <button
1544
+ onClick={() => handleDelete(key.id, key.name)}
1545
+ className="text-sm text-red-400 hover:text-red-300 px-2 py-1 flex-shrink-0"
1546
+ >
1547
+ Delete
1548
+ </button>
1549
+ )}
1550
+ </div>
1551
+ ))}
1552
+ </div>
1553
+ )}
1554
+
1555
+ {/* Usage Info */}
1556
+ {keys.length > 0 && (
1557
+ <div className="mt-6 bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
1558
+ <h3 className="font-medium mb-2 text-sm">Usage</h3>
1559
+ <code className="block bg-[#0a0a0a] px-3 py-2 rounded font-mono text-xs text-[#888]">
1560
+ curl -H "X-API-Key: apt_..." http://localhost:4280/api/agents
1561
+ </code>
1562
+ </div>
1563
+ )}
1564
+ </div>
1565
+ </>
1566
+ );
1567
+ }
1568
+
1286
1569
  function AccountSettings() {
1287
1570
  const { authFetch, user } = useAuth();
1288
1571
  const [currentPassword, setCurrentPassword] = useState("");
@@ -325,7 +325,7 @@ export function SkillsPage() {
325
325
  : "text-[#666] hover:text-[#888]"
326
326
  }`}
327
327
  >
328
- Installed ({skills.length})
328
+ Installed ({filteredSkills.length})
329
329
  </button>
330
330
  <button
331
331
  onClick={() => setActiveTab("github")}
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from "react";
2
2
  import { TasksIcon, CloseIcon } from "../common/Icons";
3
- import { useAuth } from "../../context";
3
+ import { useAuth, useProjects } from "../../context";
4
4
  import { useTelemetry } from "../../context/TelemetryContext";
5
5
  import type { Task, TaskTrajectoryStep, ToolUseBlock, ToolResultBlock } from "../../types";
6
6
 
@@ -10,6 +10,7 @@ interface TasksPageProps {
10
10
 
11
11
  export function TasksPage({ onSelectAgent }: TasksPageProps) {
12
12
  const { authFetch } = useAuth();
13
+ const { currentProjectId } = useProjects();
13
14
  const [tasks, setTasks] = useState<Task[]>([]);
14
15
  const [loading, setLoading] = useState(true);
15
16
  const [filter, setFilter] = useState<string>("all");
@@ -22,7 +23,11 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
22
23
 
23
24
  const fetchTasks = useCallback(async () => {
24
25
  try {
25
- const res = await authFetch(`/api/tasks?status=${filter}`);
26
+ let url = `/api/tasks?status=${filter}`;
27
+ if (currentProjectId !== null) {
28
+ url += `&project_id=${encodeURIComponent(currentProjectId)}`;
29
+ }
30
+ const res = await authFetch(url);
26
31
  const data = await res.json();
27
32
  setTasks(data.tasks || []);
28
33
  } catch (e) {
@@ -30,7 +35,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
30
35
  } finally {
31
36
  setLoading(false);
32
37
  }
33
- }, [authFetch, filter]);
38
+ }, [authFetch, filter, currentProjectId]);
34
39
 
35
40
  // Initial fetch
36
41
  useEffect(() => {
@@ -44,7 +44,7 @@ function extractEventStats(event: TelemetryEvent): {
44
44
  }
45
45
 
46
46
  export function TelemetryPage() {
47
- const { events: realtimeEvents } = useTelemetryContext();
47
+ const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
48
48
  const { currentProjectId, currentProject } = useProjects();
49
49
  const { authFetch } = useAuth();
50
50
  const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
@@ -135,10 +135,7 @@ export function TelemetryPage() {
135
135
 
136
136
  useEffect(() => {
137
137
  fetchData();
138
- // Refresh stats every 60 seconds (events come in real-time)
139
- const interval = setInterval(fetchData, 60000);
140
- return () => clearInterval(interval);
141
- }, [filter, currentProjectId, authFetch]);
138
+ }, [filter, currentProjectId, authFetch, statusChangeCounter]);
142
139
 
143
140
  // Compute real-time stats from new events (not already counted in fetched stats)
144
141
  const stats = useMemo(() => {