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.
- package/dist/App.hzbfeg94.js +217 -0
- package/dist/index.html +3 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +386 -0
- package/src/auth/middleware.ts +183 -0
- package/src/binary.ts +19 -1
- package/src/db.ts +570 -32
- package/src/routes/api.ts +913 -38
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +60 -8
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +30 -41
- package/src/web/components/agents/AgentPanel.tsx +751 -11
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/auth/CreateAccountStep.tsx +176 -0
- package/src/web/components/auth/LoginPage.tsx +91 -0
- package/src/web/components/auth/index.ts +2 -0
- package/src/web/components/common/Icons.tsx +48 -0
- package/src/web/components/common/Modal.tsx +1 -1
- package/src/web/components/dashboard/Dashboard.tsx +91 -31
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +145 -15
- package/src/web/components/layout/Sidebar.tsx +81 -43
- package/src/web/components/mcp/McpPage.tsx +261 -32
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +404 -18
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/TelemetryContext.tsx +98 -76
- package/src/web/context/index.ts +5 -0
- package/src/web/hooks/useAgents.ts +18 -6
- package/src/web/hooks/useOnboarding.ts +20 -4
- package/src/web/hooks/useProviders.ts +15 -5
- package/src/web/icon.png +0 -0
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +6 -0
- package/dist/App.0mzj9cz9.js +0 -213
package/src/db.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface AgentFeatures {
|
|
|
11
11
|
operator: boolean;
|
|
12
12
|
mcp: boolean;
|
|
13
13
|
realtime: boolean;
|
|
14
|
+
files: boolean;
|
|
15
|
+
agents: boolean;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export const DEFAULT_FEATURES: AgentFeatures = {
|
|
@@ -20,6 +22,8 @@ export const DEFAULT_FEATURES: AgentFeatures = {
|
|
|
20
22
|
operator: false,
|
|
21
23
|
mcp: false,
|
|
22
24
|
realtime: false,
|
|
25
|
+
files: false,
|
|
26
|
+
agents: false,
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
export interface Agent {
|
|
@@ -32,6 +36,25 @@ export interface Agent {
|
|
|
32
36
|
port: number | null;
|
|
33
37
|
features: AgentFeatures;
|
|
34
38
|
mcp_servers: string[]; // Array of MCP server IDs
|
|
39
|
+
project_id: string | null; // Optional project grouping
|
|
40
|
+
created_at: string;
|
|
41
|
+
updated_at: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Project {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
description: string | null;
|
|
48
|
+
color: string; // Hex color for UI display
|
|
49
|
+
created_at: string;
|
|
50
|
+
updated_at: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ProjectRow {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
description: string | null;
|
|
57
|
+
color: string;
|
|
35
58
|
created_at: string;
|
|
36
59
|
updated_at: string;
|
|
37
60
|
}
|
|
@@ -46,6 +69,7 @@ export interface AgentRow {
|
|
|
46
69
|
port: number | null;
|
|
47
70
|
features: string | null;
|
|
48
71
|
mcp_servers: string | null;
|
|
72
|
+
project_id: string | null;
|
|
49
73
|
created_at: string;
|
|
50
74
|
updated_at: string;
|
|
51
75
|
}
|
|
@@ -272,6 +296,58 @@ function runMigrations() {
|
|
|
272
296
|
CREATE INDEX IF NOT EXISTS idx_telemetry_trace ON telemetry_events(trace_id);
|
|
273
297
|
`,
|
|
274
298
|
},
|
|
299
|
+
{
|
|
300
|
+
name: "010_create_users",
|
|
301
|
+
sql: `
|
|
302
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
303
|
+
id TEXT PRIMARY KEY,
|
|
304
|
+
username TEXT UNIQUE NOT NULL,
|
|
305
|
+
password_hash TEXT NOT NULL,
|
|
306
|
+
email TEXT,
|
|
307
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
308
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
309
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
310
|
+
last_login_at TEXT
|
|
311
|
+
);
|
|
312
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
313
|
+
`,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "011_create_sessions",
|
|
317
|
+
sql: `
|
|
318
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
319
|
+
id TEXT PRIMARY KEY,
|
|
320
|
+
user_id TEXT NOT NULL,
|
|
321
|
+
refresh_token_hash TEXT NOT NULL,
|
|
322
|
+
expires_at TEXT NOT NULL,
|
|
323
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
324
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
325
|
+
);
|
|
326
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
327
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
328
|
+
`,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "012_create_projects",
|
|
332
|
+
sql: `
|
|
333
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
334
|
+
id TEXT PRIMARY KEY,
|
|
335
|
+
name TEXT NOT NULL,
|
|
336
|
+
description TEXT,
|
|
337
|
+
color TEXT NOT NULL DEFAULT '#6366f1',
|
|
338
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
339
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
340
|
+
);
|
|
341
|
+
CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
|
|
342
|
+
`,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "013_add_agent_project_id",
|
|
346
|
+
sql: `
|
|
347
|
+
ALTER TABLE agents ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
|
|
348
|
+
CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
|
|
349
|
+
`,
|
|
350
|
+
},
|
|
275
351
|
];
|
|
276
352
|
|
|
277
353
|
// Check which migrations have been applied
|
|
@@ -289,6 +365,57 @@ function runMigrations() {
|
|
|
289
365
|
db.run("INSERT INTO migrations (name) VALUES (?)", [migration.name]);
|
|
290
366
|
}
|
|
291
367
|
}
|
|
368
|
+
|
|
369
|
+
// Schema upgrade migrations (check actual table structure)
|
|
370
|
+
runSchemaUpgrades();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Handle schema changes that require checking actual table structure
|
|
374
|
+
function runSchemaUpgrades() {
|
|
375
|
+
// Check if users table needs migration from email-based to username-based
|
|
376
|
+
const tableInfo = db.query("PRAGMA table_info(users)").all() as { name: string }[];
|
|
377
|
+
const columns = new Set(tableInfo.map(c => c.name));
|
|
378
|
+
|
|
379
|
+
// Old schema has 'email' as required + 'name', new schema has 'username' + optional 'email'
|
|
380
|
+
if (columns.has("name") && !columns.has("username")) {
|
|
381
|
+
console.log("[db] Migrating users table from email-based to username-based auth...");
|
|
382
|
+
|
|
383
|
+
// Get existing users
|
|
384
|
+
const existingUsers = db.query("SELECT * FROM users").all() as any[];
|
|
385
|
+
|
|
386
|
+
// Drop old table and indexes
|
|
387
|
+
db.run("DROP INDEX IF EXISTS idx_users_email");
|
|
388
|
+
db.run("DROP TABLE users");
|
|
389
|
+
|
|
390
|
+
// Create new schema
|
|
391
|
+
db.run(`
|
|
392
|
+
CREATE TABLE users (
|
|
393
|
+
id TEXT PRIMARY KEY,
|
|
394
|
+
username TEXT UNIQUE NOT NULL,
|
|
395
|
+
password_hash TEXT NOT NULL,
|
|
396
|
+
email TEXT,
|
|
397
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
398
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
399
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
400
|
+
last_login_at TEXT
|
|
401
|
+
)
|
|
402
|
+
`);
|
|
403
|
+
db.run("CREATE UNIQUE INDEX idx_users_username ON users(username)");
|
|
404
|
+
|
|
405
|
+
// Migrate existing users (use part before @ in email as username)
|
|
406
|
+
for (const user of existingUsers) {
|
|
407
|
+
const username = user.email.split("@")[0].replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 20);
|
|
408
|
+
db.run(
|
|
409
|
+
`INSERT INTO users (id, username, password_hash, email, role, created_at, updated_at, last_login_at)
|
|
410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
411
|
+
[user.id, username, user.password_hash, user.email, user.role, user.created_at, user.updated_at, user.last_login_at]
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (existingUsers.length > 0) {
|
|
416
|
+
console.log(`[db] Migrated ${existingUsers.length} user(s). Usernames derived from email addresses.`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
292
419
|
}
|
|
293
420
|
|
|
294
421
|
// Agent CRUD operations
|
|
@@ -299,10 +426,10 @@ export const AgentDB = {
|
|
|
299
426
|
const featuresJson = JSON.stringify(agent.features || DEFAULT_FEATURES);
|
|
300
427
|
const mcpServersJson = JSON.stringify(agent.mcp_servers || []);
|
|
301
428
|
const stmt = db.prepare(`
|
|
302
|
-
INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, status, port, created_at, updated_at)
|
|
303
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?, ?)
|
|
429
|
+
INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, project_id, status, port, created_at, updated_at)
|
|
430
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?, ?)
|
|
304
431
|
`);
|
|
305
|
-
stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, now, now);
|
|
432
|
+
stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, agent.project_id || null, now, now);
|
|
306
433
|
return this.findById(agent.id)!;
|
|
307
434
|
},
|
|
308
435
|
|
|
@@ -364,6 +491,10 @@ export const AgentDB = {
|
|
|
364
491
|
fields.push("mcp_servers = ?");
|
|
365
492
|
values.push(JSON.stringify(updates.mcp_servers));
|
|
366
493
|
}
|
|
494
|
+
if (updates.project_id !== undefined) {
|
|
495
|
+
fields.push("project_id = ?");
|
|
496
|
+
values.push(updates.project_id);
|
|
497
|
+
}
|
|
367
498
|
|
|
368
499
|
if (fields.length > 0) {
|
|
369
500
|
fields.push("updated_at = ?");
|
|
@@ -376,6 +507,16 @@ export const AgentDB = {
|
|
|
376
507
|
return this.findById(id);
|
|
377
508
|
},
|
|
378
509
|
|
|
510
|
+
// Find agents by project
|
|
511
|
+
findByProject(projectId: string | null): Agent[] {
|
|
512
|
+
if (projectId === null) {
|
|
513
|
+
const rows = db.query("SELECT * FROM agents WHERE project_id IS NULL ORDER BY created_at DESC").all() as AgentRow[];
|
|
514
|
+
return rows.map(rowToAgent);
|
|
515
|
+
}
|
|
516
|
+
const rows = db.query("SELECT * FROM agents WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as AgentRow[];
|
|
517
|
+
return rows.map(rowToAgent);
|
|
518
|
+
},
|
|
519
|
+
|
|
379
520
|
// Delete agent
|
|
380
521
|
delete(id: string): boolean {
|
|
381
522
|
const result = db.run("DELETE FROM agents WHERE id = ?", [id]);
|
|
@@ -405,6 +546,107 @@ export const AgentDB = {
|
|
|
405
546
|
},
|
|
406
547
|
};
|
|
407
548
|
|
|
549
|
+
// Project CRUD operations
|
|
550
|
+
export const ProjectDB = {
|
|
551
|
+
// Create a new project
|
|
552
|
+
create(project: { name: string; description?: string | null; color?: string }): Project {
|
|
553
|
+
const id = generateId();
|
|
554
|
+
const now = new Date().toISOString();
|
|
555
|
+
const color = project.color || "#6366f1";
|
|
556
|
+
|
|
557
|
+
db.run(
|
|
558
|
+
`INSERT INTO projects (id, name, description, color, created_at, updated_at)
|
|
559
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
560
|
+
[id, project.name, project.description || null, color, now, now]
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
return this.findById(id)!;
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
// Find project by ID
|
|
567
|
+
findById(id: string): Project | null {
|
|
568
|
+
const row = db.query("SELECT * FROM projects WHERE id = ?").get(id) as ProjectRow | null;
|
|
569
|
+
return row ? rowToProject(row) : null;
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
// Get all projects
|
|
573
|
+
findAll(): Project[] {
|
|
574
|
+
const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all() as ProjectRow[];
|
|
575
|
+
return rows.map(rowToProject);
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
// Update project
|
|
579
|
+
update(id: string, updates: Partial<Omit<Project, "id" | "created_at">>): Project | null {
|
|
580
|
+
const project = this.findById(id);
|
|
581
|
+
if (!project) return null;
|
|
582
|
+
|
|
583
|
+
const fields: string[] = [];
|
|
584
|
+
const values: unknown[] = [];
|
|
585
|
+
|
|
586
|
+
if (updates.name !== undefined) {
|
|
587
|
+
fields.push("name = ?");
|
|
588
|
+
values.push(updates.name);
|
|
589
|
+
}
|
|
590
|
+
if (updates.description !== undefined) {
|
|
591
|
+
fields.push("description = ?");
|
|
592
|
+
values.push(updates.description);
|
|
593
|
+
}
|
|
594
|
+
if (updates.color !== undefined) {
|
|
595
|
+
fields.push("color = ?");
|
|
596
|
+
values.push(updates.color);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (fields.length > 0) {
|
|
600
|
+
fields.push("updated_at = ?");
|
|
601
|
+
values.push(new Date().toISOString());
|
|
602
|
+
values.push(id);
|
|
603
|
+
|
|
604
|
+
db.run(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`, values);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return this.findById(id);
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
// Delete project (agents will have project_id set to NULL)
|
|
611
|
+
delete(id: string): boolean {
|
|
612
|
+
const result = db.run("DELETE FROM projects WHERE id = ?", [id]);
|
|
613
|
+
return result.changes > 0;
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
// Count projects
|
|
617
|
+
count(): number {
|
|
618
|
+
const row = db.query("SELECT COUNT(*) as count FROM projects").get() as { count: number };
|
|
619
|
+
return row.count;
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
// Get agent count per project
|
|
623
|
+
getAgentCounts(): Map<string | null, number> {
|
|
624
|
+
const rows = db.query(`
|
|
625
|
+
SELECT project_id, COUNT(*) as count
|
|
626
|
+
FROM agents
|
|
627
|
+
GROUP BY project_id
|
|
628
|
+
`).all() as { project_id: string | null; count: number }[];
|
|
629
|
+
|
|
630
|
+
const counts = new Map<string | null, number>();
|
|
631
|
+
for (const row of rows) {
|
|
632
|
+
counts.set(row.project_id, row.count);
|
|
633
|
+
}
|
|
634
|
+
return counts;
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Helper to convert DB row to Project type
|
|
639
|
+
function rowToProject(row: ProjectRow): Project {
|
|
640
|
+
return {
|
|
641
|
+
id: row.id,
|
|
642
|
+
name: row.name,
|
|
643
|
+
description: row.description,
|
|
644
|
+
color: row.color,
|
|
645
|
+
created_at: row.created_at,
|
|
646
|
+
updated_at: row.updated_at,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
408
650
|
// Thread CRUD operations
|
|
409
651
|
export const ThreadDB = {
|
|
410
652
|
create(id: string, agentId: string, title?: string): void {
|
|
@@ -493,6 +735,7 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
493
735
|
port: row.port,
|
|
494
736
|
features,
|
|
495
737
|
mcp_servers,
|
|
738
|
+
project_id: row.project_id,
|
|
496
739
|
created_at: row.created_at,
|
|
497
740
|
updated_at: row.updated_at,
|
|
498
741
|
};
|
|
@@ -695,6 +938,45 @@ function rowToMcpServer(row: McpServerRow): McpServer {
|
|
|
695
938
|
}
|
|
696
939
|
|
|
697
940
|
// Telemetry Event types
|
|
941
|
+
// User types
|
|
942
|
+
export interface User {
|
|
943
|
+
id: string;
|
|
944
|
+
username: string;
|
|
945
|
+
password_hash: string;
|
|
946
|
+
email: string | null; // Optional, for password recovery only
|
|
947
|
+
role: "admin" | "user";
|
|
948
|
+
created_at: string;
|
|
949
|
+
updated_at: string;
|
|
950
|
+
last_login_at: string | null;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
export interface UserRow {
|
|
954
|
+
id: string;
|
|
955
|
+
username: string;
|
|
956
|
+
password_hash: string;
|
|
957
|
+
email: string | null;
|
|
958
|
+
role: string;
|
|
959
|
+
created_at: string;
|
|
960
|
+
updated_at: string;
|
|
961
|
+
last_login_at: string | null;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export interface Session {
|
|
965
|
+
id: string;
|
|
966
|
+
user_id: string;
|
|
967
|
+
refresh_token_hash: string;
|
|
968
|
+
expires_at: string;
|
|
969
|
+
created_at: string;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
export interface SessionRow {
|
|
973
|
+
id: string;
|
|
974
|
+
user_id: string;
|
|
975
|
+
refresh_token_hash: string;
|
|
976
|
+
expires_at: string;
|
|
977
|
+
created_at: string;
|
|
978
|
+
}
|
|
979
|
+
|
|
698
980
|
export interface TelemetryEvent {
|
|
699
981
|
id: string;
|
|
700
982
|
agent_id: string;
|
|
@@ -779,6 +1061,7 @@ export const TelemetryDB = {
|
|
|
779
1061
|
// Query events with filters
|
|
780
1062
|
query(filters: {
|
|
781
1063
|
agent_id?: string;
|
|
1064
|
+
project_id?: string | null; // Filter by project (null = unassigned agents)
|
|
782
1065
|
category?: string;
|
|
783
1066
|
level?: string;
|
|
784
1067
|
trace_id?: string;
|
|
@@ -791,27 +1074,35 @@ export const TelemetryDB = {
|
|
|
791
1074
|
const params: unknown[] = [];
|
|
792
1075
|
|
|
793
1076
|
if (filters.agent_id) {
|
|
794
|
-
conditions.push("agent_id = ?");
|
|
1077
|
+
conditions.push("t.agent_id = ?");
|
|
795
1078
|
params.push(filters.agent_id);
|
|
796
1079
|
}
|
|
1080
|
+
if (filters.project_id !== undefined) {
|
|
1081
|
+
if (filters.project_id === null) {
|
|
1082
|
+
conditions.push("a.project_id IS NULL");
|
|
1083
|
+
} else {
|
|
1084
|
+
conditions.push("a.project_id = ?");
|
|
1085
|
+
params.push(filters.project_id);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
797
1088
|
if (filters.category) {
|
|
798
|
-
conditions.push("category = ?");
|
|
1089
|
+
conditions.push("t.category = ?");
|
|
799
1090
|
params.push(filters.category);
|
|
800
1091
|
}
|
|
801
1092
|
if (filters.level) {
|
|
802
|
-
conditions.push("level = ?");
|
|
1093
|
+
conditions.push("t.level = ?");
|
|
803
1094
|
params.push(filters.level);
|
|
804
1095
|
}
|
|
805
1096
|
if (filters.trace_id) {
|
|
806
|
-
conditions.push("trace_id = ?");
|
|
1097
|
+
conditions.push("t.trace_id = ?");
|
|
807
1098
|
params.push(filters.trace_id);
|
|
808
1099
|
}
|
|
809
1100
|
if (filters.since) {
|
|
810
|
-
conditions.push("timestamp >= ?");
|
|
1101
|
+
conditions.push("t.timestamp >= ?");
|
|
811
1102
|
params.push(filters.since);
|
|
812
1103
|
}
|
|
813
1104
|
if (filters.until) {
|
|
814
|
-
conditions.push("timestamp <= ?");
|
|
1105
|
+
conditions.push("t.timestamp <= ?");
|
|
815
1106
|
params.push(filters.until);
|
|
816
1107
|
}
|
|
817
1108
|
|
|
@@ -819,7 +1110,11 @@ export const TelemetryDB = {
|
|
|
819
1110
|
const limit = filters.limit || 100;
|
|
820
1111
|
const offset = filters.offset || 0;
|
|
821
1112
|
|
|
822
|
-
|
|
1113
|
+
// Join with agents table when filtering by project
|
|
1114
|
+
const needsJoin = filters.project_id !== undefined;
|
|
1115
|
+
const sql = needsJoin
|
|
1116
|
+
? `SELECT t.* FROM telemetry_events t JOIN agents a ON t.agent_id = a.id ${where} ORDER BY t.timestamp DESC LIMIT ? OFFSET ?`
|
|
1117
|
+
: `SELECT * FROM telemetry_events t ${where} ORDER BY t.timestamp DESC LIMIT ? OFFSET ?`;
|
|
823
1118
|
params.push(limit, offset);
|
|
824
1119
|
|
|
825
1120
|
const rows = db.query(sql).all(...params) as TelemetryEventRow[];
|
|
@@ -829,6 +1124,7 @@ export const TelemetryDB = {
|
|
|
829
1124
|
// Get usage stats
|
|
830
1125
|
getUsage(filters: {
|
|
831
1126
|
agent_id?: string;
|
|
1127
|
+
project_id?: string | null;
|
|
832
1128
|
since?: string;
|
|
833
1129
|
until?: string;
|
|
834
1130
|
group_by?: "agent" | "day";
|
|
@@ -843,17 +1139,26 @@ export const TelemetryDB = {
|
|
|
843
1139
|
}> {
|
|
844
1140
|
const conditions: string[] = [];
|
|
845
1141
|
const params: unknown[] = [];
|
|
1142
|
+
const needsJoin = filters.project_id !== undefined;
|
|
846
1143
|
|
|
847
1144
|
if (filters.agent_id) {
|
|
848
|
-
conditions.push("agent_id = ?");
|
|
1145
|
+
conditions.push("t.agent_id = ?");
|
|
849
1146
|
params.push(filters.agent_id);
|
|
850
1147
|
}
|
|
1148
|
+
if (filters.project_id !== undefined) {
|
|
1149
|
+
if (filters.project_id === null) {
|
|
1150
|
+
conditions.push("a.project_id IS NULL");
|
|
1151
|
+
} else {
|
|
1152
|
+
conditions.push("a.project_id = ?");
|
|
1153
|
+
params.push(filters.project_id);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
851
1156
|
if (filters.since) {
|
|
852
|
-
conditions.push("timestamp >= ?");
|
|
1157
|
+
conditions.push("t.timestamp >= ?");
|
|
853
1158
|
params.push(filters.since);
|
|
854
1159
|
}
|
|
855
1160
|
if (filters.until) {
|
|
856
|
-
conditions.push("timestamp <= ?");
|
|
1161
|
+
conditions.push("t.timestamp <= ?");
|
|
857
1162
|
params.push(filters.until);
|
|
858
1163
|
}
|
|
859
1164
|
|
|
@@ -863,22 +1168,26 @@ export const TelemetryDB = {
|
|
|
863
1168
|
let selectFields = "";
|
|
864
1169
|
|
|
865
1170
|
if (filters.group_by === "day") {
|
|
866
|
-
groupBy = "GROUP BY date(timestamp)";
|
|
867
|
-
selectFields = "date(timestamp) as date,";
|
|
1171
|
+
groupBy = "GROUP BY date(t.timestamp)";
|
|
1172
|
+
selectFields = "date(t.timestamp) as date,";
|
|
868
1173
|
} else if (filters.group_by === "agent") {
|
|
869
|
-
groupBy = "GROUP BY agent_id";
|
|
870
|
-
selectFields = "agent_id,";
|
|
1174
|
+
groupBy = "GROUP BY t.agent_id";
|
|
1175
|
+
selectFields = "t.agent_id as agent_id,";
|
|
871
1176
|
}
|
|
872
1177
|
|
|
1178
|
+
const fromClause = needsJoin
|
|
1179
|
+
? "FROM telemetry_events t JOIN agents a ON t.agent_id = a.id"
|
|
1180
|
+
: "FROM telemetry_events t";
|
|
1181
|
+
|
|
873
1182
|
const sql = `
|
|
874
1183
|
SELECT
|
|
875
1184
|
${selectFields}
|
|
876
|
-
COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.input_tokens') ELSE 0 END), 0) as input_tokens,
|
|
877
|
-
COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.output_tokens') ELSE 0 END), 0) as output_tokens,
|
|
878
|
-
COALESCE(SUM(CASE WHEN category = 'LLM' THEN 1 ELSE 0 END), 0) as llm_calls,
|
|
879
|
-
COALESCE(SUM(CASE WHEN category = 'TOOL' THEN 1 ELSE 0 END), 0) as tool_calls,
|
|
880
|
-
COALESCE(SUM(CASE WHEN level = 'error' THEN 1 ELSE 0 END), 0) as errors
|
|
881
|
-
|
|
1185
|
+
COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.input_tokens') ELSE 0 END), 0) as input_tokens,
|
|
1186
|
+
COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as output_tokens,
|
|
1187
|
+
COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN 1 ELSE 0 END), 0) as llm_calls,
|
|
1188
|
+
COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as tool_calls,
|
|
1189
|
+
COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as errors
|
|
1190
|
+
${fromClause}
|
|
882
1191
|
${where}
|
|
883
1192
|
${groupBy}
|
|
884
1193
|
`;
|
|
@@ -895,7 +1204,7 @@ export const TelemetryDB = {
|
|
|
895
1204
|
},
|
|
896
1205
|
|
|
897
1206
|
// Get summary stats
|
|
898
|
-
getStats(agentId?: string): {
|
|
1207
|
+
getStats(filters: { agentId?: string; projectId?: string | null } = {}): {
|
|
899
1208
|
total_events: number;
|
|
900
1209
|
total_llm_calls: number;
|
|
901
1210
|
total_tool_calls: number;
|
|
@@ -903,18 +1212,37 @@ export const TelemetryDB = {
|
|
|
903
1212
|
total_input_tokens: number;
|
|
904
1213
|
total_output_tokens: number;
|
|
905
1214
|
} {
|
|
906
|
-
const
|
|
907
|
-
const params
|
|
1215
|
+
const conditions: string[] = [];
|
|
1216
|
+
const params: unknown[] = [];
|
|
1217
|
+
const needsJoin = filters.projectId !== undefined;
|
|
1218
|
+
|
|
1219
|
+
if (filters.agentId) {
|
|
1220
|
+
conditions.push("t.agent_id = ?");
|
|
1221
|
+
params.push(filters.agentId);
|
|
1222
|
+
}
|
|
1223
|
+
if (filters.projectId !== undefined) {
|
|
1224
|
+
if (filters.projectId === null) {
|
|
1225
|
+
conditions.push("a.project_id IS NULL");
|
|
1226
|
+
} else {
|
|
1227
|
+
conditions.push("a.project_id = ?");
|
|
1228
|
+
params.push(filters.projectId);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1233
|
+
const fromClause = needsJoin
|
|
1234
|
+
? "FROM telemetry_events t JOIN agents a ON t.agent_id = a.id"
|
|
1235
|
+
: "FROM telemetry_events t";
|
|
908
1236
|
|
|
909
1237
|
const sql = `
|
|
910
1238
|
SELECT
|
|
911
1239
|
COUNT(*) as total_events,
|
|
912
|
-
COALESCE(SUM(CASE WHEN category = 'LLM' THEN 1 ELSE 0 END), 0) as total_llm_calls,
|
|
913
|
-
COALESCE(SUM(CASE WHEN category = 'TOOL' THEN 1 ELSE 0 END), 0) as total_tool_calls,
|
|
914
|
-
COALESCE(SUM(CASE WHEN level = 'error' THEN 1 ELSE 0 END), 0) as total_errors,
|
|
915
|
-
COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.input_tokens') ELSE 0 END), 0) as total_input_tokens,
|
|
916
|
-
COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.output_tokens') ELSE 0 END), 0) as total_output_tokens
|
|
917
|
-
|
|
1240
|
+
COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN 1 ELSE 0 END), 0) as total_llm_calls,
|
|
1241
|
+
COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as total_tool_calls,
|
|
1242
|
+
COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as total_errors,
|
|
1243
|
+
COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.input_tokens') ELSE 0 END), 0) as total_input_tokens,
|
|
1244
|
+
COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as total_output_tokens
|
|
1245
|
+
${fromClause}
|
|
918
1246
|
${where}
|
|
919
1247
|
`;
|
|
920
1248
|
|
|
@@ -939,6 +1267,15 @@ export const TelemetryDB = {
|
|
|
939
1267
|
return result.changes;
|
|
940
1268
|
},
|
|
941
1269
|
|
|
1270
|
+
// Delete all events for an agent
|
|
1271
|
+
deleteByAgent(agentId: string): number {
|
|
1272
|
+
const result = db.run(
|
|
1273
|
+
"DELETE FROM telemetry_events WHERE agent_id = ?",
|
|
1274
|
+
[agentId]
|
|
1275
|
+
);
|
|
1276
|
+
return result.changes;
|
|
1277
|
+
},
|
|
1278
|
+
|
|
942
1279
|
// Count events
|
|
943
1280
|
count(agentId?: string): number {
|
|
944
1281
|
if (agentId) {
|
|
@@ -969,6 +1306,207 @@ function rowToTelemetryEvent(row: TelemetryEventRow): TelemetryEvent {
|
|
|
969
1306
|
};
|
|
970
1307
|
}
|
|
971
1308
|
|
|
1309
|
+
// User operations
|
|
1310
|
+
export const UserDB = {
|
|
1311
|
+
// Create a new user
|
|
1312
|
+
create(user: { username: string; password_hash: string; email?: string | null; role?: "admin" | "user" }): User {
|
|
1313
|
+
const id = generateId();
|
|
1314
|
+
const now = new Date().toISOString();
|
|
1315
|
+
const role = user.role || "user";
|
|
1316
|
+
|
|
1317
|
+
db.run(
|
|
1318
|
+
`INSERT INTO users (id, username, password_hash, email, role, created_at, updated_at)
|
|
1319
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1320
|
+
[id, user.username.toLowerCase(), user.password_hash, user.email || null, role, now, now]
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
return this.findById(id)!;
|
|
1324
|
+
},
|
|
1325
|
+
|
|
1326
|
+
// Find user by ID
|
|
1327
|
+
findById(id: string): User | null {
|
|
1328
|
+
const row = db.query("SELECT * FROM users WHERE id = ?").get(id) as UserRow | null;
|
|
1329
|
+
return row ? rowToUser(row) : null;
|
|
1330
|
+
},
|
|
1331
|
+
|
|
1332
|
+
// Find user by username
|
|
1333
|
+
findByUsername(username: string): User | null {
|
|
1334
|
+
const row = db.query("SELECT * FROM users WHERE username = ?").get(username.toLowerCase()) as UserRow | null;
|
|
1335
|
+
return row ? rowToUser(row) : null;
|
|
1336
|
+
},
|
|
1337
|
+
|
|
1338
|
+
// Find user by email (for password recovery)
|
|
1339
|
+
findByEmail(email: string): User | null {
|
|
1340
|
+
const row = db.query("SELECT * FROM users WHERE email = ?").get(email.toLowerCase()) as UserRow | null;
|
|
1341
|
+
return row ? rowToUser(row) : null;
|
|
1342
|
+
},
|
|
1343
|
+
|
|
1344
|
+
// Get all users
|
|
1345
|
+
findAll(): User[] {
|
|
1346
|
+
const rows = db.query("SELECT * FROM users ORDER BY created_at DESC").all() as UserRow[];
|
|
1347
|
+
return rows.map(rowToUser);
|
|
1348
|
+
},
|
|
1349
|
+
|
|
1350
|
+
// Update user
|
|
1351
|
+
update(id: string, updates: Partial<Omit<User, "id" | "created_at">>): User | null {
|
|
1352
|
+
const user = this.findById(id);
|
|
1353
|
+
if (!user) return null;
|
|
1354
|
+
|
|
1355
|
+
const fields: string[] = [];
|
|
1356
|
+
const values: unknown[] = [];
|
|
1357
|
+
|
|
1358
|
+
if (updates.username !== undefined) {
|
|
1359
|
+
fields.push("username = ?");
|
|
1360
|
+
values.push(updates.username.toLowerCase());
|
|
1361
|
+
}
|
|
1362
|
+
if (updates.password_hash !== undefined) {
|
|
1363
|
+
fields.push("password_hash = ?");
|
|
1364
|
+
values.push(updates.password_hash);
|
|
1365
|
+
}
|
|
1366
|
+
if (updates.email !== undefined) {
|
|
1367
|
+
fields.push("email = ?");
|
|
1368
|
+
values.push(updates.email);
|
|
1369
|
+
}
|
|
1370
|
+
if (updates.role !== undefined) {
|
|
1371
|
+
fields.push("role = ?");
|
|
1372
|
+
values.push(updates.role);
|
|
1373
|
+
}
|
|
1374
|
+
if (updates.last_login_at !== undefined) {
|
|
1375
|
+
fields.push("last_login_at = ?");
|
|
1376
|
+
values.push(updates.last_login_at);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (fields.length > 0) {
|
|
1380
|
+
fields.push("updated_at = ?");
|
|
1381
|
+
values.push(new Date().toISOString());
|
|
1382
|
+
values.push(id);
|
|
1383
|
+
|
|
1384
|
+
db.run(`UPDATE users SET ${fields.join(", ")} WHERE id = ?`, values);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return this.findById(id);
|
|
1388
|
+
},
|
|
1389
|
+
|
|
1390
|
+
// Delete user
|
|
1391
|
+
delete(id: string): boolean {
|
|
1392
|
+
const result = db.run("DELETE FROM users WHERE id = ?", [id]);
|
|
1393
|
+
return result.changes > 0;
|
|
1394
|
+
},
|
|
1395
|
+
|
|
1396
|
+
// Update last login
|
|
1397
|
+
updateLastLogin(id: string): void {
|
|
1398
|
+
db.run("UPDATE users SET last_login_at = ? WHERE id = ?", [new Date().toISOString(), id]);
|
|
1399
|
+
},
|
|
1400
|
+
|
|
1401
|
+
// Count users
|
|
1402
|
+
count(): number {
|
|
1403
|
+
const row = db.query("SELECT COUNT(*) as count FROM users").get() as { count: number };
|
|
1404
|
+
return row.count;
|
|
1405
|
+
},
|
|
1406
|
+
|
|
1407
|
+
// Check if any users exist
|
|
1408
|
+
hasUsers(): boolean {
|
|
1409
|
+
return this.count() > 0;
|
|
1410
|
+
},
|
|
1411
|
+
|
|
1412
|
+
// Count admins
|
|
1413
|
+
countAdmins(): number {
|
|
1414
|
+
const row = db.query("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get() as { count: number };
|
|
1415
|
+
return row.count;
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
// Helper to convert DB row to User type
|
|
1420
|
+
function rowToUser(row: UserRow): User {
|
|
1421
|
+
return {
|
|
1422
|
+
id: row.id,
|
|
1423
|
+
username: row.username,
|
|
1424
|
+
password_hash: row.password_hash,
|
|
1425
|
+
email: row.email,
|
|
1426
|
+
role: row.role as "admin" | "user",
|
|
1427
|
+
created_at: row.created_at,
|
|
1428
|
+
updated_at: row.updated_at,
|
|
1429
|
+
last_login_at: row.last_login_at,
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Session operations
|
|
1434
|
+
export const SessionDB = {
|
|
1435
|
+
// Create a new session
|
|
1436
|
+
create(session: { user_id: string; refresh_token_hash: string; expires_at: string }): Session {
|
|
1437
|
+
const id = generateId();
|
|
1438
|
+
const now = new Date().toISOString();
|
|
1439
|
+
|
|
1440
|
+
db.run(
|
|
1441
|
+
`INSERT INTO sessions (id, user_id, refresh_token_hash, expires_at, created_at)
|
|
1442
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1443
|
+
[id, session.user_id, session.refresh_token_hash, session.expires_at, now]
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1446
|
+
return this.findById(id)!;
|
|
1447
|
+
},
|
|
1448
|
+
|
|
1449
|
+
// Find session by ID
|
|
1450
|
+
findById(id: string): Session | null {
|
|
1451
|
+
const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id) as SessionRow | null;
|
|
1452
|
+
return row ? rowToSession(row) : null;
|
|
1453
|
+
},
|
|
1454
|
+
|
|
1455
|
+
// Find session by refresh token hash
|
|
1456
|
+
findByTokenHash(tokenHash: string): Session | null {
|
|
1457
|
+
const row = db.query("SELECT * FROM sessions WHERE refresh_token_hash = ?").get(tokenHash) as SessionRow | null;
|
|
1458
|
+
return row ? rowToSession(row) : null;
|
|
1459
|
+
},
|
|
1460
|
+
|
|
1461
|
+
// Get all sessions for a user
|
|
1462
|
+
findByUser(userId: string): Session[] {
|
|
1463
|
+
const rows = db.query("SELECT * FROM sessions WHERE user_id = ? ORDER BY created_at DESC").all(userId) as SessionRow[];
|
|
1464
|
+
return rows.map(rowToSession);
|
|
1465
|
+
},
|
|
1466
|
+
|
|
1467
|
+
// Delete session
|
|
1468
|
+
delete(id: string): boolean {
|
|
1469
|
+
const result = db.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
1470
|
+
return result.changes > 0;
|
|
1471
|
+
},
|
|
1472
|
+
|
|
1473
|
+
// Delete session by token hash
|
|
1474
|
+
deleteByTokenHash(tokenHash: string): boolean {
|
|
1475
|
+
const result = db.run("DELETE FROM sessions WHERE refresh_token_hash = ?", [tokenHash]);
|
|
1476
|
+
return result.changes > 0;
|
|
1477
|
+
},
|
|
1478
|
+
|
|
1479
|
+
// Delete all sessions for a user
|
|
1480
|
+
deleteByUser(userId: string): number {
|
|
1481
|
+
const result = db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
|
|
1482
|
+
return result.changes;
|
|
1483
|
+
},
|
|
1484
|
+
|
|
1485
|
+
// Delete expired sessions
|
|
1486
|
+
deleteExpired(): number {
|
|
1487
|
+
const result = db.run("DELETE FROM sessions WHERE expires_at < ?", [new Date().toISOString()]);
|
|
1488
|
+
return result.changes;
|
|
1489
|
+
},
|
|
1490
|
+
|
|
1491
|
+
// Check if session is valid (exists and not expired)
|
|
1492
|
+
isValid(id: string): boolean {
|
|
1493
|
+
const session = this.findById(id);
|
|
1494
|
+
if (!session) return false;
|
|
1495
|
+
return new Date(session.expires_at) > new Date();
|
|
1496
|
+
},
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
// Helper to convert DB row to Session type
|
|
1500
|
+
function rowToSession(row: SessionRow): Session {
|
|
1501
|
+
return {
|
|
1502
|
+
id: row.id,
|
|
1503
|
+
user_id: row.user_id,
|
|
1504
|
+
refresh_token_hash: row.refresh_token_hash,
|
|
1505
|
+
expires_at: row.expires_at,
|
|
1506
|
+
created_at: row.created_at,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
972
1510
|
// Generate unique ID
|
|
973
1511
|
export function generateId(): string {
|
|
974
1512
|
return Math.random().toString(36).substring(2, 15);
|