agentgate 0.4.0 → 0.5.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/README.md +118 -4
- package/package.json +3 -2
- package/public/mobile.css +178 -0
- package/public/style.css +129 -0
- package/src/index.js +232 -32
- package/src/lib/agentNotifier.js +82 -1
- package/src/lib/db.js +825 -8
- package/src/routes/agents.js +49 -8
- package/src/routes/linkedin.js +30 -10
- package/src/routes/memento.js +106 -0
- package/src/routes/queue.js +165 -6
- package/src/routes/services.js +87 -0
- package/src/routes/ui/access.js +290 -0
- package/src/routes/ui/calendar.js +65 -14
- package/src/routes/ui/fitbit.js +63 -14
- package/src/routes/ui/home.js +313 -0
- package/src/routes/ui/index.js +52 -35
- package/src/routes/ui/keys.js +561 -11
- package/src/routes/ui/linkedin.js +75 -19
- package/src/routes/ui/mastodon.js +70 -18
- package/src/routes/ui/mementos.js +363 -0
- package/src/routes/ui/messages.js +155 -18
- package/src/routes/ui/queue.js +193 -14
- package/src/routes/ui/reddit.js +63 -14
- package/src/routes/ui/services.js +46 -0
- package/src/routes/ui/shared.js +137 -7
- package/src/routes/ui/youtube.js +63 -14
- package/src/routes/webhooks.js +247 -0
- package/src/routes/ui.js +0 -2151
package/src/routes/ui/keys.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Agents routes
|
|
2
2
|
import { Router } from 'express';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { writeFileSync } from 'fs';
|
|
5
|
+
import { listApiKeys, createApiKey, deleteApiKey, regenerateApiKey, updateAgentWebhook, getApiKeyById, getAvatarsDir, getAvatarFilename, deleteAgentAvatar, setAgentEnabled } from '../../lib/db.js';
|
|
6
|
+
import { escapeHtml, formatDate, simpleNavHeader, socketScript, localizeScript, renderAvatar } from './shared.js';
|
|
5
7
|
|
|
6
8
|
const router = Router();
|
|
7
9
|
|
|
8
|
-
//
|
|
10
|
+
// Agents Management
|
|
9
11
|
router.get('/', (req, res) => {
|
|
10
12
|
const keys = listApiKeys();
|
|
11
13
|
res.send(renderKeysPage(keys));
|
|
@@ -64,6 +66,62 @@ router.post('/:id/webhook', (req, res) => {
|
|
|
64
66
|
res.redirect('/ui/keys');
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
// Test webhook
|
|
70
|
+
router.post('/:id/test-webhook', async (req, res) => {
|
|
71
|
+
const { id } = req.params;
|
|
72
|
+
const agent = getApiKeyById(id);
|
|
73
|
+
|
|
74
|
+
if (!agent) {
|
|
75
|
+
return res.status(404).json({ error: 'Agent not found' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!agent.webhook_url) {
|
|
79
|
+
return res.status(400).json({ error: 'No webhook URL configured for this agent' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const payload = {
|
|
83
|
+
text: `🧪 [agentgate] Webhook test for ${agent.name} - if you see this, your webhook is working!`,
|
|
84
|
+
mode: 'now',
|
|
85
|
+
test: true
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
90
|
+
if (agent.webhook_token) {
|
|
91
|
+
headers['Authorization'] = `Bearer ${agent.webhook_token}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const response = await fetch(agent.webhook_url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers,
|
|
97
|
+
body: JSON.stringify(payload)
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const responseText = await response.text().catch(() => '');
|
|
101
|
+
|
|
102
|
+
if (response.ok) {
|
|
103
|
+
return res.json({
|
|
104
|
+
success: true,
|
|
105
|
+
status: response.status,
|
|
106
|
+
message: `Webhook test successful (HTTP ${response.status})`
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
return res.json({
|
|
110
|
+
success: false,
|
|
111
|
+
status: response.status,
|
|
112
|
+
message: `Webhook returned HTTP ${response.status}`,
|
|
113
|
+
response: responseText.substring(0, 500)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return res.json({
|
|
118
|
+
success: false,
|
|
119
|
+
status: 0,
|
|
120
|
+
message: `Connection failed: ${err.message}`
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
67
125
|
router.delete('/:id', (req, res) => {
|
|
68
126
|
const { id } = req.params;
|
|
69
127
|
const wantsJson = req.headers.accept?.includes('application/json');
|
|
@@ -77,11 +135,170 @@ router.delete('/:id', (req, res) => {
|
|
|
77
135
|
res.redirect('/ui/keys');
|
|
78
136
|
});
|
|
79
137
|
|
|
138
|
+
// Regenerate API key
|
|
139
|
+
router.post('/:id/regenerate', async (req, res) => {
|
|
140
|
+
const { id } = req.params;
|
|
141
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
142
|
+
|
|
143
|
+
const agent = getApiKeyById(id);
|
|
144
|
+
if (!agent) {
|
|
145
|
+
return wantsJson
|
|
146
|
+
? res.status(404).json({ error: 'Agent not found' })
|
|
147
|
+
: res.status(404).send('Agent not found');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const newKey = await regenerateApiKey(id);
|
|
152
|
+
const keys = listApiKeys();
|
|
153
|
+
|
|
154
|
+
if (wantsJson) {
|
|
155
|
+
return res.json({ success: true, key: newKey.key, keyPrefix: newKey.keyPrefix, name: newKey.name, keys });
|
|
156
|
+
}
|
|
157
|
+
res.send(renderKeysPage(keys, null, newKey));
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error('Key regeneration error:', err);
|
|
160
|
+
return wantsJson
|
|
161
|
+
? res.status(500).json({ error: err.message || 'Failed to regenerate key' })
|
|
162
|
+
: res.send(renderKeysPage(listApiKeys(), err.message || 'Failed to regenerate key'));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
// Toggle agent enabled status
|
|
168
|
+
router.post('/:id/toggle-enabled', (req, res) => {
|
|
169
|
+
const { id } = req.params;
|
|
170
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
171
|
+
|
|
172
|
+
const agent = getApiKeyById(id);
|
|
173
|
+
if (!agent) {
|
|
174
|
+
return wantsJson
|
|
175
|
+
? res.status(404).json({ error: 'Agent not found' })
|
|
176
|
+
: res.status(404).send('Agent not found');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const newEnabled = agent.enabled === 0 ? 1 : 0;
|
|
180
|
+
setAgentEnabled(id, newEnabled);
|
|
181
|
+
|
|
182
|
+
const keys = listApiKeys();
|
|
183
|
+
if (wantsJson) {
|
|
184
|
+
return res.json({ success: true, enabled: newEnabled === 1, keys });
|
|
185
|
+
}
|
|
186
|
+
res.redirect('/ui/agents');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Avatar routes
|
|
190
|
+
|
|
191
|
+
// Get avatar for an agent by name
|
|
192
|
+
router.get('/avatar/:name', (req, res) => {
|
|
193
|
+
const { name } = req.params;
|
|
194
|
+
const filename = getAvatarFilename(name);
|
|
195
|
+
|
|
196
|
+
if (filename) {
|
|
197
|
+
const filepath = join(getAvatarsDir(), filename);
|
|
198
|
+
return res.sendFile(filepath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Return 404 - client should handle with fallback/initials
|
|
202
|
+
res.status(404).send('Avatar not found');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Upload avatar for an agent (by id)
|
|
206
|
+
router.post('/:id/avatar', (req, res) => {
|
|
207
|
+
const { id } = req.params;
|
|
208
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
209
|
+
|
|
210
|
+
const agent = getApiKeyById(id);
|
|
211
|
+
if (!agent) {
|
|
212
|
+
return wantsJson
|
|
213
|
+
? res.status(404).json({ error: 'Agent not found' })
|
|
214
|
+
: res.status(404).send('Agent not found');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if file was uploaded
|
|
218
|
+
if (!req.body || !req.body.avatar) {
|
|
219
|
+
return wantsJson
|
|
220
|
+
? res.status(400).json({ error: 'No avatar data provided' })
|
|
221
|
+
: res.status(400).send('No avatar data provided');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// Expect base64 encoded image with data URI prefix
|
|
226
|
+
const avatarData = req.body.avatar;
|
|
227
|
+
const matches = avatarData.match(/^data:image\/(png|jpeg|jpg|gif|webp);base64,(.+)$/);
|
|
228
|
+
|
|
229
|
+
if (!matches) {
|
|
230
|
+
return wantsJson
|
|
231
|
+
? res.status(400).json({ error: 'Invalid image format. Use base64 data URI.' })
|
|
232
|
+
: res.status(400).send('Invalid image format');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const ext = matches[1] === 'jpeg' ? 'jpg' : matches[1];
|
|
236
|
+
const base64Data = matches[2];
|
|
237
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
238
|
+
|
|
239
|
+
// Size limit: 500KB
|
|
240
|
+
if (buffer.length > 500 * 1024) {
|
|
241
|
+
return wantsJson
|
|
242
|
+
? res.status(400).json({ error: 'Avatar too large. Maximum size is 500KB.' })
|
|
243
|
+
: res.status(400).send('Avatar too large');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Delete any existing avatar for this agent
|
|
247
|
+
deleteAgentAvatar(agent.name);
|
|
248
|
+
|
|
249
|
+
// Save new avatar
|
|
250
|
+
const safeName = agent.name.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
251
|
+
const filename = `${safeName}.${ext}`;
|
|
252
|
+
const filepath = join(getAvatarsDir(), filename);
|
|
253
|
+
writeFileSync(filepath, buffer);
|
|
254
|
+
|
|
255
|
+
if (wantsJson) {
|
|
256
|
+
return res.json({ success: true, filename, url: `/ui/keys/avatar/${encodeURIComponent(agent.name)}` });
|
|
257
|
+
}
|
|
258
|
+
res.redirect('/ui/keys');
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error('Avatar upload error:', err);
|
|
261
|
+
return wantsJson
|
|
262
|
+
? res.status(500).json({ error: 'Failed to save avatar' })
|
|
263
|
+
: res.status(500).send('Failed to save avatar');
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Delete avatar for an agent
|
|
268
|
+
router.delete('/:id/avatar', (req, res) => {
|
|
269
|
+
const { id } = req.params;
|
|
270
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
271
|
+
|
|
272
|
+
const agent = getApiKeyById(id);
|
|
273
|
+
if (!agent) {
|
|
274
|
+
return wantsJson
|
|
275
|
+
? res.status(404).json({ error: 'Agent not found' })
|
|
276
|
+
: res.status(404).send('Agent not found');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
deleteAgentAvatar(agent.name);
|
|
280
|
+
|
|
281
|
+
if (wantsJson) {
|
|
282
|
+
return res.json({ success: true });
|
|
283
|
+
}
|
|
284
|
+
res.redirect('/ui/keys');
|
|
285
|
+
});
|
|
286
|
+
|
|
80
287
|
// Render function
|
|
81
288
|
function renderKeysPage(keys, error = null, newKey = null) {
|
|
82
289
|
const renderKeyRow = (k) => `
|
|
83
|
-
<tr id="key-${k.id}">
|
|
84
|
-
<td
|
|
290
|
+
<tr id="key-${k.id}" class="${k.enabled === 0 ? 'agent-disabled' : ''}">
|
|
291
|
+
<td>
|
|
292
|
+
<div class="agent-with-avatar">
|
|
293
|
+
<span class="avatar-clickable" data-id="${k.id}" data-name="${escapeHtml(k.name)}" title="Click to change avatar">
|
|
294
|
+
${renderAvatar(k.name, { size: 32 })}
|
|
295
|
+
</span>
|
|
296
|
+
<div>
|
|
297
|
+
<strong>${escapeHtml(k.name)}</strong>
|
|
298
|
+
${k.enabled === 0 ? '<span class="status-disabled">Disabled</span>' : ''}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</td>
|
|
85
302
|
<td><code class="key-value">${escapeHtml(k.key_prefix)}</code></td>
|
|
86
303
|
<td>
|
|
87
304
|
${k.webhook_url ? `
|
|
@@ -92,7 +309,9 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
92
309
|
<button type="button" class="btn-sm webhook-btn" data-id="${k.id}" data-name="${escapeHtml(k.name)}" data-url="${escapeHtml(k.webhook_url || '')}" data-token="${escapeHtml(k.webhook_token || '')}">Configure</button>
|
|
93
310
|
</td>
|
|
94
311
|
<td>${formatDate(k.created_at)}</td>
|
|
95
|
-
<td>
|
|
312
|
+
<td style="white-space: nowrap;">
|
|
313
|
+
<button type="button" class="btn-sm btn-regen" data-id="${k.id}" data-name="${escapeHtml(k.name)}" data-prefix="${escapeHtml(k.key_prefix)}" title="Regenerate API Key">🔄</button>
|
|
314
|
+
<button type="button" class="btn-sm btn-toggle ${k.enabled === 0 ? 'btn-enable' : 'btn-disable'}" onclick="toggleEnabled('${k.id}')" title="${k.enabled === 0 ? 'Enable' : 'Disable'}">${k.enabled === 0 ? '✓' : '⏸'}</button>
|
|
96
315
|
<button type="button" class="delete-btn" onclick="deleteKey('${k.id}')" title="Delete">×</button>
|
|
97
316
|
</td>
|
|
98
317
|
</tr>
|
|
@@ -101,9 +320,10 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
101
320
|
return `<!DOCTYPE html>
|
|
102
321
|
<html>
|
|
103
322
|
<head>
|
|
104
|
-
<title>agentgate -
|
|
323
|
+
<title>agentgate - Agents</title>
|
|
105
324
|
<link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
|
|
106
325
|
<link rel="stylesheet" href="/public/style.css">
|
|
326
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
107
327
|
<style>
|
|
108
328
|
.keys-table { width: 100%; border-collapse: collapse; margin-top: 16px; }
|
|
109
329
|
.keys-table th, .keys-table td { padding: 12px; text-align: left; border-bottom: 1px solid #374151; }
|
|
@@ -113,6 +333,8 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
113
333
|
.new-key-banner code { background: #1f2937; color: #10b981; padding: 8px 12px; border-radius: 4px; display: block; margin-top: 8px; font-size: 14px; word-break: break-all; }
|
|
114
334
|
.delete-btn { background: none; border: none; color: #f87171; font-size: 20px; cursor: pointer; padding: 0 4px; line-height: 1; font-weight: bold; }
|
|
115
335
|
.delete-btn:hover { color: #dc2626; }
|
|
336
|
+
.btn-regen { background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.3); color: #fbbf24; font-size: 14px; padding: 4px 8px; cursor: pointer; border-radius: 4px; margin-right: 8px; }
|
|
337
|
+
.btn-regen:hover { background: rgba(245, 158, 11, 0.25); border-color: rgba(245, 158, 11, 0.5); }
|
|
116
338
|
.back-link { color: #a78bfa; text-decoration: none; font-weight: 500; }
|
|
117
339
|
.back-link:hover { text-decoration: underline; }
|
|
118
340
|
.error-message { background: #7f1d1d; color: #fecaca; padding: 12px; border-radius: 8px; margin-bottom: 16px; }
|
|
@@ -121,6 +343,9 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
121
343
|
.webhook-none { background: #374151; color: #9ca3af; }
|
|
122
344
|
.btn-sm { font-size: 12px; padding: 4px 8px; background: #4f46e5; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
|
123
345
|
.btn-sm:hover { background: #4338ca; }
|
|
346
|
+
.btn-test { padding: 10px 20px; background: #059669; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; }
|
|
347
|
+
.btn-test:hover { background: #047857; }
|
|
348
|
+
.btn-test:disabled { background: #6b7280; cursor: not-allowed; }
|
|
124
349
|
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 1000; align-items: center; justify-content: center; }
|
|
125
350
|
.modal-overlay.active { display: flex; }
|
|
126
351
|
.modal { background: #1f2937; border-radius: 12px; padding: 24px; max-width: 500px; width: 90%; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
|
|
@@ -130,12 +355,21 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
130
355
|
.modal input:focus { border-color: #6366f1; outline: none; }
|
|
131
356
|
.modal-buttons { display: flex; gap: 12px; justify-content: flex-end; margin-top: 16px; }
|
|
132
357
|
.modal .help-text { font-size: 12px; color: #9ca3af; margin-top: -8px; margin-bottom: 12px; }
|
|
358
|
+
.avatar-clickable { cursor: pointer; display: inline-block; border-radius: 50%; transition: transform 0.15s ease, box-shadow 0.15s ease; }
|
|
359
|
+
.avatar-clickable:hover { transform: scale(1.1); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.4); }
|
|
360
|
+
|
|
361
|
+
.agent-disabled { opacity: 0.5; }
|
|
362
|
+
.status-disabled { background: #7f1d1d; color: #fca5a5; font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 8px; }
|
|
363
|
+
.btn-toggle { margin-right: 8px; }
|
|
364
|
+
.btn-enable { background: #065f46; border-color: #10b981; color: #6ee7b7; }
|
|
365
|
+
.btn-enable:hover { background: #047857; }
|
|
366
|
+
.btn-disable { background: #7f1d1d; border-color: #ef4444; color: #fca5a5; }
|
|
367
|
+
.btn-disable:hover { background: #991b1b; }
|
|
133
368
|
</style>
|
|
134
369
|
</head>
|
|
135
370
|
<body>
|
|
136
|
-
<div
|
|
137
|
-
|
|
138
|
-
<a href="/ui" class="back-link">← Back to Dashboard</a>
|
|
371
|
+
<div>
|
|
372
|
+
${simpleNavHeader()}
|
|
139
373
|
</div>
|
|
140
374
|
<p>Manage API keys for your agents. Keys are hashed and can only be viewed once at creation.</p>
|
|
141
375
|
|
|
@@ -201,9 +435,72 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
201
435
|
|
|
202
436
|
<div class="modal-buttons">
|
|
203
437
|
<button type="button" class="btn-secondary" onclick="closeWebhookModal()">Cancel</button>
|
|
438
|
+
<button type="button" id="webhook-test-btn" class="btn-test" onclick="testWebhook()" style="display: none;">Test</button>
|
|
204
439
|
<button type="submit" class="btn-primary">Save Webhook</button>
|
|
205
440
|
</div>
|
|
206
441
|
</form>
|
|
442
|
+
<div id="webhook-test-result" style="margin-top: 12px; display: none;"></div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<!-- Regenerate Key Modal -->
|
|
447
|
+
<div id="regen-modal" class="modal-overlay">
|
|
448
|
+
<div class="modal">
|
|
449
|
+
<h3>🔄 Regenerate API Key</h3>
|
|
450
|
+
<!-- Confirmation view -->
|
|
451
|
+
<div id="regen-confirm-view">
|
|
452
|
+
<p style="color: #fbbf24; font-size: 14px; margin-bottom: 16px; background: rgba(245, 158, 11, 0.1); padding: 12px; border-radius: 8px; border: 1px solid rgba(245, 158, 11, 0.3);">
|
|
453
|
+
⚠️ <strong>Warning:</strong> This will immediately invalidate the current API key. Any agents using it will lose access until updated with the new key.
|
|
454
|
+
</p>
|
|
455
|
+
<p style="color: #9ca3af; margin-bottom: 8px;">Agent: <strong id="regen-agent-name" style="color: #f3f4f6;"></strong></p>
|
|
456
|
+
<p style="color: #9ca3af; margin-bottom: 16px;">Current key: <code id="regen-key-prefix" style="background: #374151; padding: 2px 6px; border-radius: 4px;"></code></p>
|
|
457
|
+
<input type="hidden" id="regen-agent-id">
|
|
458
|
+
<div class="modal-buttons">
|
|
459
|
+
<button type="button" class="btn-secondary" onclick="closeRegenModal()">Cancel</button>
|
|
460
|
+
<button type="button" id="regen-confirm-btn" class="btn-danger" onclick="confirmRegenerate()">Regenerate Key</button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
<!-- Success view with new key -->
|
|
464
|
+
<div id="regen-success-view" style="display: none;">
|
|
465
|
+
<p style="color: #34d399; font-size: 14px; margin-bottom: 16px; background: rgba(16, 185, 129, 0.1); padding: 12px; border-radius: 8px; border: 1px solid rgba(16, 185, 129, 0.3);">
|
|
466
|
+
✅ <strong>Key regenerated!</strong> Copy it now - you won't be able to see it again.
|
|
467
|
+
</p>
|
|
468
|
+
<p style="color: #9ca3af; margin-bottom: 8px;">Agent: <strong id="regen-success-name" style="color: #f3f4f6;"></strong></p>
|
|
469
|
+
<div style="background: #1f2937; padding: 12px; border-radius: 8px; margin-bottom: 16px; word-break: break-all;">
|
|
470
|
+
<code id="regen-new-key" style="color: #34d399; font-size: 14px;"></code>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="modal-buttons">
|
|
473
|
+
<button type="button" id="regen-copy-btn" class="btn-primary" onclick="copyRegenKey()">Copy to Clipboard</button>
|
|
474
|
+
<button type="button" class="btn-secondary" onclick="closeRegenModalAndRefresh()">Done</button>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<!-- Avatar Modal -->
|
|
481
|
+
<div id="avatar-modal" class="modal-overlay">
|
|
482
|
+
<div class="modal">
|
|
483
|
+
<h3>Avatar for <span id="avatar-agent-name"></span></h3>
|
|
484
|
+
<p style="color: #9ca3af; font-size: 14px; margin-bottom: 16px;">
|
|
485
|
+
Upload an image (PNG, JPG, GIF, WebP). Max size: 500KB.
|
|
486
|
+
</p>
|
|
487
|
+
<input type="hidden" id="avatar-agent-id">
|
|
488
|
+
|
|
489
|
+
<div id="avatar-preview-container" style="text-align: center; margin-bottom: 16px;">
|
|
490
|
+
<div id="avatar-preview" style="width: 80px; height: 80px; border-radius: 50%; margin: 0 auto; background: #374151; display: flex; align-items: center; justify-content: center; overflow: hidden;">
|
|
491
|
+
<span id="avatar-preview-text" style="color: #9ca3af;">No image</span>
|
|
492
|
+
<img id="avatar-preview-img" style="width: 100%; height: 100%; object-fit: cover; display: none;">
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<input type="file" id="avatar-file" accept="image/png,image/jpeg,image/gif,image/webp" style="margin-bottom: 16px;">
|
|
497
|
+
<p class="help-text">Select an image file to upload</p>
|
|
498
|
+
|
|
499
|
+
<div class="modal-buttons">
|
|
500
|
+
<button type="button" class="btn-secondary" onclick="closeAvatarModal()">Cancel</button>
|
|
501
|
+
<button type="button" id="avatar-delete-btn" class="btn-danger" onclick="deleteAvatar()" style="display: none;">Delete</button>
|
|
502
|
+
<button type="button" id="avatar-upload-btn" class="btn-primary" onclick="uploadAvatar()" disabled>Upload</button>
|
|
503
|
+
</div>
|
|
207
504
|
</div>
|
|
208
505
|
</div>
|
|
209
506
|
|
|
@@ -221,6 +518,12 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
221
518
|
document.getElementById('modal-agent-name').textContent = btn.dataset.name;
|
|
222
519
|
document.getElementById('webhook-url').value = btn.dataset.url;
|
|
223
520
|
document.getElementById('webhook-token').value = btn.dataset.token;
|
|
521
|
+
// Show test button if webhook URL is already configured
|
|
522
|
+
const testBtn = document.getElementById('webhook-test-btn');
|
|
523
|
+
const testResult = document.getElementById('webhook-test-result');
|
|
524
|
+
testBtn.style.display = btn.dataset.url ? 'inline-block' : 'none';
|
|
525
|
+
testResult.style.display = 'none';
|
|
526
|
+
testResult.innerHTML = '';
|
|
224
527
|
document.getElementById('webhook-modal').classList.add('active');
|
|
225
528
|
}
|
|
226
529
|
|
|
@@ -228,6 +531,113 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
228
531
|
document.getElementById('webhook-modal').classList.remove('active');
|
|
229
532
|
}
|
|
230
533
|
|
|
534
|
+
// Regenerate key modal functions
|
|
535
|
+
let regenNewKey = null;
|
|
536
|
+
|
|
537
|
+
function showRegenModal(btn) {
|
|
538
|
+
document.getElementById('regen-agent-id').value = btn.dataset.id;
|
|
539
|
+
document.getElementById('regen-agent-name').textContent = btn.dataset.name;
|
|
540
|
+
document.getElementById('regen-key-prefix').textContent = btn.dataset.prefix;
|
|
541
|
+
// Reset to confirmation view
|
|
542
|
+
document.getElementById('regen-confirm-view').style.display = '';
|
|
543
|
+
document.getElementById('regen-success-view').style.display = 'none';
|
|
544
|
+
document.getElementById('regen-confirm-btn').disabled = false;
|
|
545
|
+
document.getElementById('regen-confirm-btn').textContent = 'Regenerate Key';
|
|
546
|
+
regenNewKey = null;
|
|
547
|
+
document.getElementById('regen-modal').classList.add('active');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function closeRegenModal() {
|
|
551
|
+
document.getElementById('regen-modal').classList.remove('active');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function closeRegenModalAndRefresh() {
|
|
555
|
+
closeRegenModal();
|
|
556
|
+
window.location.reload();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function copyRegenKey() {
|
|
560
|
+
if (!regenNewKey) return;
|
|
561
|
+
navigator.clipboard.writeText(regenNewKey).then(() => {
|
|
562
|
+
const btn = document.getElementById('regen-copy-btn');
|
|
563
|
+
const orig = btn.textContent;
|
|
564
|
+
btn.textContent = 'Copied!';
|
|
565
|
+
setTimeout(() => btn.textContent = orig, 1500);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function confirmRegenerate() {
|
|
570
|
+
const id = document.getElementById('regen-agent-id').value;
|
|
571
|
+
const btn = document.getElementById('regen-confirm-btn');
|
|
572
|
+
btn.disabled = true;
|
|
573
|
+
btn.textContent = 'Regenerating...';
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const res = await fetch('/ui/keys/' + id + '/regenerate', {
|
|
577
|
+
method: 'POST',
|
|
578
|
+
headers: { 'Accept': 'application/json' }
|
|
579
|
+
});
|
|
580
|
+
const data = await res.json();
|
|
581
|
+
|
|
582
|
+
if (data.success) {
|
|
583
|
+
// Show success view with new key
|
|
584
|
+
regenNewKey = data.key;
|
|
585
|
+
document.getElementById('regen-success-name').textContent = data.name;
|
|
586
|
+
document.getElementById('regen-new-key').textContent = data.key;
|
|
587
|
+
document.getElementById('regen-confirm-view').style.display = 'none';
|
|
588
|
+
document.getElementById('regen-success-view').style.display = '';
|
|
589
|
+
} else {
|
|
590
|
+
alert(data.error || 'Failed to regenerate key');
|
|
591
|
+
btn.disabled = false;
|
|
592
|
+
btn.textContent = 'Regenerate Key';
|
|
593
|
+
}
|
|
594
|
+
} catch (err) {
|
|
595
|
+
alert('Error: ' + err.message);
|
|
596
|
+
btn.disabled = false;
|
|
597
|
+
btn.textContent = 'Regenerate Key';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
document.querySelectorAll('.btn-regen').forEach(btn => {
|
|
602
|
+
btn.addEventListener('click', () => showRegenModal(btn));
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
document.getElementById('regen-modal').addEventListener('click', (e) => {
|
|
606
|
+
if (e.target.classList.contains('modal-overlay')) {
|
|
607
|
+
closeRegenModal();
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
async function testWebhook() {
|
|
612
|
+
const id = document.getElementById('webhook-agent-id').value;
|
|
613
|
+
const testBtn = document.getElementById('webhook-test-btn');
|
|
614
|
+
const testResult = document.getElementById('webhook-test-result');
|
|
615
|
+
|
|
616
|
+
testBtn.disabled = true;
|
|
617
|
+
testBtn.textContent = 'Testing...';
|
|
618
|
+
testResult.style.display = 'block';
|
|
619
|
+
testResult.innerHTML = '<span style="color: #9ca3af;">Sending test webhook...</span>';
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const res = await fetch('/ui/keys/' + id + '/test-webhook', {
|
|
623
|
+
method: 'POST',
|
|
624
|
+
headers: { 'Accept': 'application/json' }
|
|
625
|
+
});
|
|
626
|
+
const data = await res.json();
|
|
627
|
+
|
|
628
|
+
if (data.success) {
|
|
629
|
+
testResult.innerHTML = '<span style="color: #34d399;">✓ ' + data.message + '</span>';
|
|
630
|
+
} else {
|
|
631
|
+
testResult.innerHTML = '<span style="color: #f87171;">✗ ' + data.message + '</span>';
|
|
632
|
+
}
|
|
633
|
+
} catch (err) {
|
|
634
|
+
testResult.innerHTML = '<span style="color: #f87171;">✗ Error: ' + err.message + '</span>';
|
|
635
|
+
} finally {
|
|
636
|
+
testBtn.disabled = false;
|
|
637
|
+
testBtn.textContent = 'Test';
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
231
641
|
document.querySelectorAll('.webhook-btn').forEach(btn => {
|
|
232
642
|
btn.addEventListener('click', () => showWebhookModal(btn));
|
|
233
643
|
});
|
|
@@ -263,6 +673,23 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
263
673
|
}
|
|
264
674
|
});
|
|
265
675
|
|
|
676
|
+
async function toggleEnabled(id) {
|
|
677
|
+
try {
|
|
678
|
+
const res = await fetch('/ui/keys/' + id + '/toggle-enabled', {
|
|
679
|
+
method: 'POST',
|
|
680
|
+
headers: { 'Accept': 'application/json' }
|
|
681
|
+
});
|
|
682
|
+
const data = await res.json();
|
|
683
|
+
if (data.success) {
|
|
684
|
+
window.location.reload();
|
|
685
|
+
} else {
|
|
686
|
+
alert(data.error || 'Failed to toggle agent status');
|
|
687
|
+
}
|
|
688
|
+
} catch (err) {
|
|
689
|
+
alert('Error: ' + err.message);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
266
693
|
async function deleteKey(id) {
|
|
267
694
|
if (!confirm('Delete this API key? Any agents using it will lose access.')) return;
|
|
268
695
|
|
|
@@ -294,7 +721,130 @@ function renderKeysPage(keys, error = null, newKey = null) {
|
|
|
294
721
|
alert('Error: ' + err.message);
|
|
295
722
|
}
|
|
296
723
|
}
|
|
724
|
+
|
|
725
|
+
// Avatar functionality
|
|
726
|
+
let currentAvatarData = null;
|
|
727
|
+
|
|
728
|
+
function showAvatarModal(btn) {
|
|
729
|
+
const id = btn.dataset.id;
|
|
730
|
+
const name = btn.dataset.name;
|
|
731
|
+
document.getElementById('avatar-agent-id').value = id;
|
|
732
|
+
document.getElementById('avatar-agent-name').textContent = name;
|
|
733
|
+
document.getElementById('avatar-file').value = '';
|
|
734
|
+
document.getElementById('avatar-upload-btn').disabled = true;
|
|
735
|
+
currentAvatarData = null;
|
|
736
|
+
|
|
737
|
+
// Try to load existing avatar
|
|
738
|
+
const img = document.getElementById('avatar-preview-img');
|
|
739
|
+
const text = document.getElementById('avatar-preview-text');
|
|
740
|
+
img.src = '/ui/keys/avatar/' + encodeURIComponent(name) + '?t=' + Date.now();
|
|
741
|
+
img.onload = function() {
|
|
742
|
+
img.style.display = 'block';
|
|
743
|
+
text.style.display = 'none';
|
|
744
|
+
document.getElementById('avatar-delete-btn').style.display = '';
|
|
745
|
+
};
|
|
746
|
+
img.onerror = function() {
|
|
747
|
+
img.style.display = 'none';
|
|
748
|
+
text.style.display = '';
|
|
749
|
+
document.getElementById('avatar-delete-btn').style.display = 'none';
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
document.getElementById('avatar-modal').classList.add('active');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function closeAvatarModal() {
|
|
756
|
+
document.getElementById('avatar-modal').classList.remove('active');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
document.querySelectorAll('.avatar-clickable').forEach(el => {
|
|
760
|
+
el.addEventListener('click', () => showAvatarModal(el));
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
document.getElementById('avatar-file').addEventListener('change', function(e) {
|
|
764
|
+
const file = e.target.files[0];
|
|
765
|
+
if (!file) return;
|
|
766
|
+
|
|
767
|
+
if (file.size > 500 * 1024) {
|
|
768
|
+
alert('File too large. Maximum size is 500KB.');
|
|
769
|
+
e.target.value = '';
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const reader = new FileReader();
|
|
774
|
+
reader.onload = function(event) {
|
|
775
|
+
currentAvatarData = event.target.result;
|
|
776
|
+
const img = document.getElementById('avatar-preview-img');
|
|
777
|
+
const text = document.getElementById('avatar-preview-text');
|
|
778
|
+
img.src = currentAvatarData;
|
|
779
|
+
img.style.display = 'block';
|
|
780
|
+
text.style.display = 'none';
|
|
781
|
+
document.getElementById('avatar-upload-btn').disabled = false;
|
|
782
|
+
};
|
|
783
|
+
reader.readAsDataURL(file);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
async function uploadAvatar() {
|
|
787
|
+
if (!currentAvatarData) return;
|
|
788
|
+
|
|
789
|
+
const id = document.getElementById('avatar-agent-id').value;
|
|
790
|
+
const btn = document.getElementById('avatar-upload-btn');
|
|
791
|
+
btn.disabled = true;
|
|
792
|
+
btn.textContent = 'Uploading...';
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const res = await fetch('/ui/keys/' + id + '/avatar', {
|
|
796
|
+
method: 'POST',
|
|
797
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
798
|
+
body: JSON.stringify({ avatar: currentAvatarData })
|
|
799
|
+
});
|
|
800
|
+
const data = await res.json();
|
|
801
|
+
|
|
802
|
+
if (data.success) {
|
|
803
|
+
closeAvatarModal();
|
|
804
|
+
window.location.reload();
|
|
805
|
+
} else {
|
|
806
|
+
alert(data.error || 'Failed to upload avatar');
|
|
807
|
+
btn.disabled = false;
|
|
808
|
+
btn.textContent = 'Upload';
|
|
809
|
+
}
|
|
810
|
+
} catch (err) {
|
|
811
|
+
alert('Error: ' + err.message);
|
|
812
|
+
btn.disabled = false;
|
|
813
|
+
btn.textContent = 'Upload';
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function deleteAvatar() {
|
|
818
|
+
if (!confirm('Delete this avatar?')) return;
|
|
819
|
+
|
|
820
|
+
const id = document.getElementById('avatar-agent-id').value;
|
|
821
|
+
|
|
822
|
+
try {
|
|
823
|
+
const res = await fetch('/ui/keys/' + id + '/avatar', {
|
|
824
|
+
method: 'DELETE',
|
|
825
|
+
headers: { 'Accept': 'application/json' }
|
|
826
|
+
});
|
|
827
|
+
const data = await res.json();
|
|
828
|
+
|
|
829
|
+
if (data.success) {
|
|
830
|
+
closeAvatarModal();
|
|
831
|
+
window.location.reload();
|
|
832
|
+
} else {
|
|
833
|
+
alert(data.error || 'Failed to delete avatar');
|
|
834
|
+
}
|
|
835
|
+
} catch (err) {
|
|
836
|
+
alert('Error: ' + err.message);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
document.getElementById('avatar-modal').addEventListener('click', (e) => {
|
|
841
|
+
if (e.target.classList.contains('modal-overlay')) {
|
|
842
|
+
closeAvatarModal();
|
|
843
|
+
}
|
|
844
|
+
});
|
|
297
845
|
</script>
|
|
846
|
+
${socketScript()}
|
|
847
|
+
${localizeScript()}
|
|
298
848
|
</body>
|
|
299
849
|
</html>`;
|
|
300
850
|
}
|