apteva 0.2.7 → 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 +561 -32
- package/src/routes/api.ts +901 -35
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +46 -5
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +24 -22
- 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 +70 -22
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +135 -18
- 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 +320 -21
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- 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.3kb50qa3.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" | "data";
|
|
7
|
+
type SettingsTab = "providers" | "projects" | "updates" | "data";
|
|
6
8
|
|
|
7
9
|
export function SettingsPage() {
|
|
8
10
|
const [activeTab, setActiveTab] = useState<SettingsTab>("providers");
|
|
@@ -18,6 +20,11 @@ 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"}
|
|
@@ -34,6 +41,7 @@ export function SettingsPage() {
|
|
|
34
41
|
{/* Settings Content */}
|
|
35
42
|
<div className="flex-1 overflow-auto p-6">
|
|
36
43
|
{activeTab === "providers" && <ProvidersSettings />}
|
|
44
|
+
{activeTab === "projects" && <ProjectsSettings />}
|
|
37
45
|
{activeTab === "updates" && <UpdatesSettings />}
|
|
38
46
|
{activeTab === "data" && <DataSettings />}
|
|
39
47
|
</div>
|
|
@@ -65,6 +73,7 @@ function SettingsNavItem({
|
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
function ProvidersSettings() {
|
|
76
|
+
const { authFetch } = useAuth();
|
|
68
77
|
const [providers, setProviders] = useState<Provider[]>([]);
|
|
69
78
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
70
79
|
const [apiKey, setApiKey] = useState("");
|
|
@@ -74,7 +83,7 @@ function ProvidersSettings() {
|
|
|
74
83
|
const [success, setSuccess] = useState<string | null>(null);
|
|
75
84
|
|
|
76
85
|
const fetchProviders = async () => {
|
|
77
|
-
const res = await
|
|
86
|
+
const res = await authFetch("/api/providers");
|
|
78
87
|
const data = await res.json();
|
|
79
88
|
setProviders(data.providers || []);
|
|
80
89
|
};
|
|
@@ -91,7 +100,7 @@ function ProvidersSettings() {
|
|
|
91
100
|
|
|
92
101
|
try {
|
|
93
102
|
setTesting(true);
|
|
94
|
-
const testRes = await
|
|
103
|
+
const testRes = await authFetch(`/api/keys/${selectedProvider}/test`, {
|
|
95
104
|
method: "POST",
|
|
96
105
|
headers: { "Content-Type": "application/json" },
|
|
97
106
|
body: JSON.stringify({ key: apiKey }),
|
|
@@ -105,7 +114,7 @@ function ProvidersSettings() {
|
|
|
105
114
|
return;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
const saveRes = await
|
|
117
|
+
const saveRes = await authFetch(`/api/keys/${selectedProvider}`, {
|
|
109
118
|
method: "POST",
|
|
110
119
|
headers: { "Content-Type": "application/json" },
|
|
111
120
|
body: JSON.stringify({ key: apiKey }),
|
|
@@ -128,7 +137,7 @@ function ProvidersSettings() {
|
|
|
128
137
|
|
|
129
138
|
const deleteKey = async (providerId: string) => {
|
|
130
139
|
if (!confirm("Are you sure you want to remove this API key?")) return;
|
|
131
|
-
await
|
|
140
|
+
await authFetch(`/api/keys/${providerId}`, { method: "DELETE" });
|
|
132
141
|
fetchProviders();
|
|
133
142
|
};
|
|
134
143
|
|
|
@@ -138,7 +147,7 @@ function ProvidersSettings() {
|
|
|
138
147
|
const intConfiguredCount = integrations.filter(p => p.hasKey).length;
|
|
139
148
|
|
|
140
149
|
return (
|
|
141
|
-
<div className="
|
|
150
|
+
<div className="space-y-10">
|
|
142
151
|
{/* AI Providers Section */}
|
|
143
152
|
<div>
|
|
144
153
|
<div className="mb-6">
|
|
@@ -148,7 +157,7 @@ function ProvidersSettings() {
|
|
|
148
157
|
</p>
|
|
149
158
|
</div>
|
|
150
159
|
|
|
151
|
-
<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">
|
|
152
161
|
{llmProviders.map(provider => (
|
|
153
162
|
<ProviderKeyCard
|
|
154
163
|
key={provider.id}
|
|
@@ -186,7 +195,7 @@ function ProvidersSettings() {
|
|
|
186
195
|
</p>
|
|
187
196
|
</div>
|
|
188
197
|
|
|
189
|
-
<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">
|
|
190
199
|
{integrations.map(provider => (
|
|
191
200
|
<IntegrationKeyCard
|
|
192
201
|
key={provider.id}
|
|
@@ -218,6 +227,225 @@ function ProvidersSettings() {
|
|
|
218
227
|
);
|
|
219
228
|
}
|
|
220
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
|
+
|
|
221
449
|
interface ProviderKeyCardProps {
|
|
222
450
|
provider: Provider;
|
|
223
451
|
isEditing: boolean;
|
|
@@ -243,9 +471,11 @@ interface VersionInfo {
|
|
|
243
471
|
interface AllVersionInfo {
|
|
244
472
|
apteva: VersionInfo;
|
|
245
473
|
agent: VersionInfo;
|
|
474
|
+
isDocker?: boolean;
|
|
246
475
|
}
|
|
247
476
|
|
|
248
477
|
function UpdatesSettings() {
|
|
478
|
+
const { authFetch } = useAuth();
|
|
249
479
|
const [versions, setVersions] = useState<AllVersionInfo | null>(null);
|
|
250
480
|
const [checking, setChecking] = useState(false);
|
|
251
481
|
const [updatingAgent, setUpdatingAgent] = useState(false);
|
|
@@ -257,7 +487,7 @@ function UpdatesSettings() {
|
|
|
257
487
|
setChecking(true);
|
|
258
488
|
setError(null);
|
|
259
489
|
try {
|
|
260
|
-
const res = await
|
|
490
|
+
const res = await authFetch("/api/version");
|
|
261
491
|
if (!res.ok) throw new Error("Failed to check for updates");
|
|
262
492
|
const data = await res.json();
|
|
263
493
|
setVersions(data);
|
|
@@ -272,7 +502,7 @@ function UpdatesSettings() {
|
|
|
272
502
|
setError(null);
|
|
273
503
|
setUpdateSuccess(null);
|
|
274
504
|
try {
|
|
275
|
-
const res = await
|
|
505
|
+
const res = await authFetch("/api/version/update", { method: "POST" });
|
|
276
506
|
const data = await res.json();
|
|
277
507
|
if (!data.success) {
|
|
278
508
|
setError(data.error || "Update failed");
|
|
@@ -299,7 +529,7 @@ function UpdatesSettings() {
|
|
|
299
529
|
const hasAnyUpdate = versions?.apteva.updateAvailable || versions?.agent.updateAvailable;
|
|
300
530
|
|
|
301
531
|
return (
|
|
302
|
-
<div className="max-w-
|
|
532
|
+
<div className="max-w-4xl w-full">
|
|
303
533
|
<div className="mb-6">
|
|
304
534
|
<h1 className="text-2xl font-semibold mb-1">Updates</h1>
|
|
305
535
|
<p className="text-[#666]">
|
|
@@ -308,10 +538,74 @@ function UpdatesSettings() {
|
|
|
308
538
|
</div>
|
|
309
539
|
|
|
310
540
|
{checking && !versions ? (
|
|
311
|
-
<div className="text-[#666]">Checking
|
|
541
|
+
<div className="text-[#666]">Checking version info...</div>
|
|
312
542
|
) : error && !versions ? (
|
|
313
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>
|
|
314
607
|
) : versions ? (
|
|
608
|
+
/* Non-Docker Environment */
|
|
315
609
|
<div className="space-y-6">
|
|
316
610
|
{updateSuccess && (
|
|
317
611
|
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 text-green-400">
|
|
@@ -441,18 +735,22 @@ function ProviderKeyCard({
|
|
|
441
735
|
<div className={`bg-[#111] border rounded-lg p-4 ${
|
|
442
736
|
provider.hasKey ? 'border-green-500/20' : 'border-[#1a1a1a]'
|
|
443
737
|
}`}>
|
|
444
|
-
<div className="flex items-
|
|
445
|
-
<div>
|
|
738
|
+
<div className="flex items-start justify-between gap-2 mb-2">
|
|
739
|
+
<div className="min-w-0">
|
|
446
740
|
<h3 className="font-medium">{provider.name}</h3>
|
|
447
|
-
<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>
|
|
448
746
|
</div>
|
|
449
747
|
{provider.hasKey ? (
|
|
450
|
-
<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">
|
|
451
749
|
<CheckIcon className="w-3 h-3" />
|
|
452
750
|
{provider.keyHint}
|
|
453
751
|
</span>
|
|
454
752
|
) : (
|
|
455
|
-
<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">
|
|
456
754
|
Not configured
|
|
457
755
|
</span>
|
|
458
756
|
)}
|
|
@@ -636,13 +934,14 @@ function IntegrationKeyCard({
|
|
|
636
934
|
}
|
|
637
935
|
|
|
638
936
|
function DataSettings() {
|
|
937
|
+
const { authFetch } = useAuth();
|
|
639
938
|
const [clearing, setClearing] = useState(false);
|
|
640
939
|
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
|
|
641
940
|
const [eventCount, setEventCount] = useState<number | null>(null);
|
|
642
941
|
|
|
643
942
|
const fetchStats = async () => {
|
|
644
943
|
try {
|
|
645
|
-
const res = await
|
|
944
|
+
const res = await authFetch("/api/telemetry/stats");
|
|
646
945
|
const data = await res.json();
|
|
647
946
|
setEventCount(data.stats?.total_events || 0);
|
|
648
947
|
} catch {
|
|
@@ -663,7 +962,7 @@ function DataSettings() {
|
|
|
663
962
|
setMessage(null);
|
|
664
963
|
|
|
665
964
|
try {
|
|
666
|
-
const res = await
|
|
965
|
+
const res = await authFetch("/api/telemetry/clear", { method: "POST" });
|
|
667
966
|
const data = await res.json();
|
|
668
967
|
|
|
669
968
|
if (res.ok) {
|
|
@@ -680,7 +979,7 @@ function DataSettings() {
|
|
|
680
979
|
};
|
|
681
980
|
|
|
682
981
|
return (
|
|
683
|
-
<div className="max-w-
|
|
982
|
+
<div className="max-w-4xl w-full">
|
|
684
983
|
<div className="mb-6">
|
|
685
984
|
<h1 className="text-2xl font-semibold mb-1">Data Management</h1>
|
|
686
985
|
<p className="text-[#666]">Manage stored data and telemetry.</p>
|
|
@@ -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>}
|