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.
Files changed (41) hide show
  1. package/dist/App.hzbfeg94.js +217 -0
  2. package/dist/index.html +3 -1
  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 +570 -32
  9. package/src/routes/api.ts +913 -38
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +60 -8
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +30 -41
  14. package/src/web/components/agents/AgentPanel.tsx +751 -11
  15. package/src/web/components/agents/AgentsView.tsx +81 -9
  16. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  17. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  18. package/src/web/components/auth/LoginPage.tsx +91 -0
  19. package/src/web/components/auth/index.ts +2 -0
  20. package/src/web/components/common/Icons.tsx +48 -0
  21. package/src/web/components/common/Modal.tsx +1 -1
  22. package/src/web/components/dashboard/Dashboard.tsx +91 -31
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +145 -15
  25. package/src/web/components/layout/Sidebar.tsx +81 -43
  26. package/src/web/components/mcp/McpPage.tsx +261 -32
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  28. package/src/web/components/settings/SettingsPage.tsx +404 -18
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/TelemetryContext.tsx +98 -76
  34. package/src/web/context/index.ts +5 -0
  35. package/src/web/hooks/useAgents.ts +18 -6
  36. package/src/web/hooks/useOnboarding.ts +20 -4
  37. package/src/web/hooks/useProviders.ts +15 -5
  38. package/src/web/icon.png +0 -0
  39. package/src/web/styles.css +12 -0
  40. package/src/web/types.ts +6 -0
  41. 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 fetch("/api/providers");
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 fetch(`/api/keys/${selectedProvider}/test`, {
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 fetch(`/api/keys/${selectedProvider}`, {
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 fetch(`/api/keys/${providerId}`, { method: "DELETE" });
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="max-w-4xl space-y-10">
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 fetch("/api/version");
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 fetch("/api/version/update", { method: "POST" });
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-2xl">
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 for updates...</div>
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-center justify-between mb-2">
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]">{provider.models.length} models</p>
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
- useEffect(() => {
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 fetch(`/api/tasks?status=${filter}`);
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="flex items-center justify-between mb-6">
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>}