apteva 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/App.m4hg4bxq.js +218 -0
  2. package/dist/index.html +4 -2
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +688 -45
  9. package/src/integrations/composio.ts +437 -0
  10. package/src/integrations/index.ts +80 -0
  11. package/src/openapi.ts +1724 -0
  12. package/src/routes/api.ts +1476 -118
  13. package/src/routes/auth.ts +242 -0
  14. package/src/server.ts +121 -11
  15. package/src/web/App.tsx +64 -19
  16. package/src/web/components/agents/AgentCard.tsx +24 -22
  17. package/src/web/components/agents/AgentPanel.tsx +810 -45
  18. package/src/web/components/agents/AgentsView.tsx +81 -9
  19. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  20. package/src/web/components/api/ApiDocsPage.tsx +583 -0
  21. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  22. package/src/web/components/auth/LoginPage.tsx +91 -0
  23. package/src/web/components/auth/index.ts +2 -0
  24. package/src/web/components/common/Icons.tsx +56 -0
  25. package/src/web/components/common/Modal.tsx +184 -1
  26. package/src/web/components/dashboard/Dashboard.tsx +70 -22
  27. package/src/web/components/index.ts +3 -0
  28. package/src/web/components/layout/Header.tsx +135 -18
  29. package/src/web/components/layout/Sidebar.tsx +87 -43
  30. package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
  31. package/src/web/components/mcp/McpPage.tsx +451 -63
  32. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  33. package/src/web/components/settings/SettingsPage.tsx +340 -26
  34. package/src/web/components/tasks/TasksPage.tsx +22 -20
  35. package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
  36. package/src/web/context/AuthContext.tsx +230 -0
  37. package/src/web/context/ProjectContext.tsx +182 -0
  38. package/src/web/context/index.ts +5 -0
  39. package/src/web/hooks/useAgents.ts +18 -6
  40. package/src/web/hooks/useOnboarding.ts +20 -4
  41. package/src/web/hooks/useProviders.ts +15 -5
  42. package/src/web/icon.png +0 -0
  43. package/src/web/index.html +1 -1
  44. package/src/web/styles.css +12 -0
  45. package/src/web/types.ts +10 -1
  46. package/dist/App.3kb50qa3.js +0 -213
@@ -1,13 +1,16 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { CheckIcon } from "../common/Icons";
3
+ import { CreateAccountStep } from "../auth";
3
4
  import type { Provider } from "../../types";
4
5
 
5
6
  interface OnboardingWizardProps {
6
7
  onComplete: () => void;
8
+ needsAccount?: boolean; // Whether to show account creation step
7
9
  }
8
10
 
9
- export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
10
- const [step, setStep] = useState(1);
11
+ export function OnboardingWizard({ onComplete, needsAccount = false }: OnboardingWizardProps) {
12
+ // Step 0 = account creation (if needed), Step 1 = add keys, Step 2 = complete
13
+ const [step, setStep] = useState(needsAccount ? 0 : 1);
11
14
  const [providers, setProviders] = useState<Provider[]>([]);
12
15
  const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
13
16
  const [apiKey, setApiKey] = useState("");
@@ -15,16 +18,26 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
15
18
  const [testing, setTesting] = useState(false);
16
19
  const [error, setError] = useState<string | null>(null);
17
20
  const [success, setSuccess] = useState<string | null>(null);
21
+ const [accountCreated, setAccountCreated] = useState(false);
22
+
23
+ // Get auth token from session storage (set during account creation)
24
+ const getAuthHeaders = (): Record<string, string> => {
25
+ const token = sessionStorage.getItem("accessToken");
26
+ return token ? { Authorization: `Bearer ${token}` } : {};
27
+ };
18
28
 
19
29
  useEffect(() => {
20
- fetch("/api/providers")
30
+ // Don't fetch providers until after account is created (if needed)
31
+ if (needsAccount && !accountCreated && step === 0) return;
32
+
33
+ fetch("/api/providers", { headers: getAuthHeaders() })
21
34
  .then(res => res.json())
22
35
  .then(data => {
23
36
  // Only show LLM providers in onboarding, not integrations
24
37
  const llmProviders = (data.providers || []).filter((p: Provider) => p.type === "llm");
25
38
  setProviders(llmProviders);
26
39
  });
27
- }, []);
40
+ }, [accountCreated, step, needsAccount]);
28
41
 
29
42
  const configuredProviders = providers.filter(p => p.hasKey);
30
43
 
@@ -38,7 +51,7 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
38
51
  setTesting(true);
39
52
  const testRes = await fetch(`/api/keys/${selectedProvider}/test`, {
40
53
  method: "POST",
41
- headers: { "Content-Type": "application/json" },
54
+ headers: { "Content-Type": "application/json", ...getAuthHeaders() },
42
55
  body: JSON.stringify({ key: apiKey }),
43
56
  });
44
57
  const testData = await testRes.json();
@@ -52,7 +65,7 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
52
65
 
53
66
  const saveRes = await fetch(`/api/keys/${selectedProvider}`, {
54
67
  method: "POST",
55
- headers: { "Content-Type": "application/json" },
68
+ headers: { "Content-Type": "application/json", ...getAuthHeaders() },
56
69
  body: JSON.stringify({ key: apiKey }),
57
70
  });
58
71
  const saveData = await saveRes.json();
@@ -62,7 +75,7 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
62
75
  } else {
63
76
  setSuccess("API key saved successfully!");
64
77
  setApiKey("");
65
- const res = await fetch("/api/providers");
78
+ const res = await fetch("/api/providers", { headers: getAuthHeaders() });
66
79
  const data = await res.json();
67
80
  setProviders(data.providers || []);
68
81
  setSelectedProvider(null);
@@ -74,10 +87,43 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
74
87
  };
75
88
 
76
89
  const completeOnboarding = async () => {
77
- await fetch("/api/onboarding/complete", { method: "POST" });
90
+ // Create a default project for the user
91
+ try {
92
+ const projectRes = await fetch("/api/projects", {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json", ...getAuthHeaders() },
95
+ body: JSON.stringify({
96
+ name: "My Project",
97
+ description: "Default project for organizing agents",
98
+ color: "#f97316", // Orange - matches brand color
99
+ }),
100
+ });
101
+
102
+ if (projectRes.ok) {
103
+ const data = await projectRes.json();
104
+ // Set this project as the current project in localStorage
105
+ if (data.project?.id) {
106
+ localStorage.setItem("apteva_current_project", data.project.id);
107
+ }
108
+ }
109
+ } catch (e) {
110
+ // Don't block onboarding if project creation fails
111
+ console.error("Failed to create default project:", e);
112
+ }
113
+
114
+ await fetch("/api/onboarding/complete", { method: "POST", headers: getAuthHeaders() });
78
115
  onComplete();
79
116
  };
80
117
 
118
+ const handleAccountCreated = () => {
119
+ setAccountCreated(true);
120
+ setStep(1);
121
+ };
122
+
123
+ // Calculate total steps and current progress
124
+ const totalSteps = needsAccount ? 3 : 2;
125
+ const currentStep = needsAccount ? step : step - 1;
126
+
81
127
  return (
82
128
  <div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex items-center justify-center p-8">
83
129
  <div className="w-full max-w-2xl">
@@ -92,12 +138,22 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
92
138
 
93
139
  {/* Progress */}
94
140
  <div className="flex items-center justify-center gap-2 mb-8">
141
+ {needsAccount && (
142
+ <>
143
+ <div className={`w-3 h-3 rounded-full ${step >= 0 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
144
+ <div className={`w-16 h-0.5 ${step >= 1 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
145
+ </>
146
+ )}
95
147
  <div className={`w-3 h-3 rounded-full ${step >= 1 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
96
148
  <div className={`w-16 h-0.5 ${step >= 2 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
97
149
  <div className={`w-3 h-3 rounded-full ${step >= 2 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
98
150
  </div>
99
151
 
100
152
  <div className="bg-[#111] rounded-lg border border-[#1a1a1a] p-8">
153
+ {step === 0 && needsAccount && (
154
+ <CreateAccountStep onComplete={handleAccountCreated} />
155
+ )}
156
+
101
157
  {step === 1 && (
102
158
  <Step1AddKeys
103
159
  providers={providers}
@@ -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, useConfirm } 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("");
@@ -72,9 +81,10 @@ function ProvidersSettings() {
72
81
  const [testing, setTesting] = useState(false);
73
82
  const [error, setError] = useState<string | null>(null);
74
83
  const [success, setSuccess] = useState<string | null>(null);
84
+ const { confirm, ConfirmDialog } = useConfirm();
75
85
 
76
86
  const fetchProviders = async () => {
77
- const res = await fetch("/api/providers");
87
+ const res = await authFetch("/api/providers");
78
88
  const data = await res.json();
79
89
  setProviders(data.providers || []);
80
90
  };
@@ -91,7 +101,7 @@ function ProvidersSettings() {
91
101
 
92
102
  try {
93
103
  setTesting(true);
94
- const testRes = await fetch(`/api/keys/${selectedProvider}/test`, {
104
+ const testRes = await authFetch(`/api/keys/${selectedProvider}/test`, {
95
105
  method: "POST",
96
106
  headers: { "Content-Type": "application/json" },
97
107
  body: JSON.stringify({ key: apiKey }),
@@ -105,7 +115,7 @@ function ProvidersSettings() {
105
115
  return;
106
116
  }
107
117
 
108
- const saveRes = await fetch(`/api/keys/${selectedProvider}`, {
118
+ const saveRes = await authFetch(`/api/keys/${selectedProvider}`, {
109
119
  method: "POST",
110
120
  headers: { "Content-Type": "application/json" },
111
121
  body: JSON.stringify({ key: apiKey }),
@@ -127,8 +137,9 @@ function ProvidersSettings() {
127
137
  };
128
138
 
129
139
  const deleteKey = async (providerId: string) => {
130
- if (!confirm("Are you sure you want to remove this API key?")) return;
131
- await fetch(`/api/keys/${providerId}`, { method: "DELETE" });
140
+ const confirmed = await confirm("Are you sure you want to remove this API key?", { confirmText: "Remove", title: "Remove API Key" });
141
+ if (!confirmed) return;
142
+ await authFetch(`/api/keys/${providerId}`, { method: "DELETE" });
132
143
  fetchProviders();
133
144
  };
134
145
 
@@ -138,7 +149,9 @@ function ProvidersSettings() {
138
149
  const intConfiguredCount = integrations.filter(p => p.hasKey).length;
139
150
 
140
151
  return (
141
- <div className="max-w-4xl space-y-10">
152
+ <>
153
+ {ConfirmDialog}
154
+ <div className="space-y-10">
142
155
  {/* AI Providers Section */}
143
156
  <div>
144
157
  <div className="mb-6">
@@ -148,7 +161,7 @@ function ProvidersSettings() {
148
161
  </p>
149
162
  </div>
150
163
 
151
- <div className="grid gap-4 md:grid-cols-2">
164
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
152
165
  {llmProviders.map(provider => (
153
166
  <ProviderKeyCard
154
167
  key={provider.id}
@@ -186,7 +199,7 @@ function ProvidersSettings() {
186
199
  </p>
187
200
  </div>
188
201
 
189
- <div className="grid gap-4 md:grid-cols-2">
202
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
190
203
  {integrations.map(provider => (
191
204
  <IntegrationKeyCard
192
205
  key={provider.id}
@@ -215,6 +228,229 @@ function ProvidersSettings() {
215
228
  </div>
216
229
  </div>
217
230
  </div>
231
+ </>
232
+ );
233
+ }
234
+
235
+ const DEFAULT_PROJECT_COLORS = [
236
+ "#f97316", // orange
237
+ "#6366f1", // indigo
238
+ "#22c55e", // green
239
+ "#ef4444", // red
240
+ "#3b82f6", // blue
241
+ "#a855f7", // purple
242
+ "#14b8a6", // teal
243
+ "#f59e0b", // amber
244
+ ];
245
+
246
+ function ProjectsSettings() {
247
+ const { projects, createProject, updateProject, deleteProject } = useProjects();
248
+ const [showModal, setShowModal] = useState(false);
249
+ const [editingProject, setEditingProject] = useState<Project | null>(null);
250
+ const { confirm, ConfirmDialog } = useConfirm();
251
+
252
+ const handleDelete = async (id: string) => {
253
+ const confirmed = await confirm("Are you sure you want to delete this project? Agents in this project will become unassigned.", { confirmText: "Delete", title: "Delete Project" });
254
+ if (!confirmed) return;
255
+ await deleteProject(id);
256
+ };
257
+
258
+ const openCreate = () => {
259
+ setEditingProject(null);
260
+ setShowModal(true);
261
+ };
262
+
263
+ const openEdit = (project: Project) => {
264
+ setEditingProject(project);
265
+ setShowModal(true);
266
+ };
267
+
268
+ const closeModal = () => {
269
+ setShowModal(false);
270
+ setEditingProject(null);
271
+ };
272
+
273
+ return (
274
+ <>
275
+ {ConfirmDialog}
276
+ <div className="max-w-4xl w-full">
277
+ <div className="mb-6 flex items-center justify-between gap-4">
278
+ <div>
279
+ <h1 className="text-2xl font-semibold mb-1">Projects</h1>
280
+ <p className="text-[#666]">
281
+ Organize agents into projects for better management.
282
+ </p>
283
+ </div>
284
+ <button
285
+ onClick={openCreate}
286
+ className="flex items-center gap-2 bg-[#f97316] hover:bg-[#fb923c] text-black px-4 py-2 rounded font-medium transition flex-shrink-0"
287
+ >
288
+ <PlusIcon className="w-4 h-4" />
289
+ New Project
290
+ </button>
291
+ </div>
292
+
293
+ {/* Project List */}
294
+ {projects.length === 0 ? (
295
+ <div className="text-center py-12 text-[#666]">
296
+ <p className="text-lg mb-2">No projects yet</p>
297
+ <p className="text-sm">Create a project to organize your agents.</p>
298
+ </div>
299
+ ) : (
300
+ <div className="space-y-3">
301
+ {projects.map(project => (
302
+ <div
303
+ key={project.id}
304
+ className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 flex items-center gap-4"
305
+ >
306
+ <div
307
+ className="w-4 h-4 rounded-full flex-shrink-0"
308
+ style={{ backgroundColor: project.color }}
309
+ />
310
+ <div className="flex-1 min-w-0">
311
+ <h3 className="font-medium">{project.name}</h3>
312
+ {project.description && (
313
+ <p className="text-sm text-[#666] truncate">{project.description}</p>
314
+ )}
315
+ <p className="text-xs text-[#666] mt-1">
316
+ {project.agentCount} agent{project.agentCount !== 1 ? "s" : ""}
317
+ </p>
318
+ </div>
319
+ <div className="flex items-center gap-2">
320
+ <button
321
+ onClick={() => openEdit(project)}
322
+ className="text-sm text-[#888] hover:text-[#e0e0e0] px-2 py-1"
323
+ >
324
+ Edit
325
+ </button>
326
+ <button
327
+ onClick={() => handleDelete(project.id)}
328
+ className="text-sm text-red-400 hover:text-red-300 px-2 py-1"
329
+ >
330
+ Delete
331
+ </button>
332
+ </div>
333
+ </div>
334
+ ))}
335
+ </div>
336
+ )}
337
+
338
+ {/* Project Modal */}
339
+ {showModal && (
340
+ <ProjectModal
341
+ project={editingProject}
342
+ onSave={async (data) => {
343
+ if (editingProject) {
344
+ const result = await updateProject(editingProject.id, data);
345
+ if (result) closeModal();
346
+ return !!result;
347
+ } else {
348
+ const result = await createProject(data);
349
+ if (result) closeModal();
350
+ return !!result;
351
+ }
352
+ }}
353
+ onClose={closeModal}
354
+ />
355
+ )}
356
+ </div>
357
+ </>
358
+ );
359
+ }
360
+
361
+ interface ProjectModalProps {
362
+ project: Project | null;
363
+ onSave: (data: { name: string; description?: string; color: string }) => Promise<boolean>;
364
+ onClose: () => void;
365
+ }
366
+
367
+ function ProjectModal({ project, onSave, onClose }: ProjectModalProps) {
368
+ const [name, setName] = useState(project?.name || "");
369
+ const [description, setDescription] = useState(project?.description || "");
370
+ const [color, setColor] = useState(
371
+ project?.color || DEFAULT_PROJECT_COLORS[Math.floor(Math.random() * DEFAULT_PROJECT_COLORS.length)]
372
+ );
373
+ const [saving, setSaving] = useState(false);
374
+ const [error, setError] = useState<string | null>(null);
375
+
376
+ const handleSubmit = async () => {
377
+ if (!name.trim()) {
378
+ setError("Name is required");
379
+ return;
380
+ }
381
+ setSaving(true);
382
+ setError(null);
383
+ const success = await onSave({ name, description: description || undefined, color });
384
+ setSaving(false);
385
+ if (!success) {
386
+ setError(project ? "Failed to update project" : "Failed to create project");
387
+ }
388
+ };
389
+
390
+ return (
391
+ <Modal onClose={onClose}>
392
+ <h2 className="text-xl font-semibold mb-6">{project ? "Edit Project" : "Create New Project"}</h2>
393
+
394
+ <div className="space-y-4">
395
+ <div>
396
+ <label className="block text-sm text-[#666] mb-1">Name</label>
397
+ <input
398
+ type="text"
399
+ value={name}
400
+ onChange={e => setName(e.target.value)}
401
+ className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
402
+ placeholder="My Project"
403
+ autoFocus
404
+ />
405
+ </div>
406
+
407
+ <div>
408
+ <label className="block text-sm text-[#666] mb-1">Description (optional)</label>
409
+ <input
410
+ type="text"
411
+ value={description}
412
+ onChange={e => setDescription(e.target.value)}
413
+ className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
414
+ placeholder="A short description"
415
+ />
416
+ </div>
417
+
418
+ <div>
419
+ <label className="block text-sm text-[#666] mb-1">Color</label>
420
+ <div className="flex gap-3 flex-wrap">
421
+ {DEFAULT_PROJECT_COLORS.map(c => (
422
+ <button
423
+ key={c}
424
+ type="button"
425
+ onClick={() => setColor(c)}
426
+ className={`w-10 h-10 rounded-full transition ${
427
+ color === c ? "ring-2 ring-white ring-offset-2 ring-offset-[#111]" : "hover:scale-110"
428
+ }`}
429
+ style={{ backgroundColor: c }}
430
+ />
431
+ ))}
432
+ </div>
433
+ </div>
434
+
435
+ {error && <p className="text-red-400 text-sm">{error}</p>}
436
+ </div>
437
+
438
+ <div className="flex gap-3 mt-6">
439
+ <button
440
+ onClick={onClose}
441
+ className="flex-1 border border-[#333] hover:border-[#f97316] hover:text-[#f97316] px-4 py-2 rounded font-medium transition"
442
+ >
443
+ Cancel
444
+ </button>
445
+ <button
446
+ onClick={handleSubmit}
447
+ disabled={saving || !name.trim()}
448
+ className="flex-1 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
449
+ >
450
+ {saving ? "Saving..." : project ? "Update" : "Create"}
451
+ </button>
452
+ </div>
453
+ </Modal>
218
454
  );
219
455
  }
220
456
 
@@ -243,9 +479,11 @@ interface VersionInfo {
243
479
  interface AllVersionInfo {
244
480
  apteva: VersionInfo;
245
481
  agent: VersionInfo;
482
+ isDocker?: boolean;
246
483
  }
247
484
 
248
485
  function UpdatesSettings() {
486
+ const { authFetch } = useAuth();
249
487
  const [versions, setVersions] = useState<AllVersionInfo | null>(null);
250
488
  const [checking, setChecking] = useState(false);
251
489
  const [updatingAgent, setUpdatingAgent] = useState(false);
@@ -257,7 +495,7 @@ function UpdatesSettings() {
257
495
  setChecking(true);
258
496
  setError(null);
259
497
  try {
260
- const res = await fetch("/api/version");
498
+ const res = await authFetch("/api/version");
261
499
  if (!res.ok) throw new Error("Failed to check for updates");
262
500
  const data = await res.json();
263
501
  setVersions(data);
@@ -272,12 +510,16 @@ function UpdatesSettings() {
272
510
  setError(null);
273
511
  setUpdateSuccess(null);
274
512
  try {
275
- const res = await fetch("/api/version/update", { method: "POST" });
513
+ const res = await authFetch("/api/version/update", { method: "POST" });
276
514
  const data = await res.json();
277
515
  if (!data.success) {
278
516
  setError(data.error || "Update failed");
279
517
  } else {
280
- setUpdateSuccess(`Agent updated to v${data.version}. New agents will use this version.`);
518
+ const restartedCount = data.restarted?.length || 0;
519
+ const restartMsg = restartedCount > 0
520
+ ? ` ${restartedCount} running agent${restartedCount > 1 ? 's' : ''} restarted.`
521
+ : '';
522
+ setUpdateSuccess(`Agent binary updated to v${data.version}.${restartMsg}`);
281
523
  await checkForUpdates();
282
524
  }
283
525
  } catch (e) {
@@ -299,7 +541,7 @@ function UpdatesSettings() {
299
541
  const hasAnyUpdate = versions?.apteva.updateAvailable || versions?.agent.updateAvailable;
300
542
 
301
543
  return (
302
- <div className="max-w-2xl">
544
+ <div className="max-w-4xl w-full">
303
545
  <div className="mb-6">
304
546
  <h1 className="text-2xl font-semibold mb-1">Updates</h1>
305
547
  <p className="text-[#666]">
@@ -308,10 +550,74 @@ function UpdatesSettings() {
308
550
  </div>
309
551
 
310
552
  {checking && !versions ? (
311
- <div className="text-[#666]">Checking for updates...</div>
553
+ <div className="text-[#666]">Checking version info...</div>
312
554
  ) : error && !versions ? (
313
555
  <div className="text-red-400">{error}</div>
556
+ ) : versions?.isDocker ? (
557
+ /* Docker Environment */
558
+ <div className="space-y-6">
559
+ <div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-4">
560
+ <div className="flex items-center gap-2 text-blue-400 mb-2">
561
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
562
+ <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"/>
563
+ </svg>
564
+ <span className="font-medium">Docker Environment</span>
565
+ </div>
566
+ <p className="text-sm text-[#888]">
567
+ Updates are automatic when you pull a new image version.
568
+ </p>
569
+ </div>
570
+
571
+ {/* Current Version */}
572
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-5">
573
+ <div className="flex items-center justify-between mb-4">
574
+ <div>
575
+ <h3 className="font-medium text-lg">Current Version</h3>
576
+ <p className="text-sm text-[#666]">apteva + agent binary</p>
577
+ </div>
578
+ <div className="text-right">
579
+ <div className="text-xl font-mono">v{versions.apteva.installed || "?"}</div>
580
+ </div>
581
+ </div>
582
+
583
+ {hasAnyUpdate ? (
584
+ <div className="bg-[#f97316]/10 border border-[#f97316]/30 rounded-lg p-4">
585
+ <p className="text-sm text-[#888] mb-3">
586
+ A newer version (v{versions.apteva.latest}) is available. To update:
587
+ </p>
588
+ <div className="space-y-2">
589
+ <code className="block bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#888]">
590
+ docker pull apteva/apteva:latest
591
+ </code>
592
+ <code className="block bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#888]">
593
+ docker compose up -d
594
+ </code>
595
+ </div>
596
+ <button
597
+ onClick={() => {
598
+ navigator.clipboard.writeText("docker pull apteva/apteva:latest && docker compose up -d");
599
+ setCopied("docker");
600
+ setTimeout(() => setCopied(null), 2000);
601
+ }}
602
+ className="mt-3 px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#222] rounded text-sm"
603
+ >
604
+ {copied === "docker" ? "Copied!" : "Copy commands"}
605
+ </button>
606
+ </div>
607
+ ) : (
608
+ <div className="flex items-center gap-2 text-green-400 text-sm">
609
+ <CheckIcon className="w-4 h-4" />
610
+ Up to date
611
+ </div>
612
+ )}
613
+ </div>
614
+
615
+ <p className="text-xs text-[#555]">
616
+ Your data is stored in a Docker volume and persists across updates.
617
+ </p>
618
+ </div>
314
619
  ) : versions ? (
620
+ /* Non-Docker Environment */
315
621
  <div className="space-y-6">
316
622
  {updateSuccess && (
317
623
  <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 text-green-400">
@@ -441,18 +747,22 @@ function ProviderKeyCard({
441
747
  <div className={`bg-[#111] border rounded-lg p-4 ${
442
748
  provider.hasKey ? 'border-green-500/20' : 'border-[#1a1a1a]'
443
749
  }`}>
444
- <div className="flex items-center justify-between mb-2">
445
- <div>
750
+ <div className="flex items-start justify-between gap-2 mb-2">
751
+ <div className="min-w-0">
446
752
  <h3 className="font-medium">{provider.name}</h3>
447
- <p className="text-sm text-[#666]">{provider.models.length} models</p>
753
+ <p className="text-sm text-[#666] truncate">
754
+ {provider.type === "integration"
755
+ ? (provider.description || "MCP integration")
756
+ : `${provider.models.length} models`}
757
+ </p>
448
758
  </div>
449
759
  {provider.hasKey ? (
450
- <span className="text-green-400 text-xs flex items-center gap-1 bg-green-500/10 px-2 py-1 rounded">
760
+ <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
761
  <CheckIcon className="w-3 h-3" />
452
762
  {provider.keyHint}
453
763
  </span>
454
764
  ) : (
455
- <span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded">
765
+ <span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded whitespace-nowrap flex-shrink-0">
456
766
  Not configured
457
767
  </span>
458
768
  )}
@@ -636,13 +946,15 @@ function IntegrationKeyCard({
636
946
  }
637
947
 
638
948
  function DataSettings() {
949
+ const { authFetch } = useAuth();
639
950
  const [clearing, setClearing] = useState(false);
640
951
  const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
641
952
  const [eventCount, setEventCount] = useState<number | null>(null);
953
+ const { confirm, ConfirmDialog } = useConfirm();
642
954
 
643
955
  const fetchStats = async () => {
644
956
  try {
645
- const res = await fetch("/api/telemetry/stats");
957
+ const res = await authFetch("/api/telemetry/stats");
646
958
  const data = await res.json();
647
959
  setEventCount(data.stats?.total_events || 0);
648
960
  } catch {
@@ -655,15 +967,14 @@ function DataSettings() {
655
967
  }, []);
656
968
 
657
969
  const clearTelemetry = async () => {
658
- if (!confirm("Are you sure you want to delete all telemetry data? This cannot be undone.")) {
659
- return;
660
- }
970
+ const confirmed = await confirm("Are you sure you want to delete all telemetry data? This cannot be undone.", { confirmText: "Clear All", title: "Clear Telemetry Data" });
971
+ if (!confirmed) return;
661
972
 
662
973
  setClearing(true);
663
974
  setMessage(null);
664
975
 
665
976
  try {
666
- const res = await fetch("/api/telemetry/clear", { method: "POST" });
977
+ const res = await authFetch("/api/telemetry/clear", { method: "POST" });
667
978
  const data = await res.json();
668
979
 
669
980
  if (res.ok) {
@@ -680,7 +991,9 @@ function DataSettings() {
680
991
  };
681
992
 
682
993
  return (
683
- <div className="max-w-2xl">
994
+ <>
995
+ {ConfirmDialog}
996
+ <div className="max-w-4xl w-full">
684
997
  <div className="mb-6">
685
998
  <h1 className="text-2xl font-semibold mb-1">Data Management</h1>
686
999
  <p className="text-[#666]">Manage stored data and telemetry.</p>
@@ -713,5 +1026,6 @@ function DataSettings() {
713
1026
  </button>
714
1027
  </div>
715
1028
  </div>
1029
+ </>
716
1030
  );
717
1031
  }