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.
- package/dist/App.jdzxkzm1.js +228 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/middleware.ts +42 -26
- package/src/crypto.ts +2 -2
- package/src/db-tests.ts +174 -0
- package/src/db.ts +302 -5
- package/src/integrations/agentdojo.ts +168 -42
- package/src/mcp-client.ts +15 -9
- package/src/mcp-platform.ts +160 -0
- package/src/openapi.ts +416 -21
- package/src/routes/api/agent-utils.ts +2 -2
- package/src/routes/api/api-keys.ts +95 -0
- package/src/routes/api/mcp.ts +2 -2
- package/src/routes/api/system.ts +10 -1
- package/src/routes/api/tests.ts +148 -0
- package/src/routes/api.ts +4 -0
- package/src/server.ts +2 -1
- package/src/test-runner.ts +598 -0
- package/src/web/App.tsx +23 -10
- package/src/web/components/agents/AgentPanel.tsx +4 -8
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/dashboard/Dashboard.tsx +2 -4
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/settings/SettingsPage.tsx +288 -5
- package/src/web/components/skills/SkillsPage.tsx +1 -1
- package/src/web/components/tasks/TasksPage.tsx +8 -3
- package/src/web/components/telemetry/TelemetryPage.tsx +2 -5
- package/src/web/components/tests/TestsPage.tsx +580 -0
- package/src/web/context/index.ts +1 -1
- package/src/web/types.ts +1 -1
- package/dist/App.9ph8javh.js +0 -228
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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 ||
|
|
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
|
-
{
|
|
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("");
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(() => {
|