@agenticmail/enterprise 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. package/tsconfig.json +14 -0
@@ -0,0 +1,535 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AgenticMail Enterprise Dashboard</title>
7
+ <style>
8
+ /*
9
+ * AgenticMail Enterprise — Pure HTML Dashboard
10
+ *
11
+ * ZERO dependencies. No Node.js, no npm, no React, no build tools.
12
+ * Just open this file in any browser.
13
+ *
14
+ * Setup:
15
+ * 1. Edit the API_URL below to point to your AgenticMail Enterprise server
16
+ * 2. Open this file in any browser (or host it on any web server)
17
+ * 3. Login with your admin credentials
18
+ *
19
+ * That's it. No coding required.
20
+ */
21
+
22
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
23
+ :root {
24
+ --bg: #0a0a0f; --surface: #12121a; --surface-hover: #1a1a25;
25
+ --border: #1e1e2e; --border-hover: #2e2e4e;
26
+ --text: #e4e4ef; --text-dim: #8888a0; --text-muted: #55556a;
27
+ --primary: #6366f1; --primary-hover: #818cf8; --primary-bg: rgba(99,102,241,0.12);
28
+ --success: #22c55e; --success-bg: rgba(34,197,94,0.12);
29
+ --warning: #f59e0b; --warning-bg: rgba(245,158,11,0.12);
30
+ --danger: #ef4444; --danger-bg: rgba(239,68,68,0.12);
31
+ --radius: 8px; --radius-lg: 12px;
32
+ --shadow: 0 1px 3px rgba(0,0,0,0.3);
33
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
34
+ }
35
+ body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }
36
+
37
+ /* ─── Setup Banner ─────────────────────────── */
38
+ #setup-banner { display: none; background: var(--warning-bg); border: 1px solid var(--warning); border-radius: var(--radius); padding: 16px 20px; margin: 20px; }
39
+ #setup-banner h3 { color: var(--warning); margin-bottom: 8px; }
40
+ #setup-banner input { padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; width: 400px; max-width: 100%; }
41
+ #setup-banner button { padding: 8px 16px; background: var(--warning); border: none; border-radius: var(--radius); color: #000; font-weight: 600; cursor: pointer; margin-left: 8px; }
42
+
43
+ /* ─── Login ─────────────────────────────────── */
44
+ #login-screen { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
45
+ #login-box { width: 380px; max-width: 90vw; }
46
+ #login-box h1 { text-align: center; font-size: 22px; margin-bottom: 4px; }
47
+ #login-box .subtitle { text-align: center; color: var(--text-dim); font-size: 13px; margin-bottom: 32px; }
48
+ #login-error { display: none; background: var(--danger-bg); border: 1px solid var(--danger); border-radius: var(--radius); padding: 10px 14px; margin-bottom: 16px; font-size: 13px; color: var(--danger); }
49
+ .form-group { margin-bottom: 14px; }
50
+ .form-label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 4px; font-weight: 500; }
51
+ .input { width: 100%; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; outline: none; transition: border-color 0.15s; }
52
+ .input:focus { border-color: var(--primary); }
53
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 20px; border-radius: var(--radius); font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--text); transition: all 0.15s; text-decoration: none; }
54
+ .btn:hover { border-color: var(--border-hover); background: var(--surface-hover); }
55
+ .btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; width: 100%; }
56
+ .btn-primary:hover { background: var(--primary-hover); }
57
+ .btn-sm { padding: 6px 12px; font-size: 12px; }
58
+ .btn-danger { color: var(--danger); border-color: var(--danger); }
59
+ .btn-danger:hover { background: var(--danger-bg); }
60
+
61
+ /* ─── Layout ────────────────────────────────── */
62
+ #app { display: none; }
63
+ .layout { display: flex; min-height: 100vh; }
64
+ .sidebar { width: 240px; background: var(--surface); border-right: 1px solid var(--border); position: fixed; top: 0; left: 0; bottom: 0; display: flex; flex-direction: column; }
65
+ .sidebar-header { padding: 20px; border-bottom: 1px solid var(--border); }
66
+ .sidebar-header h2 { font-size: 16px; letter-spacing: -0.02em; }
67
+ .sidebar-header h2 em { font-style: normal; color: var(--primary); }
68
+ .sidebar-header p { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
69
+ .nav { flex: 1; padding: 8px 0; }
70
+ .nav-section { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); padding: 12px 20px 4px; }
71
+ .nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 20px; color: var(--text-dim); cursor: pointer; font-size: 13px; border: none; background: none; width: 100%; text-align: left; transition: all 0.1s; }
72
+ .nav-item:hover { color: var(--text); background: rgba(255,255,255,0.03); }
73
+ .nav-item.active { color: var(--primary); background: var(--primary-bg); border-right: 2px solid var(--primary); }
74
+ .sidebar-footer { padding: 16px 20px; border-top: 1px solid var(--border); font-size: 12px; }
75
+ .sidebar-footer .name { color: var(--text-dim); }
76
+ .sidebar-footer .email { color: var(--text-muted); font-size: 11px; }
77
+ .sidebar-footer a { color: var(--text-muted); font-size: 11px; cursor: pointer; margin-top: 6px; display: inline-block; }
78
+
79
+ .content { flex: 1; margin-left: 240px; padding: 32px; max-width: 1100px; }
80
+ .page-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 4px; }
81
+ .page-desc { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; }
82
+
83
+ /* ─── Cards & Stats ────────────────────────── */
84
+ .stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
85
+ .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; }
86
+ .stat-card .label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
87
+ .stat-card .value { font-size: 30px; font-weight: 700; margin-top: 4px; letter-spacing: -0.03em; }
88
+
89
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 16px; box-shadow: var(--shadow); }
90
+ .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
91
+ .card-title { font-size: 13px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
92
+
93
+ /* ─── Table ─────────────────────────────────── */
94
+ .table-wrap { overflow-x: auto; }
95
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
96
+ th { text-align: left; padding: 10px 12px; color: var(--text-muted); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
97
+ td { padding: 12px; border-bottom: 1px solid var(--border); }
98
+ tr:hover td { background: rgba(255,255,255,0.015); }
99
+ .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; }
100
+ .badge-active { background: var(--success-bg); color: var(--success); }
101
+ .badge-archived { background: rgba(136,136,160,0.12); color: var(--text-dim); }
102
+ .badge-suspended { background: var(--danger-bg); color: var(--danger); }
103
+ .badge-owner { background: var(--warning-bg); color: var(--warning); }
104
+ .badge-admin { background: var(--primary-bg); color: var(--primary); }
105
+ .badge-member { background: rgba(136,136,160,0.08); color: var(--text-dim); }
106
+ .badge-viewer { background: rgba(136,136,160,0.06); color: var(--text-muted); }
107
+ .empty { text-align: center; padding: 48px 20px; color: var(--text-muted); }
108
+ .empty-icon { font-size: 36px; margin-bottom: 10px; }
109
+
110
+ /* ─── Modal ─────────────────────────────────── */
111
+ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); align-items: center; justify-content: center; z-index: 100; }
112
+ .modal-overlay.open { display: flex; }
113
+ .modal { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px; width: 440px; max-width: 90vw; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
114
+ .modal h3 { font-size: 16px; font-weight: 700; margin-bottom: 16px; }
115
+ .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
116
+
117
+ /* ─── Toast ──────────────────────────────────── */
118
+ #toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; background: var(--surface); border: 1px solid var(--success); border-radius: var(--radius); font-size: 13px; z-index: 200; display: none; animation: slideUp 0.2s ease; }
119
+ #toast.error { border-color: var(--danger); }
120
+ @keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
121
+
122
+ /* ─── Responsive ────────────────────────────── */
123
+ @media (max-width: 768px) {
124
+ .sidebar { width: 56px; }
125
+ .sidebar-header h2, .sidebar-header p, .nav-item span, .nav-section, .sidebar-footer { display: none; }
126
+ .nav-item { justify-content: center; padding: 14px 0; font-size: 18px; }
127
+ .content { margin-left: 56px; padding: 16px; }
128
+ }
129
+ </style>
130
+ </head>
131
+ <body>
132
+
133
+ <!-- ═══════════════════════════════════════════════════
134
+ CONFIGURATION — Edit this ONE line to get started
135
+ ═══════════════════════════════════════════════════ -->
136
+ <script>
137
+ // 👇 Change this to your AgenticMail Enterprise server URL
138
+ var API_URL = 'http://localhost:3000';
139
+ </script>
140
+
141
+ <!-- Setup banner (shown if API_URL is still localhost) -->
142
+ <div id="setup-banner">
143
+ <h3>⚡ Quick Setup</h3>
144
+ <p style="color:var(--text-dim);font-size:13px;margin-bottom:12px;">Enter your AgenticMail Enterprise server URL to get started:</p>
145
+ <div>
146
+ <input type="text" id="setup-url" placeholder="https://your-company.agenticmail.cloud" value="">
147
+ <button onclick="saveApiUrl()">Connect</button>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Login Screen -->
152
+ <div id="login-screen">
153
+ <div id="login-box">
154
+ <h1>🏢 <em style="color:var(--primary)">AgenticMail</em> Enterprise</h1>
155
+ <p class="subtitle">Sign in to your dashboard</p>
156
+ <div id="login-error"></div>
157
+ <form onsubmit="doLogin(event)">
158
+ <div class="form-group">
159
+ <label class="form-label">Email</label>
160
+ <input class="input" type="email" id="login-email" required autofocus>
161
+ </div>
162
+ <div class="form-group">
163
+ <label class="form-label">Password</label>
164
+ <input class="input" type="password" id="login-password" required>
165
+ </div>
166
+ <button class="btn btn-primary" type="submit" id="login-btn">Sign In</button>
167
+ </form>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Main App -->
172
+ <div id="app">
173
+ <div class="layout">
174
+ <div class="sidebar">
175
+ <div class="sidebar-header">
176
+ <h2>🏢 <em>Agentic</em>Mail</h2>
177
+ <p>Enterprise Dashboard</p>
178
+ </div>
179
+ <div class="nav">
180
+ <div class="nav-section">Overview</div>
181
+ <button class="nav-item active" onclick="navigate('dashboard')" data-page="dashboard">📊 <span>Dashboard</span></button>
182
+ <div class="nav-section">Manage</div>
183
+ <button class="nav-item" onclick="navigate('agents')" data-page="agents">🤖 <span>Agents</span></button>
184
+ <button class="nav-item" onclick="navigate('users')" data-page="users">👥 <span>Users</span></button>
185
+ <button class="nav-item" onclick="navigate('api-keys')" data-page="api-keys">🔑 <span>API Keys</span></button>
186
+ <div class="nav-section">System</div>
187
+ <button class="nav-item" onclick="navigate('audit')" data-page="audit">📋 <span>Audit Log</span></button>
188
+ <button class="nav-item" onclick="navigate('settings')" data-page="settings">⚙️ <span>Settings</span></button>
189
+ </div>
190
+ <div class="sidebar-footer">
191
+ <div class="name" id="user-name">-</div>
192
+ <div class="email" id="user-email">-</div>
193
+ <a onclick="doLogout()">Sign out</a>
194
+ </div>
195
+ </div>
196
+
197
+ <div class="content" id="page-content">
198
+ <!-- Pages injected here by JS -->
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <!-- Create Agent Modal -->
204
+ <div class="modal-overlay" id="modal-agent">
205
+ <div class="modal">
206
+ <h3>Create Agent</h3>
207
+ <form onsubmit="createAgent(event)">
208
+ <div class="form-group"><label class="form-label">Name</label><input class="input" id="agent-name" required placeholder="e.g. researcher"></div>
209
+ <div class="form-group"><label class="form-label">Email (optional)</label><input class="input" id="agent-email" placeholder="auto-generated if blank"></div>
210
+ <div class="form-group"><label class="form-label">Role</label>
211
+ <select class="input" id="agent-role"><option>assistant</option><option>secretary</option><option>researcher</option><option>writer</option><option>custom</option></select>
212
+ </div>
213
+ <div class="modal-actions">
214
+ <button class="btn" type="button" onclick="closeModal('modal-agent')">Cancel</button>
215
+ <button class="btn btn-primary" type="submit" style="width:auto">Create</button>
216
+ </div>
217
+ </form>
218
+ </div>
219
+ </div>
220
+
221
+ <!-- Create User Modal -->
222
+ <div class="modal-overlay" id="modal-user">
223
+ <div class="modal">
224
+ <h3>Create User</h3>
225
+ <form onsubmit="createUser(event)">
226
+ <div class="form-group"><label class="form-label">Name</label><input class="input" id="new-user-name" required></div>
227
+ <div class="form-group"><label class="form-label">Email</label><input class="input" type="email" id="new-user-email" required></div>
228
+ <div class="form-group"><label class="form-label">Role</label>
229
+ <select class="input" id="new-user-role"><option>member</option><option>admin</option><option>owner</option><option>viewer</option></select>
230
+ </div>
231
+ <div class="form-group"><label class="form-label">Password</label><input class="input" type="password" id="new-user-password" required minlength="8"></div>
232
+ <div class="modal-actions">
233
+ <button class="btn" type="button" onclick="closeModal('modal-user')">Cancel</button>
234
+ <button class="btn btn-primary" type="submit" style="width:auto">Create</button>
235
+ </div>
236
+ </form>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Create API Key Modal -->
241
+ <div class="modal-overlay" id="modal-apikey">
242
+ <div class="modal">
243
+ <h3>Create API Key</h3>
244
+ <form onsubmit="createApiKey(event)">
245
+ <div class="form-group"><label class="form-label">Key Name</label><input class="input" id="apikey-name" required placeholder="e.g. CI/CD pipeline"></div>
246
+ <div class="modal-actions">
247
+ <button class="btn" type="button" onclick="closeModal('modal-apikey')">Cancel</button>
248
+ <button class="btn btn-primary" type="submit" style="width:auto">Create</button>
249
+ </div>
250
+ </form>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- Toast -->
255
+ <div id="toast"></div>
256
+
257
+ <script>
258
+ // ═══════════════════════════════════════════════════════
259
+ // AgenticMail Enterprise — Pure JavaScript Dashboard
260
+ // No frameworks, no build tools, no dependencies.
261
+ // ═══════════════════════════════════════════════════════
262
+
263
+ var token = localStorage.getItem('am_token');
264
+ var currentUser = null;
265
+ var currentPage = 'dashboard';
266
+
267
+ // ─── API ─────────────────────────────────────────────
268
+
269
+ function apiUrl() { return localStorage.getItem('am_api_url') || API_URL; }
270
+
271
+ function api(path, opts) {
272
+ opts = opts || {};
273
+ var headers = { 'Content-Type': 'application/json' };
274
+ if (token) headers['Authorization'] = 'Bearer ' + token;
275
+ return fetch(apiUrl() + '/api' + path, {
276
+ method: opts.method || 'GET',
277
+ headers: headers,
278
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
279
+ }).then(function(r) { return r.json().then(function(d) { if (!r.ok) throw new Error(d.error || 'Request failed'); return d; }); });
280
+ }
281
+
282
+ // ─── Setup ───────────────────────────────────────────
283
+
284
+ function saveApiUrl() {
285
+ var url = document.getElementById('setup-url').value.trim().replace(/\/+$/, '');
286
+ if (!url) return;
287
+ localStorage.setItem('am_api_url', url);
288
+ API_URL = url;
289
+ document.getElementById('setup-banner').style.display = 'none';
290
+ location.reload();
291
+ }
292
+
293
+ // ─── Auth ────────────────────────────────────────────
294
+
295
+ function doLogin(e) {
296
+ e.preventDefault();
297
+ var btn = document.getElementById('login-btn');
298
+ btn.textContent = 'Signing in...'; btn.disabled = true;
299
+ fetch(apiUrl() + '/auth/login', {
300
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
301
+ body: JSON.stringify({ email: document.getElementById('login-email').value, password: document.getElementById('login-password').value }),
302
+ }).then(function(r) { return r.json(); }).then(function(d) {
303
+ if (d.error) throw new Error(d.error);
304
+ token = d.token;
305
+ localStorage.setItem('am_token', d.token);
306
+ currentUser = d.user;
307
+ showApp();
308
+ }).catch(function(err) {
309
+ document.getElementById('login-error').style.display = 'block';
310
+ document.getElementById('login-error').textContent = err.message;
311
+ }).finally(function() { btn.textContent = 'Sign In'; btn.disabled = false; });
312
+ }
313
+
314
+ function doLogout() {
315
+ localStorage.removeItem('am_token'); token = null; currentUser = null;
316
+ document.getElementById('app').style.display = 'none';
317
+ document.getElementById('login-screen').style.display = 'flex';
318
+ }
319
+
320
+ function checkAuth() {
321
+ if (!token) return;
322
+ fetch(apiUrl() + '/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
323
+ .then(function(r) { if (!r.ok) throw new Error(); return r.json(); })
324
+ .then(function(u) { currentUser = u; showApp(); })
325
+ .catch(function() { localStorage.removeItem('am_token'); token = null; });
326
+ }
327
+
328
+ function showApp() {
329
+ document.getElementById('login-screen').style.display = 'none';
330
+ document.getElementById('app').style.display = 'block';
331
+ document.getElementById('user-name').textContent = currentUser.name;
332
+ document.getElementById('user-email').textContent = currentUser.email;
333
+ navigate('dashboard');
334
+ }
335
+
336
+ // ─── Navigation ──────────────────────────────────────
337
+
338
+ function navigate(page) {
339
+ currentPage = page;
340
+ document.querySelectorAll('.nav-item').forEach(function(el) {
341
+ el.classList.toggle('active', el.getAttribute('data-page') === page);
342
+ });
343
+ var pages = { dashboard: renderDashboard, agents: renderAgents, users: renderUsers, 'api-keys': renderApiKeys, audit: renderAudit, settings: renderSettings };
344
+ (pages[page] || renderDashboard)();
345
+ }
346
+
347
+ // ─── Toast ───────────────────────────────────────────
348
+
349
+ function toast(msg, type) {
350
+ var el = document.getElementById('toast');
351
+ el.textContent = msg; el.className = type || ''; el.style.display = 'block';
352
+ clearTimeout(el._t);
353
+ el._t = setTimeout(function() { el.style.display = 'none'; }, 3000);
354
+ }
355
+
356
+ // ─── Modals ──────────────────────────────────────────
357
+
358
+ function openModal(id) { document.getElementById(id).classList.add('open'); }
359
+ function closeModal(id) { document.getElementById(id).classList.remove('open'); }
360
+
361
+ // ─── Dashboard ───────────────────────────────────────
362
+
363
+ function renderDashboard() {
364
+ var el = document.getElementById('page-content');
365
+ el.innerHTML = '<h2 class="page-title">Dashboard</h2><p class="page-desc">Loading...</p>';
366
+ api('/stats').then(function(s) {
367
+ api('/audit?limit=8').then(function(a) {
368
+ var events = (a.events || []).map(function(e) {
369
+ return '<div style="padding:10px 0;border-bottom:1px solid var(--border);font-size:13px">' +
370
+ '<span style="color:var(--primary);font-weight:500">' + esc(e.action) + '</span> on ' + esc(e.resource) +
371
+ '<div style="font-size:11px;color:var(--text-muted)">' + new Date(e.timestamp).toLocaleString() + (e.ip ? ' · ' + e.ip : '') + '</div></div>';
372
+ }).join('');
373
+ el.innerHTML = '<h2 class="page-title">Dashboard</h2><p class="page-desc">Overview of your AgenticMail instance</p>' +
374
+ '<div class="stats-row">' +
375
+ stat('Total Agents', s.totalAgents, 'var(--primary)') +
376
+ stat('Active Agents', s.activeAgents, 'var(--success)') +
377
+ stat('Users', s.totalUsers, 'var(--text)') +
378
+ stat('Audit Events', s.totalAuditEvents, 'var(--text)') +
379
+ '</div>' +
380
+ '<div class="card"><div class="card-title">Recent Activity</div>' +
381
+ (events || '<div class="empty"><div class="empty-icon">📋</div>No activity yet</div>') +
382
+ '</div>';
383
+ });
384
+ });
385
+ }
386
+
387
+ function stat(label, value, color) {
388
+ return '<div class="stat-card"><div class="label">' + label + '</div><div class="value" style="color:' + color + '">' + value + '</div></div>';
389
+ }
390
+
391
+ // ─── Agents ──────────────────────────────────────────
392
+
393
+ function renderAgents() {
394
+ var el = document.getElementById('page-content');
395
+ el.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px"><div><h2 class="page-title">Agents</h2><p class="page-desc" style="margin:0">Manage AI agent identities</p></div><button class="btn btn-primary" style="width:auto" onclick="openModal(\'modal-agent\')">+ New Agent</button></div><div class="card"><div class="page-desc">Loading...</div></div>';
396
+ api('/agents').then(function(d) {
397
+ var agents = d.agents || [];
398
+ if (agents.length === 0) { el.querySelector('.card').innerHTML = '<div class="empty"><div class="empty-icon">🤖</div>No agents yet. Create your first one!</div>'; return; }
399
+ var rows = agents.map(function(a) {
400
+ return '<tr><td style="font-weight:600">' + esc(a.name) + '</td><td style="color:var(--text-dim)">' + esc(a.email) + '</td><td>' + esc(a.role) + '</td><td><span class="badge badge-' + a.status + '">' + a.status + '</span></td><td style="color:var(--text-muted);font-size:12px">' + new Date(a.createdAt).toLocaleDateString() + '</td><td>' + (a.status === 'active' ? '<button class="btn btn-sm btn-danger" onclick="archiveAgent(\'' + a.id + '\')">Archive</button>' : '') + '</td></tr>';
401
+ }).join('');
402
+ el.querySelector('.card').innerHTML = '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th>Created</th><th></th></tr></thead><tbody>' + rows + '</tbody></table></div>';
403
+ });
404
+ }
405
+
406
+ function createAgent(e) {
407
+ e.preventDefault();
408
+ api('/agents', { method: 'POST', body: { name: document.getElementById('agent-name').value, email: document.getElementById('agent-email').value || undefined, role: document.getElementById('agent-role').value } })
409
+ .then(function() { toast('Agent created!', 'success'); closeModal('modal-agent'); renderAgents(); })
410
+ .catch(function(err) { toast(err.message, 'error'); });
411
+ }
412
+
413
+ function archiveAgent(id) {
414
+ api('/agents/' + id + '/archive', { method: 'POST' })
415
+ .then(function() { toast('Agent archived', 'success'); renderAgents(); })
416
+ .catch(function(err) { toast(err.message, 'error'); });
417
+ }
418
+
419
+ // ─── Users ───────────────────────────────────────────
420
+
421
+ function renderUsers() {
422
+ var el = document.getElementById('page-content');
423
+ el.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px"><div><h2 class="page-title">Users</h2><p class="page-desc" style="margin:0">Manage team members</p></div><button class="btn btn-primary" style="width:auto" onclick="openModal(\'modal-user\')">+ New User</button></div><div class="card"><div class="page-desc">Loading...</div></div>';
424
+ api('/users').then(function(d) {
425
+ var users = d.users || [];
426
+ if (users.length === 0) { el.querySelector('.card').innerHTML = '<div class="empty"><div class="empty-icon">👥</div>No users yet</div>'; return; }
427
+ var rows = users.map(function(u) {
428
+ return '<tr><td style="font-weight:600">' + esc(u.name) + '</td><td style="color:var(--text-dim)">' + esc(u.email) + '</td><td><span class="badge badge-' + u.role + '">' + u.role + '</span></td><td style="color:var(--text-muted);font-size:12px">' + (u.lastLoginAt ? new Date(u.lastLoginAt).toLocaleString() : 'Never') + '</td></tr>';
429
+ }).join('');
430
+ el.querySelector('.card').innerHTML = '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Last Login</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
431
+ });
432
+ }
433
+
434
+ function createUser(e) {
435
+ e.preventDefault();
436
+ api('/users', { method: 'POST', body: { name: document.getElementById('new-user-name').value, email: document.getElementById('new-user-email').value, role: document.getElementById('new-user-role').value, password: document.getElementById('new-user-password').value } })
437
+ .then(function() { toast('User created!', 'success'); closeModal('modal-user'); renderUsers(); })
438
+ .catch(function(err) { toast(err.message, 'error'); });
439
+ }
440
+
441
+ // ─── API Keys ────────────────────────────────────────
442
+
443
+ function renderApiKeys() {
444
+ var el = document.getElementById('page-content');
445
+ el.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px"><div><h2 class="page-title">API Keys</h2><p class="page-desc" style="margin:0">Manage programmatic access</p></div><button class="btn btn-primary" style="width:auto" onclick="openModal(\'modal-apikey\')">+ New Key</button></div><div id="apikey-banner"></div><div class="card"><div class="page-desc">Loading...</div></div>';
446
+ api('/api-keys').then(function(d) {
447
+ var keys = d.keys || [];
448
+ if (keys.length === 0) { el.querySelector('.card').innerHTML = '<div class="empty"><div class="empty-icon">🔑</div>No API keys</div>'; return; }
449
+ var rows = keys.map(function(k) {
450
+ return '<tr><td style="font-weight:600">' + esc(k.name) + '</td><td><code style="font-size:12px">' + esc(k.keyPrefix) + '...</code></td><td style="font-size:12px">' + (k.scopes || []).join(', ') + '</td><td style="color:var(--text-muted);font-size:12px">' + (k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleString() : 'Never') + '</td><td><span class="badge ' + (k.revoked ? 'badge-archived' : 'badge-active') + '">' + (k.revoked ? 'revoked' : 'active') + '</span></td><td>' + (!k.revoked ? '<button class="btn btn-sm btn-danger" onclick="revokeApiKey(\'' + k.id + '\')">Revoke</button>' : '') + '</td></tr>';
451
+ }).join('');
452
+ el.querySelector('.card').innerHTML = '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Key</th><th>Scopes</th><th>Last Used</th><th>Status</th><th></th></tr></thead><tbody>' + rows + '</tbody></table></div>';
453
+ });
454
+ }
455
+
456
+ function createApiKey(e) {
457
+ e.preventDefault();
458
+ api('/api-keys', { method: 'POST', body: { name: document.getElementById('apikey-name').value } })
459
+ .then(function(d) {
460
+ closeModal('modal-apikey');
461
+ document.getElementById('apikey-banner').innerHTML = '<div class="card" style="border-color:var(--warning);background:var(--warning-bg)"><div style="font-weight:600;color:var(--warning);margin-bottom:8px">⚠️ Copy this key now — it won\'t be shown again</div><code style="display:block;background:var(--bg);padding:10px 14px;border-radius:var(--radius);font-size:13px;word-break:break-all;cursor:pointer" onclick="navigator.clipboard.writeText(\'' + d.plaintext + '\');toast(\'Copied!\',\'success\')">' + d.plaintext + '</code><button class="btn btn-sm" style="margin-top:8px" onclick="this.parentElement.remove()">Dismiss</button></div>';
462
+ renderApiKeys();
463
+ })
464
+ .catch(function(err) { toast(err.message, 'error'); });
465
+ }
466
+
467
+ function revokeApiKey(id) {
468
+ api('/api-keys/' + id, { method: 'DELETE' })
469
+ .then(function() { toast('Key revoked', 'success'); renderApiKeys(); })
470
+ .catch(function(err) { toast(err.message, 'error'); });
471
+ }
472
+
473
+ // ─── Audit ───────────────────────────────────────────
474
+
475
+ var auditPage = 0;
476
+ function renderAudit() {
477
+ var el = document.getElementById('page-content');
478
+ el.innerHTML = '<h2 class="page-title">Audit Log</h2><p class="page-desc">Loading...</p>';
479
+ api('/audit?limit=25&offset=' + (auditPage * 25)).then(function(d) {
480
+ var events = d.events || [];
481
+ var total = d.total || 0;
482
+ var pages = Math.ceil(total / 25) || 1;
483
+ if (events.length === 0) { el.innerHTML = '<h2 class="page-title">Audit Log</h2><p class="page-desc">0 events</p><div class="card"><div class="empty"><div class="empty-icon">📋</div>No audit events yet</div></div>'; return; }
484
+ var rows = events.map(function(e) {
485
+ return '<tr><td style="font-size:12px;color:var(--text-muted);white-space:nowrap">' + new Date(e.timestamp).toLocaleString() + '</td><td>' + esc(e.actor) + '</td><td style="color:var(--primary);font-weight:500">' + esc(e.action) + '</td><td style="font-size:12px">' + esc(e.resource) + '</td><td style="font-size:12px;color:var(--text-muted)">' + (e.ip || '-') + '</td></tr>';
486
+ }).join('');
487
+ el.innerHTML = '<h2 class="page-title">Audit Log</h2><p class="page-desc">' + total + ' total events</p><div class="card"><div class="table-wrap"><table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Resource</th><th>IP</th></tr></thead><tbody>' + rows + '</tbody></table></div><div style="display:flex;gap:8px;justify-content:center;margin-top:16px"><button class="btn btn-sm" ' + (auditPage === 0 ? 'disabled' : '') + ' onclick="auditPage--;renderAudit()">← Prev</button><span style="padding:6px 12px;font-size:12px;color:var(--text-muted)">Page ' + (auditPage + 1) + ' of ' + pages + '</span><button class="btn btn-sm" ' + ((auditPage + 1) * 25 >= total ? 'disabled' : '') + ' onclick="auditPage++;renderAudit()">Next →</button></div></div>';
488
+ });
489
+ }
490
+
491
+ // ─── Settings ────────────────────────────────────────
492
+
493
+ function renderSettings() {
494
+ var el = document.getElementById('page-content');
495
+ el.innerHTML = '<h2 class="page-title">Settings</h2><p class="page-desc">Loading...</p>';
496
+ Promise.all([api('/settings').catch(function() { return {}; }), api('/retention').catch(function() { return { enabled: false, retainDays: 365 }; })])
497
+ .then(function(results) {
498
+ var s = results[0], r = results[1];
499
+ el.innerHTML = '<h2 class="page-title">Settings</h2><p class="page-desc">Configure your organization</p>' +
500
+ '<div class="card"><div class="card-title">General</div>' +
501
+ '<form onsubmit="saveSettings(event)" style="display:grid;grid-template-columns:1fr 1fr;gap:14px">' +
502
+ '<div class="form-group"><label class="form-label">Organization Name</label><input class="input" id="set-name" value="' + esc(s.name || '') + '"></div>' +
503
+ '<div class="form-group"><label class="form-label">Domain</label><input class="input" id="set-domain" value="' + esc(s.domain || '') + '" placeholder="agents.acme.com"></div>' +
504
+ '<div class="form-group"><label class="form-label">Primary Color</label><input class="input" type="color" id="set-color" value="' + (s.primaryColor || '#6366f1') + '" style="height:38px;padding:4px"></div>' +
505
+ '<div class="form-group"><label class="form-label">Logo URL</label><input class="input" id="set-logo" value="' + esc(s.logoUrl || '') + '" placeholder="https://..."></div>' +
506
+ '<div style="grid-column:span 2"><button class="btn btn-primary" type="submit" style="width:auto">Save Settings</button></div></form></div>' +
507
+ '<div class="card"><div class="card-title">Plan</div>' +
508
+ '<span class="badge badge-active" style="font-size:14px;padding:4px 12px">' + (s.plan || 'free').toUpperCase() + '</span> ' +
509
+ '<span style="font-size:13px;color:var(--text-dim)">Subdomain: ' + esc(s.subdomain || 'not set') + '.agenticmail.cloud</span></div>' +
510
+ '<div class="card"><div class="card-title">Data Retention</div><div style="font-size:13px">' +
511
+ 'Status: <span style="color:' + (r.enabled ? 'var(--success)' : 'var(--text-muted)') + '">' + (r.enabled ? 'Enabled' : 'Disabled') + '</span><br>' +
512
+ '<span style="color:var(--text-dim)">Retain emails for ' + r.retainDays + ' days' + (r.archiveFirst ? ' (archive before delete)' : '') + '</span></div></div>';
513
+ });
514
+ }
515
+
516
+ function saveSettings(e) {
517
+ e.preventDefault();
518
+ api('/settings', { method: 'PATCH', body: { name: document.getElementById('set-name').value, domain: document.getElementById('set-domain').value, primaryColor: document.getElementById('set-color').value, logoUrl: document.getElementById('set-logo').value } })
519
+ .then(function() { toast('Settings saved!', 'success'); })
520
+ .catch(function(err) { toast(err.message, 'error'); });
521
+ }
522
+
523
+ // ─── Util ────────────────────────────────────────────
524
+
525
+ function esc(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
526
+
527
+ // ─── Init ────────────────────────────────────────────
528
+
529
+ if (API_URL === 'http://localhost:3000' && !localStorage.getItem('am_api_url')) {
530
+ document.getElementById('setup-banner').style.display = 'block';
531
+ }
532
+ checkAuth();
533
+ </script>
534
+ </body>
535
+ </html>