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,8 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { CheckIcon } from "../common/Icons";
|
|
2
|
+
import { CheckIcon, CloseIcon, PlusIcon } from "../common/Icons";
|
|
3
|
+
import { Modal } from "../common/Modal";
|
|
4
|
+
import { useProjects, useAuth, type Project } from "../../context";
|
|
3
5
|
import type { Provider } from "../../types";
|
|
4
6
|
|
|
5
|
-
type SettingsTab = "providers" | "updates";
|
|
7
|
+
type SettingsTab = "providers" | "projects" | "updates" | "data";
|
|
6
8
|
|
|
7
9
|
export function SettingsPage() {
|
|
8
10
|
const [activeTab, setActiveTab] = useState<SettingsTab>("providers");
|
|
@@ -18,18 +20,30 @@ export function SettingsPage() {
|
|
|
18
20
|
active={activeTab === "providers"}
|
|
19
21
|
onClick={() => setActiveTab("providers")}
|
|
20
22
|
/>
|
|
23
|
+
<SettingsNavItem
|
|
24
|
+
label="Projects"
|
|
25
|
+
active={activeTab === "projects"}
|
|
26
|
+
onClick={() => setActiveTab("projects")}
|
|
27
|
+
/>
|
|
21
28
|
<SettingsNavItem
|
|
22
29
|
label="Updates"
|
|
23
30
|
active={activeTab === "updates"}
|
|
24
31
|
onClick={() => setActiveTab("updates")}
|
|
25
32
|
/>
|
|
33
|
+
<SettingsNavItem
|
|
34
|
+
label="Data"
|
|
35
|
+
active={activeTab === "data"}
|
|
36
|
+
onClick={() => setActiveTab("data")}
|
|
37
|
+
/>
|
|
26
38
|
</nav>
|
|
27
39
|
</div>
|
|
28
40
|
|
|
29
41
|
{/* Settings Content */}
|
|
30
42
|
<div className="flex-1 overflow-auto p-6">
|
|
31
43
|
{activeTab === "providers" && <ProvidersSettings />}
|
|
44
|
+
{activeTab === "projects" && <ProjectsSettings />}
|
|
32
45
|
{activeTab === "updates" && <UpdatesSettings />}
|
|
46
|
+
{activeTab === "data" && <DataSettings />}
|
|
33
47
|
</div>
|
|
34
48
|
</div>
|
|
35
49
|
);
|
|
@@ -59,6 +73,7 @@ function SettingsNavItem({
|
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
function ProvidersSettings() {
|
|
76
|
+
const { authFetch } = useAuth();
|
|
62
77
|
const [providers, setProviders] = useState<Provider[]>([]);
|
|
63
78
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
64
79
|
const [apiKey, setApiKey] = useState("");
|
|
@@ -68,7 +83,7 @@ function ProvidersSettings() {
|
|
|
68
83
|
const [success, setSuccess] = useState<string | null>(null);
|
|
69
84
|
|
|
70
85
|
const fetchProviders = async () => {
|
|
71
|
-
const res = await
|
|
86
|
+
const res = await authFetch("/api/providers");
|
|
72
87
|
const data = await res.json();
|
|
73
88
|
setProviders(data.providers || []);
|
|
74
89
|
};
|
|
@@ -85,7 +100,7 @@ function ProvidersSettings() {
|
|
|
85
100
|
|
|
86
101
|
try {
|
|
87
102
|
setTesting(true);
|
|
88
|
-
const testRes = await
|
|
103
|
+
const testRes = await authFetch(`/api/keys/${selectedProvider}/test`, {
|
|
89
104
|
method: "POST",
|
|
90
105
|
headers: { "Content-Type": "application/json" },
|
|
91
106
|
body: JSON.stringify({ key: apiKey }),
|
|
@@ -99,7 +114,7 @@ function ProvidersSettings() {
|
|
|
99
114
|
return;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
const saveRes = await
|
|
117
|
+
const saveRes = await authFetch(`/api/keys/${selectedProvider}`, {
|
|
103
118
|
method: "POST",
|
|
104
119
|
headers: { "Content-Type": "application/json" },
|
|
105
120
|
body: JSON.stringify({ key: apiKey }),
|
|
@@ -122,7 +137,7 @@ function ProvidersSettings() {
|
|
|
122
137
|
|
|
123
138
|
const deleteKey = async (providerId: string) => {
|
|
124
139
|
if (!confirm("Are you sure you want to remove this API key?")) return;
|
|
125
|
-
await
|
|
140
|
+
await authFetch(`/api/keys/${providerId}`, { method: "DELETE" });
|
|
126
141
|
fetchProviders();
|
|
127
142
|
};
|
|
128
143
|
|
|
@@ -132,7 +147,7 @@ function ProvidersSettings() {
|
|
|
132
147
|
const intConfiguredCount = integrations.filter(p => p.hasKey).length;
|
|
133
148
|
|
|
134
149
|
return (
|
|
135
|
-
<div className="
|
|
150
|
+
<div className="space-y-10">
|
|
136
151
|
{/* AI Providers Section */}
|
|
137
152
|
<div>
|
|
138
153
|
<div className="mb-6">
|
|
@@ -142,7 +157,7 @@ function ProvidersSettings() {
|
|
|
142
157
|
</p>
|
|
143
158
|
</div>
|
|
144
159
|
|
|
145
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
160
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
146
161
|
{llmProviders.map(provider => (
|
|
147
162
|
<ProviderKeyCard
|
|
148
163
|
key={provider.id}
|
|
@@ -180,7 +195,7 @@ function ProvidersSettings() {
|
|
|
180
195
|
</p>
|
|
181
196
|
</div>
|
|
182
197
|
|
|
183
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
198
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
184
199
|
{integrations.map(provider => (
|
|
185
200
|
<IntegrationKeyCard
|
|
186
201
|
key={provider.id}
|
|
@@ -212,6 +227,225 @@ function ProvidersSettings() {
|
|
|
212
227
|
);
|
|
213
228
|
}
|
|
214
229
|
|
|
230
|
+
const DEFAULT_PROJECT_COLORS = [
|
|
231
|
+
"#f97316", // orange
|
|
232
|
+
"#6366f1", // indigo
|
|
233
|
+
"#22c55e", // green
|
|
234
|
+
"#ef4444", // red
|
|
235
|
+
"#3b82f6", // blue
|
|
236
|
+
"#a855f7", // purple
|
|
237
|
+
"#14b8a6", // teal
|
|
238
|
+
"#f59e0b", // amber
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
function ProjectsSettings() {
|
|
242
|
+
const { projects, createProject, updateProject, deleteProject } = useProjects();
|
|
243
|
+
const [showModal, setShowModal] = useState(false);
|
|
244
|
+
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
245
|
+
|
|
246
|
+
const handleDelete = async (id: string) => {
|
|
247
|
+
if (!confirm("Are you sure you want to delete this project? Agents in this project will become unassigned.")) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await deleteProject(id);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const openCreate = () => {
|
|
254
|
+
setEditingProject(null);
|
|
255
|
+
setShowModal(true);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const openEdit = (project: Project) => {
|
|
259
|
+
setEditingProject(project);
|
|
260
|
+
setShowModal(true);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const closeModal = () => {
|
|
264
|
+
setShowModal(false);
|
|
265
|
+
setEditingProject(null);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div className="max-w-4xl w-full">
|
|
270
|
+
<div className="mb-6 flex items-center justify-between gap-4">
|
|
271
|
+
<div>
|
|
272
|
+
<h1 className="text-2xl font-semibold mb-1">Projects</h1>
|
|
273
|
+
<p className="text-[#666]">
|
|
274
|
+
Organize agents into projects for better management.
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
<button
|
|
278
|
+
onClick={openCreate}
|
|
279
|
+
className="flex items-center gap-2 bg-[#f97316] hover:bg-[#fb923c] text-black px-4 py-2 rounded font-medium transition flex-shrink-0"
|
|
280
|
+
>
|
|
281
|
+
<PlusIcon className="w-4 h-4" />
|
|
282
|
+
New Project
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{/* Project List */}
|
|
287
|
+
{projects.length === 0 ? (
|
|
288
|
+
<div className="text-center py-12 text-[#666]">
|
|
289
|
+
<p className="text-lg mb-2">No projects yet</p>
|
|
290
|
+
<p className="text-sm">Create a project to organize your agents.</p>
|
|
291
|
+
</div>
|
|
292
|
+
) : (
|
|
293
|
+
<div className="space-y-3">
|
|
294
|
+
{projects.map(project => (
|
|
295
|
+
<div
|
|
296
|
+
key={project.id}
|
|
297
|
+
className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 flex items-center gap-4"
|
|
298
|
+
>
|
|
299
|
+
<div
|
|
300
|
+
className="w-4 h-4 rounded-full flex-shrink-0"
|
|
301
|
+
style={{ backgroundColor: project.color }}
|
|
302
|
+
/>
|
|
303
|
+
<div className="flex-1 min-w-0">
|
|
304
|
+
<h3 className="font-medium">{project.name}</h3>
|
|
305
|
+
{project.description && (
|
|
306
|
+
<p className="text-sm text-[#666] truncate">{project.description}</p>
|
|
307
|
+
)}
|
|
308
|
+
<p className="text-xs text-[#666] mt-1">
|
|
309
|
+
{project.agentCount} agent{project.agentCount !== 1 ? "s" : ""}
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<button
|
|
314
|
+
onClick={() => openEdit(project)}
|
|
315
|
+
className="text-sm text-[#888] hover:text-[#e0e0e0] px-2 py-1"
|
|
316
|
+
>
|
|
317
|
+
Edit
|
|
318
|
+
</button>
|
|
319
|
+
<button
|
|
320
|
+
onClick={() => handleDelete(project.id)}
|
|
321
|
+
className="text-sm text-red-400 hover:text-red-300 px-2 py-1"
|
|
322
|
+
>
|
|
323
|
+
Delete
|
|
324
|
+
</button>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{/* Project Modal */}
|
|
332
|
+
{showModal && (
|
|
333
|
+
<ProjectModal
|
|
334
|
+
project={editingProject}
|
|
335
|
+
onSave={async (data) => {
|
|
336
|
+
if (editingProject) {
|
|
337
|
+
const result = await updateProject(editingProject.id, data);
|
|
338
|
+
if (result) closeModal();
|
|
339
|
+
return !!result;
|
|
340
|
+
} else {
|
|
341
|
+
const result = await createProject(data);
|
|
342
|
+
if (result) closeModal();
|
|
343
|
+
return !!result;
|
|
344
|
+
}
|
|
345
|
+
}}
|
|
346
|
+
onClose={closeModal}
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
interface ProjectModalProps {
|
|
354
|
+
project: Project | null;
|
|
355
|
+
onSave: (data: { name: string; description?: string; color: string }) => Promise<boolean>;
|
|
356
|
+
onClose: () => void;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ProjectModal({ project, onSave, onClose }: ProjectModalProps) {
|
|
360
|
+
const [name, setName] = useState(project?.name || "");
|
|
361
|
+
const [description, setDescription] = useState(project?.description || "");
|
|
362
|
+
const [color, setColor] = useState(
|
|
363
|
+
project?.color || DEFAULT_PROJECT_COLORS[Math.floor(Math.random() * DEFAULT_PROJECT_COLORS.length)]
|
|
364
|
+
);
|
|
365
|
+
const [saving, setSaving] = useState(false);
|
|
366
|
+
const [error, setError] = useState<string | null>(null);
|
|
367
|
+
|
|
368
|
+
const handleSubmit = async () => {
|
|
369
|
+
if (!name.trim()) {
|
|
370
|
+
setError("Name is required");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
setSaving(true);
|
|
374
|
+
setError(null);
|
|
375
|
+
const success = await onSave({ name, description: description || undefined, color });
|
|
376
|
+
setSaving(false);
|
|
377
|
+
if (!success) {
|
|
378
|
+
setError(project ? "Failed to update project" : "Failed to create project");
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<Modal onClose={onClose}>
|
|
384
|
+
<h2 className="text-xl font-semibold mb-6">{project ? "Edit Project" : "Create New Project"}</h2>
|
|
385
|
+
|
|
386
|
+
<div className="space-y-4">
|
|
387
|
+
<div>
|
|
388
|
+
<label className="block text-sm text-[#666] mb-1">Name</label>
|
|
389
|
+
<input
|
|
390
|
+
type="text"
|
|
391
|
+
value={name}
|
|
392
|
+
onChange={e => setName(e.target.value)}
|
|
393
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
394
|
+
placeholder="My Project"
|
|
395
|
+
autoFocus
|
|
396
|
+
/>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div>
|
|
400
|
+
<label className="block text-sm text-[#666] mb-1">Description (optional)</label>
|
|
401
|
+
<input
|
|
402
|
+
type="text"
|
|
403
|
+
value={description}
|
|
404
|
+
onChange={e => setDescription(e.target.value)}
|
|
405
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
406
|
+
placeholder="A short description"
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div>
|
|
411
|
+
<label className="block text-sm text-[#666] mb-1">Color</label>
|
|
412
|
+
<div className="flex gap-3 flex-wrap">
|
|
413
|
+
{DEFAULT_PROJECT_COLORS.map(c => (
|
|
414
|
+
<button
|
|
415
|
+
key={c}
|
|
416
|
+
type="button"
|
|
417
|
+
onClick={() => setColor(c)}
|
|
418
|
+
className={`w-10 h-10 rounded-full transition ${
|
|
419
|
+
color === c ? "ring-2 ring-white ring-offset-2 ring-offset-[#111]" : "hover:scale-110"
|
|
420
|
+
}`}
|
|
421
|
+
style={{ backgroundColor: c }}
|
|
422
|
+
/>
|
|
423
|
+
))}
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<div className="flex gap-3 mt-6">
|
|
431
|
+
<button
|
|
432
|
+
onClick={onClose}
|
|
433
|
+
className="flex-1 border border-[#333] hover:border-[#f97316] hover:text-[#f97316] px-4 py-2 rounded font-medium transition"
|
|
434
|
+
>
|
|
435
|
+
Cancel
|
|
436
|
+
</button>
|
|
437
|
+
<button
|
|
438
|
+
onClick={handleSubmit}
|
|
439
|
+
disabled={saving || !name.trim()}
|
|
440
|
+
className="flex-1 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
|
|
441
|
+
>
|
|
442
|
+
{saving ? "Saving..." : project ? "Update" : "Create"}
|
|
443
|
+
</button>
|
|
444
|
+
</div>
|
|
445
|
+
</Modal>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
215
449
|
interface ProviderKeyCardProps {
|
|
216
450
|
provider: Provider;
|
|
217
451
|
isEditing: boolean;
|
|
@@ -237,9 +471,11 @@ interface VersionInfo {
|
|
|
237
471
|
interface AllVersionInfo {
|
|
238
472
|
apteva: VersionInfo;
|
|
239
473
|
agent: VersionInfo;
|
|
474
|
+
isDocker?: boolean;
|
|
240
475
|
}
|
|
241
476
|
|
|
242
477
|
function UpdatesSettings() {
|
|
478
|
+
const { authFetch } = useAuth();
|
|
243
479
|
const [versions, setVersions] = useState<AllVersionInfo | null>(null);
|
|
244
480
|
const [checking, setChecking] = useState(false);
|
|
245
481
|
const [updatingAgent, setUpdatingAgent] = useState(false);
|
|
@@ -251,7 +487,7 @@ function UpdatesSettings() {
|
|
|
251
487
|
setChecking(true);
|
|
252
488
|
setError(null);
|
|
253
489
|
try {
|
|
254
|
-
const res = await
|
|
490
|
+
const res = await authFetch("/api/version");
|
|
255
491
|
if (!res.ok) throw new Error("Failed to check for updates");
|
|
256
492
|
const data = await res.json();
|
|
257
493
|
setVersions(data);
|
|
@@ -266,7 +502,7 @@ function UpdatesSettings() {
|
|
|
266
502
|
setError(null);
|
|
267
503
|
setUpdateSuccess(null);
|
|
268
504
|
try {
|
|
269
|
-
const res = await
|
|
505
|
+
const res = await authFetch("/api/version/update", { method: "POST" });
|
|
270
506
|
const data = await res.json();
|
|
271
507
|
if (!data.success) {
|
|
272
508
|
setError(data.error || "Update failed");
|
|
@@ -293,7 +529,7 @@ function UpdatesSettings() {
|
|
|
293
529
|
const hasAnyUpdate = versions?.apteva.updateAvailable || versions?.agent.updateAvailable;
|
|
294
530
|
|
|
295
531
|
return (
|
|
296
|
-
<div className="max-w-
|
|
532
|
+
<div className="max-w-4xl w-full">
|
|
297
533
|
<div className="mb-6">
|
|
298
534
|
<h1 className="text-2xl font-semibold mb-1">Updates</h1>
|
|
299
535
|
<p className="text-[#666]">
|
|
@@ -302,10 +538,74 @@ function UpdatesSettings() {
|
|
|
302
538
|
</div>
|
|
303
539
|
|
|
304
540
|
{checking && !versions ? (
|
|
305
|
-
<div className="text-[#666]">Checking
|
|
541
|
+
<div className="text-[#666]">Checking version info...</div>
|
|
306
542
|
) : error && !versions ? (
|
|
307
543
|
<div className="text-red-400">{error}</div>
|
|
544
|
+
) : versions?.isDocker ? (
|
|
545
|
+
/* Docker Environment */
|
|
546
|
+
<div className="space-y-6">
|
|
547
|
+
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-4">
|
|
548
|
+
<div className="flex items-center gap-2 text-blue-400 mb-2">
|
|
549
|
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
550
|
+
<path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.186.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.186.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.186.186 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.186.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186H5.136a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>
|
|
551
|
+
</svg>
|
|
552
|
+
<span className="font-medium">Docker Environment</span>
|
|
553
|
+
</div>
|
|
554
|
+
<p className="text-sm text-[#888]">
|
|
555
|
+
Updates are automatic when you pull a new image version.
|
|
556
|
+
</p>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
{/* Current Version */}
|
|
560
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-5">
|
|
561
|
+
<div className="flex items-center justify-between mb-4">
|
|
562
|
+
<div>
|
|
563
|
+
<h3 className="font-medium text-lg">Current Version</h3>
|
|
564
|
+
<p className="text-sm text-[#666]">apteva + agent binary</p>
|
|
565
|
+
</div>
|
|
566
|
+
<div className="text-right">
|
|
567
|
+
<div className="text-xl font-mono">v{versions.apteva.installed || "?"}</div>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
{hasAnyUpdate ? (
|
|
572
|
+
<div className="bg-[#f97316]/10 border border-[#f97316]/30 rounded-lg p-4">
|
|
573
|
+
<p className="text-sm text-[#888] mb-3">
|
|
574
|
+
A newer version (v{versions.apteva.latest}) is available. To update:
|
|
575
|
+
</p>
|
|
576
|
+
<div className="space-y-2">
|
|
577
|
+
<code className="block bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#888]">
|
|
578
|
+
docker pull apteva/apteva:latest
|
|
579
|
+
</code>
|
|
580
|
+
<code className="block bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#888]">
|
|
581
|
+
docker compose up -d
|
|
582
|
+
</code>
|
|
583
|
+
</div>
|
|
584
|
+
<button
|
|
585
|
+
onClick={() => {
|
|
586
|
+
navigator.clipboard.writeText("docker pull apteva/apteva:latest && docker compose up -d");
|
|
587
|
+
setCopied("docker");
|
|
588
|
+
setTimeout(() => setCopied(null), 2000);
|
|
589
|
+
}}
|
|
590
|
+
className="mt-3 px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#222] rounded text-sm"
|
|
591
|
+
>
|
|
592
|
+
{copied === "docker" ? "Copied!" : "Copy commands"}
|
|
593
|
+
</button>
|
|
594
|
+
</div>
|
|
595
|
+
) : (
|
|
596
|
+
<div className="flex items-center gap-2 text-green-400 text-sm">
|
|
597
|
+
<CheckIcon className="w-4 h-4" />
|
|
598
|
+
Up to date
|
|
599
|
+
</div>
|
|
600
|
+
)}
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
<p className="text-xs text-[#555]">
|
|
604
|
+
Your data is stored in a Docker volume and persists across updates.
|
|
605
|
+
</p>
|
|
606
|
+
</div>
|
|
308
607
|
) : versions ? (
|
|
608
|
+
/* Non-Docker Environment */
|
|
309
609
|
<div className="space-y-6">
|
|
310
610
|
{updateSuccess && (
|
|
311
611
|
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 text-green-400">
|
|
@@ -435,18 +735,22 @@ function ProviderKeyCard({
|
|
|
435
735
|
<div className={`bg-[#111] border rounded-lg p-4 ${
|
|
436
736
|
provider.hasKey ? 'border-green-500/20' : 'border-[#1a1a1a]'
|
|
437
737
|
}`}>
|
|
438
|
-
<div className="flex items-
|
|
439
|
-
<div>
|
|
738
|
+
<div className="flex items-start justify-between gap-2 mb-2">
|
|
739
|
+
<div className="min-w-0">
|
|
440
740
|
<h3 className="font-medium">{provider.name}</h3>
|
|
441
|
-
<p className="text-sm text-[#666]">
|
|
741
|
+
<p className="text-sm text-[#666] truncate">
|
|
742
|
+
{provider.type === "integration"
|
|
743
|
+
? (provider.description || "MCP integration")
|
|
744
|
+
: `${provider.models.length} models`}
|
|
745
|
+
</p>
|
|
442
746
|
</div>
|
|
443
747
|
{provider.hasKey ? (
|
|
444
|
-
<span className="text-green-400 text-xs flex items-center gap-1 bg-green-500/10 px-2 py-1 rounded">
|
|
748
|
+
<span className="text-green-400 text-xs flex items-center gap-1 bg-green-500/10 px-2 py-1 rounded whitespace-nowrap flex-shrink-0">
|
|
445
749
|
<CheckIcon className="w-3 h-3" />
|
|
446
750
|
{provider.keyHint}
|
|
447
751
|
</span>
|
|
448
752
|
) : (
|
|
449
|
-
<span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded">
|
|
753
|
+
<span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded whitespace-nowrap flex-shrink-0">
|
|
450
754
|
Not configured
|
|
451
755
|
</span>
|
|
452
756
|
)}
|
|
@@ -628,3 +932,85 @@ function IntegrationKeyCard({
|
|
|
628
932
|
</div>
|
|
629
933
|
);
|
|
630
934
|
}
|
|
935
|
+
|
|
936
|
+
function DataSettings() {
|
|
937
|
+
const { authFetch } = useAuth();
|
|
938
|
+
const [clearing, setClearing] = useState(false);
|
|
939
|
+
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
|
|
940
|
+
const [eventCount, setEventCount] = useState<number | null>(null);
|
|
941
|
+
|
|
942
|
+
const fetchStats = async () => {
|
|
943
|
+
try {
|
|
944
|
+
const res = await authFetch("/api/telemetry/stats");
|
|
945
|
+
const data = await res.json();
|
|
946
|
+
setEventCount(data.stats?.total_events || 0);
|
|
947
|
+
} catch {
|
|
948
|
+
setEventCount(null);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
useEffect(() => {
|
|
953
|
+
fetchStats();
|
|
954
|
+
}, []);
|
|
955
|
+
|
|
956
|
+
const clearTelemetry = async () => {
|
|
957
|
+
if (!confirm("Are you sure you want to delete all telemetry data? This cannot be undone.")) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
setClearing(true);
|
|
962
|
+
setMessage(null);
|
|
963
|
+
|
|
964
|
+
try {
|
|
965
|
+
const res = await authFetch("/api/telemetry/clear", { method: "POST" });
|
|
966
|
+
const data = await res.json();
|
|
967
|
+
|
|
968
|
+
if (res.ok) {
|
|
969
|
+
setMessage({ type: "success", text: `Cleared ${data.deleted || 0} telemetry events.` });
|
|
970
|
+
setEventCount(0);
|
|
971
|
+
} else {
|
|
972
|
+
setMessage({ type: "error", text: data.error || "Failed to clear telemetry" });
|
|
973
|
+
}
|
|
974
|
+
} catch {
|
|
975
|
+
setMessage({ type: "error", text: "Failed to clear telemetry" });
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
setClearing(false);
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
return (
|
|
982
|
+
<div className="max-w-4xl w-full">
|
|
983
|
+
<div className="mb-6">
|
|
984
|
+
<h1 className="text-2xl font-semibold mb-1">Data Management</h1>
|
|
985
|
+
<p className="text-[#666]">Manage stored data and telemetry.</p>
|
|
986
|
+
</div>
|
|
987
|
+
|
|
988
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
|
|
989
|
+
<h3 className="font-medium mb-2">Telemetry Data</h3>
|
|
990
|
+
<p className="text-sm text-[#666] mb-4">
|
|
991
|
+
{eventCount !== null
|
|
992
|
+
? `${eventCount.toLocaleString()} events stored`
|
|
993
|
+
: "Loading..."}
|
|
994
|
+
</p>
|
|
995
|
+
|
|
996
|
+
{message && (
|
|
997
|
+
<div className={`mb-4 p-3 rounded text-sm ${
|
|
998
|
+
message.type === "success"
|
|
999
|
+
? "bg-green-500/10 text-green-400 border border-green-500/30"
|
|
1000
|
+
: "bg-red-500/10 text-red-400 border border-red-500/30"
|
|
1001
|
+
}`}>
|
|
1002
|
+
{message.text}
|
|
1003
|
+
</div>
|
|
1004
|
+
)}
|
|
1005
|
+
|
|
1006
|
+
<button
|
|
1007
|
+
onClick={clearTelemetry}
|
|
1008
|
+
disabled={clearing || eventCount === 0}
|
|
1009
|
+
className="px-4 py-2 bg-red-500/20 text-red-400 hover:bg-red-500/30 disabled:opacity-50 disabled:cursor-not-allowed rounded text-sm font-medium transition"
|
|
1010
|
+
>
|
|
1011
|
+
{clearing ? "Clearing..." : "Clear All Telemetry"}
|
|
1012
|
+
</button>
|
|
1013
|
+
</div>
|
|
1014
|
+
</div>
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { TasksIcon } from "../common/Icons";
|
|
3
|
+
import { useAuth } from "../../context";
|
|
3
4
|
import type { Task } from "../../types";
|
|
4
5
|
|
|
5
6
|
interface TasksPageProps {
|
|
@@ -7,20 +8,14 @@ interface TasksPageProps {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
11
|
+
const { authFetch } = useAuth();
|
|
10
12
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
11
13
|
const [loading, setLoading] = useState(true);
|
|
12
14
|
const [filter, setFilter] = useState<string>("all");
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
fetchTasks();
|
|
16
|
-
// Refresh every 10 seconds
|
|
17
|
-
const interval = setInterval(fetchTasks, 10000);
|
|
18
|
-
return () => clearInterval(interval);
|
|
19
|
-
}, [filter]);
|
|
20
|
-
|
|
21
|
-
const fetchTasks = async () => {
|
|
16
|
+
const fetchTasks = useCallback(async () => {
|
|
22
17
|
try {
|
|
23
|
-
const res = await
|
|
18
|
+
const res = await authFetch(`/api/tasks?status=${filter}`);
|
|
24
19
|
const data = await res.json();
|
|
25
20
|
setTasks(data.tasks || []);
|
|
26
21
|
} catch (e) {
|
|
@@ -28,7 +23,14 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
|
28
23
|
} finally {
|
|
29
24
|
setLoading(false);
|
|
30
25
|
}
|
|
31
|
-
};
|
|
26
|
+
}, [authFetch, filter]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
fetchTasks();
|
|
30
|
+
// Refresh every 10 seconds
|
|
31
|
+
const interval = setInterval(fetchTasks, 10000);
|
|
32
|
+
return () => clearInterval(interval);
|
|
33
|
+
}, [fetchTasks]);
|
|
32
34
|
|
|
33
35
|
const statusColors: Record<string, string> = {
|
|
34
36
|
pending: "bg-yellow-500/20 text-yellow-400",
|
|
@@ -47,21 +49,21 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
|
47
49
|
];
|
|
48
50
|
|
|
49
51
|
return (
|
|
50
|
-
<div className="flex-1 p-6 overflow-auto">
|
|
52
|
+
<div className="flex-1 p-4 md:p-6 overflow-auto">
|
|
51
53
|
<div className="max-w-4xl mx-auto">
|
|
52
|
-
<div className="
|
|
53
|
-
<div>
|
|
54
|
-
<h1 className="text-2xl font-semibold mb-1">Tasks</h1>
|
|
55
|
-
<p className="text-[#666]">
|
|
54
|
+
<div className="mb-6">
|
|
55
|
+
<div className="mb-4">
|
|
56
|
+
<h1 className="text-xl md:text-2xl font-semibold mb-1">Tasks</h1>
|
|
57
|
+
<p className="text-sm text-[#666]">
|
|
56
58
|
View tasks from all running agents
|
|
57
59
|
</p>
|
|
58
60
|
</div>
|
|
59
|
-
<div className="flex gap-2">
|
|
61
|
+
<div className="flex gap-2 overflow-x-auto scrollbar-hide pb-1">
|
|
60
62
|
{filterOptions.map(opt => (
|
|
61
63
|
<button
|
|
62
64
|
key={opt.value}
|
|
63
65
|
onClick={() => setFilter(opt.value)}
|
|
64
|
-
className={`px-3 py-1.5 rounded text-sm transition ${
|
|
66
|
+
className={`px-3 py-1.5 rounded text-sm transition whitespace-nowrap ${
|
|
65
67
|
filter === opt.value
|
|
66
68
|
? "bg-[#f97316] text-black"
|
|
67
69
|
: "bg-[#1a1a1a] hover:bg-[#222]"
|
|
@@ -113,7 +115,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
|
113
115
|
</p>
|
|
114
116
|
)}
|
|
115
117
|
|
|
116
|
-
<div className="flex items-center gap-4 text-xs text-[#555]">
|
|
118
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-[#555]">
|
|
117
119
|
<span>Type: {task.type}</span>
|
|
118
120
|
<span>Priority: {task.priority}</span>
|
|
119
121
|
{task.recurrence && <span>Recurrence: {task.recurrence}</span>}
|