agentdev-webui 1.0.0
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/lib/agent-api.js +530 -0
- package/lib/auth.js +127 -0
- package/lib/config.js +53 -0
- package/lib/database.js +762 -0
- package/lib/device-flow.js +257 -0
- package/lib/email.js +420 -0
- package/lib/encryption.js +112 -0
- package/lib/github.js +339 -0
- package/lib/history.js +143 -0
- package/lib/pwa.js +107 -0
- package/lib/redis-logs.js +226 -0
- package/lib/routes.js +680 -0
- package/migrations/000_create_database.sql +33 -0
- package/migrations/001_create_agentdev_schema.sql +135 -0
- package/migrations/001_create_agentdev_schema.sql.old +100 -0
- package/migrations/001_create_agentdev_schema_fixed.sql +135 -0
- package/migrations/002_add_github_token.sql +17 -0
- package/migrations/003_add_agent_logs_table.sql +23 -0
- package/migrations/004_remove_oauth_columns.sql +11 -0
- package/migrations/005_add_projects.sql +44 -0
- package/migrations/006_project_github_token.sql +7 -0
- package/migrations/007_project_repositories.sql +12 -0
- package/migrations/008_add_notifications.sql +20 -0
- package/migrations/009_unified_oauth.sql +153 -0
- package/migrations/README.md +97 -0
- package/package.json +37 -0
- package/public/css/styles.css +1140 -0
- package/public/device.html +384 -0
- package/public/docs.html +862 -0
- package/public/docs.md +697 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +271 -0
- package/public/js/app.js +2379 -0
- package/public/login.html +224 -0
- package/public/profile.html +394 -0
- package/public/register.html +392 -0
- package/public/reset-password.html +349 -0
- package/public/verify-email.html +177 -0
- package/server.js +1450 -0
package/lib/database.js
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
const { Pool } = require('pg');
|
|
2
|
+
const config = require('./config');
|
|
3
|
+
|
|
4
|
+
let pool = null;
|
|
5
|
+
|
|
6
|
+
function getPool() {
|
|
7
|
+
if (!pool) {
|
|
8
|
+
pool = new Pool({
|
|
9
|
+
connectionString: config.DATABASE_URL,
|
|
10
|
+
max: 20,
|
|
11
|
+
idleTimeoutMillis: 30000,
|
|
12
|
+
connectionTimeoutMillis: 2000,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
pool.on('error', (err) => {
|
|
16
|
+
console.error('Unexpected error on idle PostgreSQL client', err);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return pool;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute a query
|
|
24
|
+
* @param {string} text - SQL query
|
|
25
|
+
* @param {Array} params - Query parameters
|
|
26
|
+
* @returns {Promise<object>} - Query result
|
|
27
|
+
*/
|
|
28
|
+
async function query(text, params) {
|
|
29
|
+
const pool = getPool();
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
try {
|
|
32
|
+
const res = await pool.query(text, params);
|
|
33
|
+
const duration = Date.now() - start;
|
|
34
|
+
if (duration > 100) {
|
|
35
|
+
console.warn('Slow query:', { text, duration, rows: res.rowCount });
|
|
36
|
+
}
|
|
37
|
+
return res;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Database query error:', { text, params, error: error.message });
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get a client from the pool for transactions
|
|
46
|
+
* @returns {Promise<object>} - PostgreSQL client
|
|
47
|
+
*/
|
|
48
|
+
async function getClient() {
|
|
49
|
+
const pool = getPool();
|
|
50
|
+
return await pool.connect();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Close all database connections
|
|
55
|
+
*/
|
|
56
|
+
async function close() {
|
|
57
|
+
if (pool) {
|
|
58
|
+
await pool.end();
|
|
59
|
+
pool = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// User operations
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
async function createUser(email, passwordHash, options = {}) {
|
|
68
|
+
const result = await query(
|
|
69
|
+
`INSERT INTO agentdev_users
|
|
70
|
+
(email, password_hash, max_agents)
|
|
71
|
+
VALUES ($1, $2, $3)
|
|
72
|
+
RETURNING id, email, max_agents, created_at`,
|
|
73
|
+
[
|
|
74
|
+
email,
|
|
75
|
+
passwordHash,
|
|
76
|
+
options.maxAgents || 3
|
|
77
|
+
]
|
|
78
|
+
);
|
|
79
|
+
return result.rows[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getUserByEmail(email) {
|
|
83
|
+
const result = await query(
|
|
84
|
+
'SELECT * FROM agentdev_users WHERE email = $1',
|
|
85
|
+
[email]
|
|
86
|
+
);
|
|
87
|
+
return result.rows[0] || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getUserById(id) {
|
|
91
|
+
const result = await query(
|
|
92
|
+
'SELECT * FROM agentdev_users WHERE id = $1',
|
|
93
|
+
[id]
|
|
94
|
+
);
|
|
95
|
+
return result.rows[0] || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function updateUserLimits(userId, limits) {
|
|
99
|
+
const result = await query(
|
|
100
|
+
`UPDATE agentdev_users
|
|
101
|
+
SET max_agents = $2
|
|
102
|
+
WHERE id = $1
|
|
103
|
+
RETURNING id, email, max_agents`,
|
|
104
|
+
[userId, limits.max_agents]
|
|
105
|
+
);
|
|
106
|
+
return result.rows[0];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function updateUserGitHubToken(userId, encryptedToken) {
|
|
110
|
+
const result = await query(
|
|
111
|
+
`UPDATE agentdev_users
|
|
112
|
+
SET github_token_encrypted = $2,
|
|
113
|
+
updated_at = NOW()
|
|
114
|
+
WHERE id = $1
|
|
115
|
+
RETURNING id, email`,
|
|
116
|
+
[userId, encryptedToken]
|
|
117
|
+
);
|
|
118
|
+
return result.rows[0];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Project operations
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
async function createProject(data) {
|
|
126
|
+
const result = await query(
|
|
127
|
+
`INSERT INTO agentdev_projects
|
|
128
|
+
(name, github_org, project_number, github_project_id, status_field_id, status_options, created_by, github_token, repositories)
|
|
129
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
130
|
+
RETURNING *`,
|
|
131
|
+
[
|
|
132
|
+
data.name,
|
|
133
|
+
data.github_org,
|
|
134
|
+
data.project_number,
|
|
135
|
+
data.github_project_id,
|
|
136
|
+
data.status_field_id,
|
|
137
|
+
JSON.stringify(data.status_options || {}),
|
|
138
|
+
data.created_by || null,
|
|
139
|
+
data.github_token || null,
|
|
140
|
+
JSON.stringify(data.repositories || [])
|
|
141
|
+
]
|
|
142
|
+
);
|
|
143
|
+
return result.rows[0];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function getProjects() {
|
|
147
|
+
const result = await query(
|
|
148
|
+
'SELECT id, name, github_org, project_number, github_project_id, status_field_id, status_options, repositories, created_by, created_at FROM agentdev_projects ORDER BY id ASC'
|
|
149
|
+
);
|
|
150
|
+
return result.rows;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function getProjectWithToken(id) {
|
|
154
|
+
const result = await query(
|
|
155
|
+
`SELECT p.*, u.github_token_encrypted
|
|
156
|
+
FROM agentdev_projects p
|
|
157
|
+
LEFT JOIN agentdev_users u ON u.id = p.created_by
|
|
158
|
+
WHERE p.id = $1`,
|
|
159
|
+
[id]
|
|
160
|
+
);
|
|
161
|
+
return result.rows[0] || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function getProjectById(id) {
|
|
165
|
+
const result = await query(
|
|
166
|
+
'SELECT * FROM agentdev_projects WHERE id = $1',
|
|
167
|
+
[id]
|
|
168
|
+
);
|
|
169
|
+
return result.rows[0] || null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function updateProject(id, data) {
|
|
173
|
+
const fields = [];
|
|
174
|
+
const values = [];
|
|
175
|
+
let paramIdx = 1;
|
|
176
|
+
|
|
177
|
+
for (const key of ['name', 'github_org', 'project_number', 'github_project_id', 'status_field_id', 'github_token']) {
|
|
178
|
+
if (data[key] !== undefined) {
|
|
179
|
+
fields.push(`${key} = $${paramIdx}`);
|
|
180
|
+
values.push(data[key]);
|
|
181
|
+
paramIdx++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// JSONB fields need stringify
|
|
185
|
+
for (const key of ['status_options', 'repositories']) {
|
|
186
|
+
if (data[key] !== undefined) {
|
|
187
|
+
fields.push(`${key} = $${paramIdx}`);
|
|
188
|
+
values.push(JSON.stringify(data[key]));
|
|
189
|
+
paramIdx++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (fields.length === 0) return null;
|
|
194
|
+
|
|
195
|
+
values.push(id);
|
|
196
|
+
const result = await query(
|
|
197
|
+
`UPDATE agentdev_projects SET ${fields.join(', ')} WHERE id = $${paramIdx} RETURNING *`,
|
|
198
|
+
values
|
|
199
|
+
);
|
|
200
|
+
return result.rows[0] || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function deleteProject(id) {
|
|
204
|
+
const result = await query(
|
|
205
|
+
'DELETE FROM agentdev_projects WHERE id = $1 RETURNING *',
|
|
206
|
+
[id]
|
|
207
|
+
);
|
|
208
|
+
return result.rows[0] || null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function assignAgentToProject(agentId, projectId) {
|
|
212
|
+
const result = await query(
|
|
213
|
+
`UPDATE agentdev_agents SET project_id = $2 WHERE id = $1 RETURNING *`,
|
|
214
|
+
[agentId, projectId]
|
|
215
|
+
);
|
|
216
|
+
return result.rows[0] || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Agent operations
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
async function createAgent(agentData) {
|
|
224
|
+
const result = await query(
|
|
225
|
+
`INSERT INTO agentdev_agents
|
|
226
|
+
(id, user_id, name, hostname, capabilities, access_token_hash, status, project_id)
|
|
227
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'offline', $7)
|
|
228
|
+
RETURNING *`,
|
|
229
|
+
[
|
|
230
|
+
agentData.id,
|
|
231
|
+
agentData.userId,
|
|
232
|
+
agentData.name,
|
|
233
|
+
agentData.hostname || null,
|
|
234
|
+
JSON.stringify(agentData.capabilities || {}),
|
|
235
|
+
agentData.accessTokenHash,
|
|
236
|
+
agentData.projectId || null
|
|
237
|
+
]
|
|
238
|
+
);
|
|
239
|
+
return result.rows[0];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function getAgentById(agentId) {
|
|
243
|
+
const result = await query(
|
|
244
|
+
'SELECT * FROM agentdev_agents WHERE id = $1',
|
|
245
|
+
[agentId]
|
|
246
|
+
);
|
|
247
|
+
return result.rows[0] || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function getAgentsByUser(userId) {
|
|
251
|
+
const result = await query(
|
|
252
|
+
`SELECT * FROM agentdev_agents
|
|
253
|
+
WHERE user_id = $1
|
|
254
|
+
ORDER BY registered_at DESC`,
|
|
255
|
+
[userId]
|
|
256
|
+
);
|
|
257
|
+
return result.rows;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function updateAgentHeartbeat(agentId, status, currentTicketId = null) {
|
|
261
|
+
const result = await query(
|
|
262
|
+
`UPDATE agentdev_agents
|
|
263
|
+
SET last_heartbeat = NOW(),
|
|
264
|
+
status = $2,
|
|
265
|
+
current_ticket_id = $3
|
|
266
|
+
WHERE id = $1
|
|
267
|
+
RETURNING *`,
|
|
268
|
+
[agentId, status, currentTicketId]
|
|
269
|
+
);
|
|
270
|
+
return result.rows[0];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function updateAgentTokenHash(agentId, tokenHash) {
|
|
274
|
+
await query(
|
|
275
|
+
'UPDATE agentdev_agents SET access_token_hash = $2 WHERE id = $1',
|
|
276
|
+
[agentId, tokenHash]
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function setAgentOffline(agentId) {
|
|
281
|
+
const result = await query(
|
|
282
|
+
`UPDATE agentdev_agents
|
|
283
|
+
SET status = 'offline',
|
|
284
|
+
current_ticket_id = NULL
|
|
285
|
+
WHERE id = $1
|
|
286
|
+
RETURNING *`,
|
|
287
|
+
[agentId]
|
|
288
|
+
);
|
|
289
|
+
return result.rows[0];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function getActiveAgents(projectId = null) {
|
|
293
|
+
// Get agents with heartbeat within last 2 minutes
|
|
294
|
+
let sql = `SELECT a.*, t.github_issue_number, t.github_repo, t.status as ticket_status
|
|
295
|
+
FROM agentdev_agents a
|
|
296
|
+
LEFT JOIN agentdev_tickets t ON a.current_ticket_id = t.id
|
|
297
|
+
WHERE a.last_heartbeat > NOW() - INTERVAL '2 minutes'`;
|
|
298
|
+
const params = [];
|
|
299
|
+
if (projectId) {
|
|
300
|
+
params.push(projectId);
|
|
301
|
+
sql += ` AND a.project_id = $${params.length}`;
|
|
302
|
+
}
|
|
303
|
+
sql += ' ORDER BY a.last_heartbeat DESC';
|
|
304
|
+
const result = await query(sql, params);
|
|
305
|
+
return result.rows;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function getAgentHistory(limit = 100, projectId = null) {
|
|
309
|
+
// Get agents with old heartbeats (completed work) as history
|
|
310
|
+
let sql = `SELECT a.*, t.github_issue_number, t.github_repo, t.status as ticket_status
|
|
311
|
+
FROM agentdev_agents a
|
|
312
|
+
LEFT JOIN agentdev_tickets t ON a.current_ticket_id = t.id
|
|
313
|
+
WHERE (a.last_heartbeat < NOW() - INTERVAL '1 hour'
|
|
314
|
+
OR a.status = 'offline')`;
|
|
315
|
+
const params = [];
|
|
316
|
+
if (projectId) {
|
|
317
|
+
params.push(projectId);
|
|
318
|
+
sql += ` AND a.project_id = $${params.length}`;
|
|
319
|
+
}
|
|
320
|
+
params.push(limit);
|
|
321
|
+
sql += ` ORDER BY a.last_heartbeat DESC LIMIT $${params.length}`;
|
|
322
|
+
const result = await query(sql, params);
|
|
323
|
+
return result.rows;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Ticket operations
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
async function createTicket(ticketData) {
|
|
331
|
+
const result = await query(
|
|
332
|
+
`INSERT INTO agentdev_tickets
|
|
333
|
+
(github_issue_number, github_repo, github_project_item_id, priority, status, project_id)
|
|
334
|
+
VALUES ($1, $2, $3, $4, 'todo', $5)
|
|
335
|
+
ON CONFLICT (github_repo, github_issue_number)
|
|
336
|
+
DO UPDATE SET
|
|
337
|
+
github_project_item_id = EXCLUDED.github_project_item_id,
|
|
338
|
+
priority = EXCLUDED.priority,
|
|
339
|
+
project_id = COALESCE(EXCLUDED.project_id, agentdev_tickets.project_id)
|
|
340
|
+
RETURNING *`,
|
|
341
|
+
[
|
|
342
|
+
ticketData.issueNumber,
|
|
343
|
+
ticketData.repo,
|
|
344
|
+
ticketData.projectItemId || null,
|
|
345
|
+
ticketData.priority || 5,
|
|
346
|
+
ticketData.projectId || null
|
|
347
|
+
]
|
|
348
|
+
);
|
|
349
|
+
return result.rows[0];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function getAvailableTicket(projectId = null) {
|
|
353
|
+
// Get highest priority unassigned ticket
|
|
354
|
+
let sql = `SELECT * FROM agentdev_tickets
|
|
355
|
+
WHERE status = 'todo' AND assigned_agent_id IS NULL`;
|
|
356
|
+
const params = [];
|
|
357
|
+
if (projectId) {
|
|
358
|
+
params.push(projectId);
|
|
359
|
+
sql += ` AND project_id = $${params.length}`;
|
|
360
|
+
}
|
|
361
|
+
sql += ' ORDER BY priority DESC, created_at ASC LIMIT 1';
|
|
362
|
+
const result = await query(sql, params);
|
|
363
|
+
return result.rows[0] || null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function assignTicket(ticketId, agentId) {
|
|
367
|
+
const result = await query(
|
|
368
|
+
`UPDATE agentdev_tickets
|
|
369
|
+
SET assigned_agent_id = $2,
|
|
370
|
+
status = 'assigned',
|
|
371
|
+
assigned_at = NOW()
|
|
372
|
+
WHERE id = $1 AND assigned_agent_id IS NULL
|
|
373
|
+
RETURNING *`,
|
|
374
|
+
[ticketId, agentId]
|
|
375
|
+
);
|
|
376
|
+
return result.rows[0] || null; // null if already assigned
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function reassignStaleTicket(ticketId, newAgentId, staleMinutes = 2) {
|
|
380
|
+
const result = await query(
|
|
381
|
+
`UPDATE agentdev_tickets t
|
|
382
|
+
SET assigned_agent_id = $2,
|
|
383
|
+
status = 'assigned',
|
|
384
|
+
assigned_at = NOW()
|
|
385
|
+
WHERE t.id = $1
|
|
386
|
+
AND t.assigned_agent_id IS NOT NULL
|
|
387
|
+
AND NOT EXISTS (
|
|
388
|
+
SELECT 1 FROM agentdev_agents a
|
|
389
|
+
WHERE a.id = t.assigned_agent_id
|
|
390
|
+
AND a.last_heartbeat > NOW() - INTERVAL '1 minute' * $3
|
|
391
|
+
)
|
|
392
|
+
RETURNING *`,
|
|
393
|
+
[ticketId, newAgentId, staleMinutes]
|
|
394
|
+
);
|
|
395
|
+
return result.rows[0] || null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Reset tickets stuck in 'assigned' whose agent has a stale heartbeat,
|
|
400
|
+
* and auto-retry 'failed' tickets (up to maxRetries).
|
|
401
|
+
* Returns the reclaimed rows.
|
|
402
|
+
*/
|
|
403
|
+
async function reclaimStaleTickets(staleMinutes = 3, maxRetries = 3) {
|
|
404
|
+
// 1. Reclaim assigned tickets with stale agents
|
|
405
|
+
const stale = await query(
|
|
406
|
+
`UPDATE agentdev_tickets t
|
|
407
|
+
SET status = 'todo', assigned_agent_id = NULL, assigned_at = NULL
|
|
408
|
+
WHERE t.status = 'assigned'
|
|
409
|
+
AND t.assigned_agent_id IS NOT NULL
|
|
410
|
+
AND NOT EXISTS (
|
|
411
|
+
SELECT 1 FROM agentdev_agents a
|
|
412
|
+
WHERE a.id = t.assigned_agent_id
|
|
413
|
+
AND a.last_heartbeat > NOW() - INTERVAL '1 minute' * $1
|
|
414
|
+
)
|
|
415
|
+
RETURNING id, github_issue_number, 'stale' AS reason`,
|
|
416
|
+
[staleMinutes]
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// 2. Reclaim tickets assigned to agents that are alive but idle (not working on them)
|
|
420
|
+
const idle = await query(
|
|
421
|
+
`UPDATE agentdev_tickets t
|
|
422
|
+
SET status = 'todo', assigned_agent_id = NULL, assigned_at = NULL
|
|
423
|
+
WHERE t.status = 'assigned'
|
|
424
|
+
AND t.assigned_agent_id IS NOT NULL
|
|
425
|
+
AND EXISTS (
|
|
426
|
+
SELECT 1 FROM agentdev_agents a
|
|
427
|
+
WHERE a.id = t.assigned_agent_id
|
|
428
|
+
AND a.status = 'idle'
|
|
429
|
+
AND a.last_heartbeat > NOW() - INTERVAL '1 minute' * $1
|
|
430
|
+
)
|
|
431
|
+
RETURNING id, github_issue_number, 'idle_agent' AS reason`,
|
|
432
|
+
[staleMinutes]
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// 3. Auto-retry failed tickets (with retry limit)
|
|
436
|
+
const failed = await query(
|
|
437
|
+
`UPDATE agentdev_tickets
|
|
438
|
+
SET status = 'todo', assigned_agent_id = NULL, assigned_at = NULL,
|
|
439
|
+
retry_count = retry_count + 1
|
|
440
|
+
WHERE status = 'failed'
|
|
441
|
+
AND retry_count < $1
|
|
442
|
+
RETURNING id, github_issue_number, retry_count, 'retry' AS reason`,
|
|
443
|
+
[maxRetries]
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
return [...stale.rows, ...idle.rows, ...failed.rows];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function updateTicketStatus(ticketId, status, errorMessage = null) {
|
|
450
|
+
const completedAt = status === 'completed' ? 'NOW()' : 'NULL';
|
|
451
|
+
const result = await query(
|
|
452
|
+
`UPDATE agentdev_tickets
|
|
453
|
+
SET status = $2,
|
|
454
|
+
error_message = $3,
|
|
455
|
+
completed_at = ${completedAt}
|
|
456
|
+
WHERE id = $1
|
|
457
|
+
RETURNING *`,
|
|
458
|
+
[ticketId, status, errorMessage]
|
|
459
|
+
);
|
|
460
|
+
return result.rows[0];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function getTicketsByStatus(status) {
|
|
464
|
+
const result = await query(
|
|
465
|
+
`SELECT * FROM agentdev_tickets
|
|
466
|
+
WHERE status = $1
|
|
467
|
+
ORDER BY priority DESC, created_at ASC`,
|
|
468
|
+
[status]
|
|
469
|
+
);
|
|
470
|
+
return result.rows;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function getCompletedTicketsForProject(projectId) {
|
|
474
|
+
const result = await query(
|
|
475
|
+
`SELECT id, github_repo, github_issue_number, completed_at, project_id
|
|
476
|
+
FROM agentdev_tickets
|
|
477
|
+
WHERE status IN ('completed', 'failed')
|
|
478
|
+
AND completed_at IS NOT NULL
|
|
479
|
+
AND project_id = $1`,
|
|
480
|
+
[projectId]
|
|
481
|
+
);
|
|
482
|
+
return result.rows;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// Log operations
|
|
487
|
+
// ============================================================================
|
|
488
|
+
|
|
489
|
+
async function insertLog(agentId, ticketId, content, level = 'INFO') {
|
|
490
|
+
await query(
|
|
491
|
+
`INSERT INTO agentdev_logs (agent_id, ticket_id, content, level)
|
|
492
|
+
VALUES ($1, $2, $3, $4)`,
|
|
493
|
+
[agentId, ticketId, content, level]
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function getLogsByAgent(agentId, limit = 100) {
|
|
498
|
+
const result = await query(
|
|
499
|
+
`SELECT * FROM agentdev_logs
|
|
500
|
+
WHERE agent_id = $1
|
|
501
|
+
ORDER BY timestamp DESC
|
|
502
|
+
LIMIT $2`,
|
|
503
|
+
[agentId, limit]
|
|
504
|
+
);
|
|
505
|
+
return result.rows;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function getLogsByTicket(ticketId, limit = 100) {
|
|
509
|
+
const result = await query(
|
|
510
|
+
`SELECT * FROM agentdev_logs
|
|
511
|
+
WHERE ticket_id = $1
|
|
512
|
+
ORDER BY timestamp ASC
|
|
513
|
+
LIMIT $2`,
|
|
514
|
+
[ticketId, limit]
|
|
515
|
+
);
|
|
516
|
+
return result.rows;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ============================================================================
|
|
520
|
+
// Device flow operations
|
|
521
|
+
// ============================================================================
|
|
522
|
+
|
|
523
|
+
async function createDeviceCode(deviceCode, userCode, agentName, capabilities) {
|
|
524
|
+
const expiresAt = new Date(Date.now() + config.DEVICE_CODE_TTL * 1000);
|
|
525
|
+
const result = await query(
|
|
526
|
+
`INSERT INTO agentdev_device_codes
|
|
527
|
+
(device_code, user_code, agent_name, agent_capabilities, expires_at)
|
|
528
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
529
|
+
RETURNING *`,
|
|
530
|
+
[deviceCode, userCode, agentName, JSON.stringify(capabilities), expiresAt]
|
|
531
|
+
);
|
|
532
|
+
return result.rows[0];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function getDeviceCodeByUserCode(userCode) {
|
|
536
|
+
const result = await query(
|
|
537
|
+
`SELECT * FROM agentdev_device_codes
|
|
538
|
+
WHERE user_code = $1 AND expires_at > NOW()`,
|
|
539
|
+
[userCode]
|
|
540
|
+
);
|
|
541
|
+
return result.rows[0] || null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function getDeviceCode(deviceCode) {
|
|
545
|
+
const result = await query(
|
|
546
|
+
`SELECT * FROM agentdev_device_codes
|
|
547
|
+
WHERE device_code = $1 AND expires_at > NOW()`,
|
|
548
|
+
[deviceCode]
|
|
549
|
+
);
|
|
550
|
+
return result.rows[0] || null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function approveDeviceCode(deviceCode, userId) {
|
|
554
|
+
const result = await query(
|
|
555
|
+
`UPDATE agentdev_device_codes
|
|
556
|
+
SET status = 'approved', user_id = $2
|
|
557
|
+
WHERE device_code = $1 AND status = 'pending'
|
|
558
|
+
RETURNING *`,
|
|
559
|
+
[deviceCode, userId]
|
|
560
|
+
);
|
|
561
|
+
return result.rows[0] || null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function denyDeviceCode(deviceCode) {
|
|
565
|
+
const result = await query(
|
|
566
|
+
`UPDATE agentdev_device_codes
|
|
567
|
+
SET status = 'denied'
|
|
568
|
+
WHERE device_code = $1 AND status = 'pending'
|
|
569
|
+
RETURNING *`,
|
|
570
|
+
[deviceCode]
|
|
571
|
+
);
|
|
572
|
+
return result.rows[0] || null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function getRecentLogs(limit = 50) {
|
|
576
|
+
const result = await query(
|
|
577
|
+
`SELECT agent_id, ticket_id, timestamp, level, content
|
|
578
|
+
FROM agentdev_logs
|
|
579
|
+
ORDER BY timestamp DESC
|
|
580
|
+
LIMIT $1`,
|
|
581
|
+
[limit]
|
|
582
|
+
);
|
|
583
|
+
return result.rows;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ============================================================================
|
|
587
|
+
// OAuth Provider operations
|
|
588
|
+
// ============================================================================
|
|
589
|
+
|
|
590
|
+
async function getOAuthProviderConfigs() {
|
|
591
|
+
const result = await query(
|
|
592
|
+
'SELECT * FROM agentdev_oauth_provider_configs WHERE is_active = true ORDER BY name ASC'
|
|
593
|
+
);
|
|
594
|
+
return result.rows;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function getOAuthProviderConfig(providerKey) {
|
|
598
|
+
const result = await query(
|
|
599
|
+
'SELECT * FROM agentdev_oauth_provider_configs WHERE provider_key = $1',
|
|
600
|
+
[providerKey]
|
|
601
|
+
);
|
|
602
|
+
return result.rows[0] || null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function upsertOAuthProvider(data) {
|
|
606
|
+
const result = await query(
|
|
607
|
+
`INSERT INTO agentdev_oauth_providers
|
|
608
|
+
(user_id, config_id, provider_key, access_token, refresh_token, token_expires_at, scopes, status)
|
|
609
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, 'active')
|
|
610
|
+
ON CONFLICT (user_id, provider_key)
|
|
611
|
+
DO UPDATE SET
|
|
612
|
+
access_token = EXCLUDED.access_token,
|
|
613
|
+
refresh_token = COALESCE(EXCLUDED.refresh_token, agentdev_oauth_providers.refresh_token),
|
|
614
|
+
token_expires_at = EXCLUDED.token_expires_at,
|
|
615
|
+
scopes = EXCLUDED.scopes,
|
|
616
|
+
status = 'active',
|
|
617
|
+
updated_at = NOW()
|
|
618
|
+
RETURNING *`,
|
|
619
|
+
[
|
|
620
|
+
data.userId,
|
|
621
|
+
data.configId,
|
|
622
|
+
data.providerKey,
|
|
623
|
+
data.accessToken,
|
|
624
|
+
data.refreshToken || null,
|
|
625
|
+
data.tokenExpiresAt || null,
|
|
626
|
+
data.scopes || []
|
|
627
|
+
]
|
|
628
|
+
);
|
|
629
|
+
return result.rows[0];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function getOAuthProvider(userId, providerKey) {
|
|
633
|
+
const result = await query(
|
|
634
|
+
`SELECT op.*, opc.name as provider_name, opc.authorize_payload, opc.callback_payload
|
|
635
|
+
FROM agentdev_oauth_providers op
|
|
636
|
+
JOIN agentdev_oauth_provider_configs opc ON op.config_id = opc.id
|
|
637
|
+
WHERE op.user_id = $1 AND op.provider_key = $2 AND op.status = 'active'`,
|
|
638
|
+
[userId, providerKey]
|
|
639
|
+
);
|
|
640
|
+
return result.rows[0] || null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function getUserOAuthProviders(userId) {
|
|
644
|
+
const result = await query(
|
|
645
|
+
`SELECT op.*, opc.name as provider_name
|
|
646
|
+
FROM agentdev_oauth_providers op
|
|
647
|
+
JOIN agentdev_oauth_provider_configs opc ON op.config_id = opc.id
|
|
648
|
+
WHERE op.user_id = $1 AND op.status = 'active'
|
|
649
|
+
ORDER BY opc.name ASC`,
|
|
650
|
+
[userId]
|
|
651
|
+
);
|
|
652
|
+
return result.rows;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function deleteOAuthProvider(userId, providerKey) {
|
|
656
|
+
const result = await query(
|
|
657
|
+
`DELETE FROM agentdev_oauth_providers
|
|
658
|
+
WHERE user_id = $1 AND provider_key = $2
|
|
659
|
+
RETURNING *`,
|
|
660
|
+
[userId, providerKey]
|
|
661
|
+
);
|
|
662
|
+
return result.rows[0] || null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Notifications
|
|
666
|
+
// ============================================================================
|
|
667
|
+
|
|
668
|
+
async function createNotification(userId, type, title, message, metadata = {}) {
|
|
669
|
+
const result = await query(
|
|
670
|
+
`INSERT INTO agentdev_notifications (user_id, type, title, message, metadata)
|
|
671
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
672
|
+
RETURNING *`,
|
|
673
|
+
[userId, type, title, message, JSON.stringify(metadata)]
|
|
674
|
+
);
|
|
675
|
+
return result.rows[0];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function getUnreadNotifications(userId) {
|
|
679
|
+
const result = await query(
|
|
680
|
+
`SELECT * FROM agentdev_notifications
|
|
681
|
+
WHERE user_id = $1 AND read = FALSE
|
|
682
|
+
ORDER BY created_at DESC
|
|
683
|
+
LIMIT 50`,
|
|
684
|
+
[userId]
|
|
685
|
+
);
|
|
686
|
+
return result.rows;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function markNotificationsRead(userId) {
|
|
690
|
+
await query(
|
|
691
|
+
'UPDATE agentdev_notifications SET read = TRUE WHERE user_id = $1 AND read = FALSE',
|
|
692
|
+
[userId]
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
module.exports = {
|
|
697
|
+
query,
|
|
698
|
+
getClient,
|
|
699
|
+
close,
|
|
700
|
+
|
|
701
|
+
// Projects
|
|
702
|
+
createProject,
|
|
703
|
+
getProjects,
|
|
704
|
+
getProjectById,
|
|
705
|
+
getProjectWithToken,
|
|
706
|
+
updateProject,
|
|
707
|
+
deleteProject,
|
|
708
|
+
assignAgentToProject,
|
|
709
|
+
|
|
710
|
+
// Users
|
|
711
|
+
createUser,
|
|
712
|
+
getUserByEmail,
|
|
713
|
+
getUserById,
|
|
714
|
+
updateUserLimits,
|
|
715
|
+
updateUserGitHubToken,
|
|
716
|
+
|
|
717
|
+
// Agents
|
|
718
|
+
createAgent,
|
|
719
|
+
getAgentById,
|
|
720
|
+
getAgentsByUser,
|
|
721
|
+
getActiveAgents,
|
|
722
|
+
getAgentHistory,
|
|
723
|
+
updateAgentHeartbeat,
|
|
724
|
+
updateAgentTokenHash,
|
|
725
|
+
setAgentOffline,
|
|
726
|
+
|
|
727
|
+
// Tickets
|
|
728
|
+
createTicket,
|
|
729
|
+
getAvailableTicket,
|
|
730
|
+
assignTicket,
|
|
731
|
+
reassignStaleTicket,
|
|
732
|
+
reclaimStaleTickets,
|
|
733
|
+
updateTicketStatus,
|
|
734
|
+
getTicketsByStatus,
|
|
735
|
+
getCompletedTicketsForProject,
|
|
736
|
+
|
|
737
|
+
// Logs
|
|
738
|
+
insertLog,
|
|
739
|
+
getRecentLogs,
|
|
740
|
+
getLogsByAgent,
|
|
741
|
+
getLogsByTicket,
|
|
742
|
+
|
|
743
|
+
// Device flow
|
|
744
|
+
createDeviceCode,
|
|
745
|
+
getDeviceCodeByUserCode,
|
|
746
|
+
getDeviceCode,
|
|
747
|
+
approveDeviceCode,
|
|
748
|
+
denyDeviceCode,
|
|
749
|
+
|
|
750
|
+
// OAuth Providers
|
|
751
|
+
getOAuthProviderConfigs,
|
|
752
|
+
getOAuthProviderConfig,
|
|
753
|
+
upsertOAuthProvider,
|
|
754
|
+
getOAuthProvider,
|
|
755
|
+
getUserOAuthProviders,
|
|
756
|
+
deleteOAuthProvider,
|
|
757
|
+
|
|
758
|
+
// Notifications
|
|
759
|
+
createNotification,
|
|
760
|
+
getUnreadNotifications,
|
|
761
|
+
markNotificationsRead
|
|
762
|
+
};
|