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,5 +1,5 @@
1
- import React, { useState, useEffect } from "react";
2
- import { useAgentActivity } from "../../context";
1
+ import React, { useState, useEffect, useCallback, useMemo } from "react";
2
+ import { useAgentActivity, useAuth, useProjects } from "../../context";
3
3
  import type { Agent, Provider, Route, DashboardStats, Task } from "../../types";
4
4
 
5
5
  interface DashboardProps {
@@ -19,20 +19,32 @@ export function Dashboard({
19
19
  onNavigate,
20
20
  onSelectAgent,
21
21
  }: DashboardProps) {
22
+ const { authFetch } = useAuth();
23
+ const { currentProjectId } = useProjects();
22
24
  const [stats, setStats] = useState<DashboardStats | null>(null);
23
25
  const [recentTasks, setRecentTasks] = useState<Task[]>([]);
24
26
 
25
- useEffect(() => {
26
- fetchDashboardData();
27
- const interval = setInterval(fetchDashboardData, 10000);
28
- return () => clearInterval(interval);
29
- }, []);
27
+ // Filter agents by current project
28
+ const filteredAgents = useMemo(() => {
29
+ if (!currentProjectId) return agents; // "All Projects"
30
+ if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
31
+ return agents.filter(a => a.projectId === currentProjectId);
32
+ }, [agents, currentProjectId]);
30
33
 
31
- const fetchDashboardData = async () => {
34
+ const filteredRunningCount = useMemo(() => {
35
+ return filteredAgents.filter(a => a.status === "running").length;
36
+ }, [filteredAgents]);
37
+
38
+ // Get agent IDs for filtering tasks
39
+ const projectAgentIds = useMemo(() => {
40
+ return new Set(filteredAgents.map(a => a.id));
41
+ }, [filteredAgents]);
42
+
43
+ const fetchDashboardData = useCallback(async () => {
32
44
  try {
33
45
  const [dashRes, tasksRes] = await Promise.all([
34
- fetch("/api/dashboard"),
35
- fetch("/api/tasks?status=all"),
46
+ authFetch("/api/dashboard"),
47
+ authFetch("/api/tasks?status=all"),
36
48
  ]);
37
49
 
38
50
  if (dashRes.ok) {
@@ -47,15 +59,38 @@ export function Dashboard({
47
59
  } catch (e) {
48
60
  console.error("Failed to fetch dashboard data:", e);
49
61
  }
50
- };
62
+ }, [authFetch]);
51
63
 
52
- const taskStats = stats?.tasks || { total: 0, pending: 0, running: 0, completed: 0 };
64
+ useEffect(() => {
65
+ fetchDashboardData();
66
+ const interval = setInterval(fetchDashboardData, 10000);
67
+ return () => clearInterval(interval);
68
+ }, [fetchDashboardData]);
69
+
70
+ // Filter tasks by project agents
71
+ const filteredTasks = useMemo(() => {
72
+ if (!currentProjectId) return recentTasks;
73
+ return recentTasks.filter(t => projectAgentIds.has(t.agentId));
74
+ }, [recentTasks, currentProjectId, projectAgentIds]);
75
+
76
+ // Calculate task stats from filtered tasks
77
+ const taskStats = useMemo(() => {
78
+ if (!currentProjectId) {
79
+ return stats?.tasks || { total: 0, pending: 0, running: 0, completed: 0 };
80
+ }
81
+ // When filtering by project, calculate from filtered tasks
82
+ const total = filteredTasks.length;
83
+ const pending = filteredTasks.filter(t => t.status === "pending").length;
84
+ const running = filteredTasks.filter(t => t.status === "running").length;
85
+ const completed = filteredTasks.filter(t => t.status === "completed").length;
86
+ return { total, pending, running, completed };
87
+ }, [stats, currentProjectId, filteredTasks]);
53
88
 
54
89
  return (
55
90
  <div className="flex-1 overflow-auto p-6">
56
91
  {/* Stats Cards */}
57
92
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
58
- <StatCard label="Agents" value={agents.length} subValue={`${runningCount} running`} />
93
+ <StatCard label="Agents" value={filteredAgents.length} subValue={`${filteredRunningCount} running`} />
59
94
  <StatCard label="Tasks" value={taskStats.total} subValue={`${taskStats.pending} pending`} />
60
95
  <StatCard label="Completed" value={taskStats.completed} color="text-green-400" />
61
96
  <StatCard label="Providers" value={configuredProviders.length} color="text-[#f97316]" />
@@ -70,12 +105,12 @@ export function Dashboard({
70
105
  >
71
106
  {loading ? (
72
107
  <div className="p-4 text-center text-[#666]">Loading...</div>
73
- ) : agents.length === 0 ? (
108
+ ) : filteredAgents.length === 0 ? (
74
109
  <div className="p-4 text-center text-[#666]">No agents yet</div>
75
110
  ) : (
76
111
  <div className="divide-y divide-[#1a1a1a]">
77
- {agents.slice(0, 5).map((agent) => (
78
- <AgentListItem key={agent.id} agent={agent} onSelect={() => onSelectAgent(agent)} />
112
+ {filteredAgents.slice(0, 5).map((agent) => (
113
+ <AgentListItem key={agent.id} agent={agent} onSelect={() => onSelectAgent(agent)} showProject={!currentProjectId} />
79
114
  ))}
80
115
  </div>
81
116
  )}
@@ -87,14 +122,14 @@ export function Dashboard({
87
122
  actionLabel="View All"
88
123
  onAction={() => onNavigate("tasks")}
89
124
  >
90
- {recentTasks.length === 0 ? (
125
+ {filteredTasks.length === 0 ? (
91
126
  <div className="p-4 text-center text-[#666]">
92
127
  <p>No tasks yet</p>
93
128
  <p className="text-sm text-[#444] mt-1">Tasks will appear when agents create them</p>
94
129
  </div>
95
130
  ) : (
96
131
  <div className="divide-y divide-[#1a1a1a]">
97
- {recentTasks.map((task) => (
132
+ {filteredTasks.map((task) => (
98
133
  <div
99
134
  key={`${task.agentId}-${task.id}`}
100
135
  className="px-4 py-3 flex items-center justify-between"
@@ -155,20 +190,33 @@ function DashboardCard({ title, actionLabel, onAction, children }: DashboardCard
155
190
  );
156
191
  }
157
192
 
158
- function AgentListItem({ agent, onSelect }: { agent: Agent; onSelect: () => void }) {
193
+ function AgentListItem({ agent, onSelect, showProject }: { agent: Agent; onSelect: () => void; showProject?: boolean }) {
159
194
  const { isActive } = useAgentActivity(agent.id);
195
+ const { projects } = useProjects();
196
+ const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
160
197
 
161
198
  return (
162
199
  <div
163
200
  onClick={onSelect}
164
201
  className="px-4 py-3 hover:bg-[#1a1a1a] cursor-pointer flex items-center justify-between"
165
202
  >
166
- <div>
203
+ <div className="flex-1 min-w-0">
167
204
  <p className="font-medium">{agent.name}</p>
168
- <p className="text-sm text-[#666]">{agent.provider}</p>
205
+ <div className="flex items-center gap-2 text-sm text-[#666]">
206
+ <span>{agent.provider}</span>
207
+ {showProject && project && (
208
+ <>
209
+ <span className="text-[#444]">·</span>
210
+ <span className="flex items-center gap-1">
211
+ <span className="w-2 h-2 rounded-full" style={{ backgroundColor: project.color }} />
212
+ {project.name}
213
+ </span>
214
+ </>
215
+ )}
216
+ </div>
169
217
  </div>
170
218
  <span
171
- className={`w-2 h-2 rounded-full ${
219
+ className={`w-2 h-2 rounded-full flex-shrink-0 ${
172
220
  agent.status === "running"
173
221
  ? isActive
174
222
  ? "bg-green-400 animate-pulse"
@@ -4,6 +4,9 @@ export { LoadingSpinner, Modal, Select, CheckIcon, CloseIcon, DashboardIcon, Age
4
4
  // Layout components
5
5
  export { Header, Sidebar, ErrorBanner } from "./layout";
6
6
 
7
+ // Auth components
8
+ export { LoginPage, CreateAccountStep } from "./auth";
9
+
7
10
  // Feature components
8
11
  export { OnboardingWizard } from "./onboarding";
9
12
  export { SettingsPage } from "./settings";
@@ -1,37 +1,154 @@
1
- import React from "react";
2
- import { useTelemetryContext } from "../../context";
1
+ import React, { useState } from "react";
2
+ import { useTelemetryContext, useAuth, useProjects } from "../../context";
3
+ import { MenuIcon, ChevronDownIcon } from "../common/Icons";
3
4
 
4
5
  interface HeaderProps {
5
- onNewAgent: () => void;
6
- canCreateAgent: boolean;
6
+ onMenuClick?: () => void;
7
7
  }
8
8
 
9
- export function Header({ onNewAgent, canCreateAgent }: HeaderProps) {
9
+ export function Header({ onMenuClick }: HeaderProps) {
10
10
  const { connected } = useTelemetryContext();
11
+ const { user, logout } = useAuth();
12
+ const { projects, currentProjectId, currentProject, setCurrentProjectId, unassignedCount } = useProjects();
13
+ const [showUserMenu, setShowUserMenu] = useState(false);
14
+ const [showProjectMenu, setShowProjectMenu] = useState(false);
15
+
16
+ const handleLogout = async () => {
17
+ await logout();
18
+ setShowUserMenu(false);
19
+ };
20
+
21
+ const handleProjectSelect = (projectId: string | null) => {
22
+ setCurrentProjectId(projectId);
23
+ setShowProjectMenu(false);
24
+ };
25
+
26
+ const getProjectLabel = () => {
27
+ if (currentProjectId === null) return "All Projects";
28
+ if (currentProjectId === "unassigned") return "Unassigned";
29
+ return currentProject?.name || "Select Project";
30
+ };
31
+
32
+ const getProjectColor = () => {
33
+ if (currentProjectId === null) return "#666";
34
+ if (currentProjectId === "unassigned") return "#888";
35
+ return currentProject?.color || "#6366f1";
36
+ };
11
37
 
12
38
  return (
13
- <header className="border-b border-[#1a1a1a] px-6 py-4 flex-shrink-0">
39
+ <header className="border-b border-[#1a1a1a] px-4 md:px-6 py-4 flex-shrink-0">
14
40
  <div className="flex items-center justify-between">
15
- <div className="flex items-center gap-2">
16
- <span className="text-[#f97316]">&gt;_</span>
17
- <span className="text-xl tracking-wider">apteva</span>
41
+ <div className="flex items-center gap-3">
42
+ {/* Hamburger menu button - mobile only */}
43
+ <button
44
+ onClick={onMenuClick}
45
+ className="p-2 -ml-2 text-[#666] hover:text-[#e0e0e0] transition md:hidden"
46
+ >
47
+ <MenuIcon />
48
+ </button>
49
+ <div className="flex items-center gap-2">
50
+ <span className="text-[#f97316]">&gt;_</span>
51
+ <span className="text-xl tracking-wider">apteva</span>
52
+ </div>
53
+
54
+ {/* Project Selector */}
55
+ {projects.length > 0 && (
56
+ <div className="relative ml-2 md:ml-4">
57
+ <button
58
+ onClick={() => setShowProjectMenu(!showProjectMenu)}
59
+ className="flex items-center gap-2 px-3 py-1.5 rounded border border-[#222] bg-[#111] hover:bg-[#1a1a1a] transition text-sm"
60
+ >
61
+ <span
62
+ className="w-2.5 h-2.5 rounded-full"
63
+ style={{ backgroundColor: getProjectColor() }}
64
+ />
65
+ <span className="hidden sm:inline max-w-[120px] md:max-w-[180px] truncate">
66
+ {getProjectLabel()}
67
+ </span>
68
+ <ChevronDownIcon />
69
+ </button>
70
+ {showProjectMenu && (
71
+ <div className="absolute left-0 top-full mt-1 w-56 bg-[#111] border border-[#222] rounded-lg shadow-xl z-50">
72
+ <div className="py-1 max-h-64 overflow-y-auto">
73
+ <button
74
+ onClick={() => handleProjectSelect(null)}
75
+ className={`w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-[#1a1a1a] transition ${
76
+ currentProjectId === null ? "bg-[#1a1a1a] text-[#f97316]" : ""
77
+ }`}
78
+ >
79
+ <span className="w-2.5 h-2.5 rounded-full bg-[#666]" />
80
+ All Projects
81
+ </button>
82
+ {projects.map(project => (
83
+ <button
84
+ key={project.id}
85
+ onClick={() => handleProjectSelect(project.id)}
86
+ className={`w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-[#1a1a1a] transition ${
87
+ currentProjectId === project.id ? "bg-[#1a1a1a] text-[#f97316]" : ""
88
+ }`}
89
+ >
90
+ <span
91
+ className="w-2.5 h-2.5 rounded-full flex-shrink-0"
92
+ style={{ backgroundColor: project.color }}
93
+ />
94
+ <span className="truncate">{project.name}</span>
95
+ <span className="ml-auto text-xs text-[#666]">{project.agentCount}</span>
96
+ </button>
97
+ ))}
98
+ {unassignedCount > 0 && (
99
+ <button
100
+ onClick={() => handleProjectSelect("unassigned")}
101
+ className={`w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-[#1a1a1a] transition ${
102
+ currentProjectId === "unassigned" ? "bg-[#1a1a1a] text-[#f97316]" : ""
103
+ }`}
104
+ >
105
+ <span className="w-2.5 h-2.5 rounded-full bg-[#888]" />
106
+ <span className="truncate">Unassigned</span>
107
+ <span className="ml-auto text-xs text-[#666]">{unassignedCount}</span>
108
+ </button>
109
+ )}
110
+ </div>
111
+ </div>
112
+ )}
113
+ </div>
114
+ )}
18
115
  </div>
19
- <div className="flex items-center gap-4">
116
+ <div className="flex items-center gap-3 md:gap-4">
20
117
  <div className="flex items-center gap-2">
21
118
  <span
22
119
  className={`w-2 h-2 rounded-full ${connected ? "bg-green-400" : "bg-red-400"}`}
23
120
  />
24
- <span className="text-xs text-[#666]">
121
+ <span className="text-xs text-[#666] hidden sm:inline">
25
122
  {connected ? "Live" : "Offline"}
26
123
  </span>
27
124
  </div>
28
- <button
29
- onClick={onNewAgent}
30
- disabled={!canCreateAgent}
31
- className="bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-2 rounded font-medium transition"
32
- >
33
- + New Agent
34
- </button>
125
+ {user && (
126
+ <div className="relative">
127
+ <button
128
+ onClick={() => setShowUserMenu(!showUserMenu)}
129
+ className="flex items-center gap-2 px-2 md:px-3 py-2 rounded hover:bg-[#1a1a1a] transition"
130
+ >
131
+ <div className="w-8 h-8 rounded-full bg-[#f97316] flex items-center justify-center text-black font-medium text-sm">
132
+ {user.username.charAt(0).toUpperCase()}
133
+ </div>
134
+ <span className="text-sm text-[#888] hidden sm:block">{user.username}</span>
135
+ </button>
136
+ {showUserMenu && (
137
+ <div className="absolute right-0 top-full mt-1 w-48 bg-[#111] border border-[#222] rounded-lg shadow-xl z-50">
138
+ <div className="px-4 py-3 border-b border-[#222]">
139
+ <p className="text-sm font-medium">{user.username}</p>
140
+ <p className="text-xs text-[#f97316] mt-1">{user.role}</p>
141
+ </div>
142
+ <button
143
+ onClick={handleLogout}
144
+ className="w-full px-4 py-2 text-left text-sm text-red-400 hover:bg-[#1a1a1a] transition"
145
+ >
146
+ Sign out
147
+ </button>
148
+ </div>
149
+ )}
150
+ </div>
151
+ )}
35
152
  </div>
36
153
  </div>
37
154
  </header>
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { DashboardIcon, AgentsIcon, TasksIcon, McpIcon, TelemetryIcon, SettingsIcon } from "../common/Icons";
2
+ import { DashboardIcon, AgentsIcon, TasksIcon, McpIcon, TelemetryIcon, SettingsIcon, CloseIcon } from "../common/Icons";
3
3
  import type { Route } from "../../types";
4
4
 
5
5
  interface SidebarProps {
@@ -7,52 +7,90 @@ interface SidebarProps {
7
7
  agentCount: number;
8
8
  taskCount?: number;
9
9
  onNavigate: (route: Route) => void;
10
+ isOpen?: boolean;
11
+ onClose?: () => void;
10
12
  }
11
13
 
12
- export function Sidebar({ route, agentCount, taskCount, onNavigate }: SidebarProps) {
14
+ export function Sidebar({ route, agentCount, taskCount, onNavigate, isOpen, onClose }: SidebarProps) {
15
+ const handleNavigate = (newRoute: Route) => {
16
+ onNavigate(newRoute);
17
+ onClose?.();
18
+ };
19
+
13
20
  return (
14
- <aside className="w-56 border-r border-[#1a1a1a] flex-shrink-0 p-4">
15
- <nav className="space-y-1">
16
- <NavButton
17
- icon={<DashboardIcon />}
18
- label="Dashboard"
19
- active={route === "dashboard"}
20
- onClick={() => onNavigate("dashboard")}
21
- />
22
- <NavButton
23
- icon={<AgentsIcon />}
24
- label="Agents"
25
- active={route === "agents"}
26
- onClick={() => onNavigate("agents")}
27
- badge={agentCount > 0 ? String(agentCount) : undefined}
28
- />
29
- <NavButton
30
- icon={<TasksIcon />}
31
- label="Tasks"
32
- active={route === "tasks"}
33
- onClick={() => onNavigate("tasks")}
34
- badge={taskCount && taskCount > 0 ? String(taskCount) : undefined}
35
- />
36
- <NavButton
37
- icon={<McpIcon />}
38
- label="MCP"
39
- active={route === "mcp"}
40
- onClick={() => onNavigate("mcp")}
41
- />
42
- <NavButton
43
- icon={<TelemetryIcon />}
44
- label="Telemetry"
45
- active={route === "telemetry"}
46
- onClick={() => onNavigate("telemetry")}
21
+ <>
22
+ {/* Mobile overlay backdrop */}
23
+ {isOpen && (
24
+ <div
25
+ className="fixed inset-0 bg-black/60 z-40 md:hidden"
26
+ onClick={onClose}
47
27
  />
48
- <NavButton
49
- icon={<SettingsIcon />}
50
- label="Settings"
51
- active={route === "settings"}
52
- onClick={() => onNavigate("settings")}
53
- />
54
- </nav>
55
- </aside>
28
+ )}
29
+
30
+ {/* Sidebar - hidden on mobile unless open, always visible on md+ */}
31
+ <aside
32
+ className={`
33
+ fixed inset-y-0 left-0 z-50 w-64 bg-[#0a0a0a] border-r border-[#1a1a1a] p-4 transform transition-transform duration-200 ease-in-out
34
+ md:relative md:w-56 md:translate-x-0 md:z-auto
35
+ ${isOpen ? "translate-x-0" : "-translate-x-full"}
36
+ `}
37
+ >
38
+ {/* Mobile header with close button */}
39
+ <div className="flex items-center justify-between mb-4 md:hidden">
40
+ <div className="flex items-center gap-2">
41
+ <span className="text-[#f97316]">&gt;_</span>
42
+ <span className="text-lg tracking-wider">apteva</span>
43
+ </div>
44
+ <button
45
+ onClick={onClose}
46
+ className="p-2 text-[#666] hover:text-[#e0e0e0] transition"
47
+ >
48
+ <CloseIcon />
49
+ </button>
50
+ </div>
51
+
52
+ <nav className="space-y-1">
53
+ <NavButton
54
+ icon={<DashboardIcon />}
55
+ label="Dashboard"
56
+ active={route === "dashboard"}
57
+ onClick={() => handleNavigate("dashboard")}
58
+ />
59
+ <NavButton
60
+ icon={<AgentsIcon />}
61
+ label="Agents"
62
+ active={route === "agents"}
63
+ onClick={() => handleNavigate("agents")}
64
+ badge={agentCount > 0 ? String(agentCount) : undefined}
65
+ />
66
+ <NavButton
67
+ icon={<TasksIcon />}
68
+ label="Tasks"
69
+ active={route === "tasks"}
70
+ onClick={() => handleNavigate("tasks")}
71
+ badge={taskCount && taskCount > 0 ? String(taskCount) : undefined}
72
+ />
73
+ <NavButton
74
+ icon={<McpIcon />}
75
+ label="MCP"
76
+ active={route === "mcp"}
77
+ onClick={() => handleNavigate("mcp")}
78
+ />
79
+ <NavButton
80
+ icon={<TelemetryIcon />}
81
+ label="Telemetry"
82
+ active={route === "telemetry"}
83
+ onClick={() => handleNavigate("telemetry")}
84
+ />
85
+ <NavButton
86
+ icon={<SettingsIcon />}
87
+ label="Settings"
88
+ active={route === "settings"}
89
+ onClick={() => handleNavigate("settings")}
90
+ />
91
+ </nav>
92
+ </aside>
93
+ </>
56
94
  );
57
95
  }
58
96