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.
Files changed (40) 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 +561 -32
  9. package/src/routes/api.ts +901 -35
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +46 -5
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +24 -22
  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 +70 -22
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +135 -18
  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 +320 -21
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/index.ts +5 -0
  34. package/src/web/hooks/useAgents.ts +18 -6
  35. package/src/web/hooks/useOnboarding.ts +20 -4
  36. package/src/web/hooks/useProviders.ts +15 -5
  37. package/src/web/icon.png +0 -0
  38. package/src/web/styles.css +12 -0
  39. package/src/web/types.ts +6 -0
  40. 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 fetch("/api/providers");
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 fetch(`/api/keys/${selectedProvider}/test`, {
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 fetch(`/api/keys/${selectedProvider}`, {
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 fetch(`/api/keys/${providerId}`, { method: "DELETE" });
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="max-w-4xl space-y-10">
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 fetch("/api/version");
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 fetch("/api/version/update", { method: "POST" });
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-2xl">
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 for updates...</div>
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-center justify-between mb-2">
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]">{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>
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 fetch("/api/telemetry/stats");
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 fetch("/api/telemetry/clear", { method: "POST" });
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-2xl">
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
- 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>}