@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,558 @@
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</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ :root {
10
+ --bg: #0a0a0f; --surface: #12121a; --border: #1e1e2e; --border-hover: #2e2e4e;
11
+ --text: #e4e4ef; --text-dim: #8888a0; --text-muted: #55556a;
12
+ --primary: #6366f1; --primary-hover: #818cf8; --primary-dim: rgba(99,102,241,0.15);
13
+ --success: #22c55e; --warning: #f59e0b; --danger: #ef4444;
14
+ --radius: 8px; --radius-lg: 12px;
15
+ }
16
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
17
+ #root { display: flex; min-height: 100vh; }
18
+
19
+ /* Sidebar */
20
+ .sidebar { width: 240px; background: var(--surface); border-right: 1px solid var(--border); padding: 20px 0; display: flex; flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 10; }
21
+ .sidebar-logo { padding: 0 20px 20px; border-bottom: 1px solid var(--border); margin-bottom: 8px; }
22
+ .sidebar-logo h1 { font-size: 16px; font-weight: 700; letter-spacing: -0.02em; }
23
+ .sidebar-logo span { color: var(--primary); }
24
+ .sidebar-logo p { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
25
+ .nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 20px; font-size: 13px; color: var(--text-dim); cursor: pointer; transition: all 0.15s; border: none; background: none; width: 100%; text-align: left; }
26
+ .nav-item:hover { color: var(--text); background: rgba(255,255,255,0.03); }
27
+ .nav-item.active { color: var(--primary); background: var(--primary-dim); border-right: 2px solid var(--primary); }
28
+ .nav-section { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); padding: 16px 20px 6px; }
29
+
30
+ /* Main content */
31
+ .main { flex: 1; margin-left: 240px; padding: 32px; max-width: 1200px; }
32
+ .page-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; letter-spacing: -0.02em; }
33
+ .page-desc { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; }
34
+
35
+ /* Cards */
36
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 16px; }
37
+ .card-title { font-size: 13px; color: var(--text-dim); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
38
+
39
+ /* Stats grid */
40
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
41
+ .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; }
42
+ .stat-card .label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
43
+ .stat-card .value { font-size: 28px; font-weight: 700; margin-top: 4px; letter-spacing: -0.02em; }
44
+ .stat-card .value.primary { color: var(--primary); }
45
+ .stat-card .value.success { color: var(--success); }
46
+
47
+ /* Table */
48
+ .table-wrap { overflow-x: auto; }
49
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
50
+ th { text-align: left; padding: 10px 12px; color: var(--text-muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
51
+ td { padding: 12px; border-bottom: 1px solid var(--border); }
52
+ tr:hover td { background: rgba(255,255,255,0.02); }
53
+
54
+ /* Badge */
55
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; }
56
+ .badge-active { background: rgba(34,197,94,0.15); color: var(--success); }
57
+ .badge-archived { background: rgba(136,136,160,0.15); color: var(--text-dim); }
58
+ .badge-suspended { background: rgba(239,68,68,0.15); color: var(--danger); }
59
+ .badge-owner { background: rgba(245,158,11,0.15); color: var(--warning); }
60
+ .badge-admin { background: rgba(99,102,241,0.15); color: var(--primary); }
61
+ .badge-member { background: rgba(136,136,160,0.1); color: var(--text-dim); }
62
+
63
+ /* Buttons */
64
+ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--text); transition: all 0.15s; }
65
+ .btn:hover { border-color: var(--border-hover); background: rgba(255,255,255,0.05); }
66
+ .btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; }
67
+ .btn-primary:hover { background: var(--primary-hover); }
68
+ .btn-danger { color: var(--danger); }
69
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
70
+
71
+ /* Input */
72
+ .input { padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 13px; width: 100%; outline: none; transition: border-color 0.15s; }
73
+ .input:focus { border-color: var(--primary); }
74
+ .form-group { margin-bottom: 14px; }
75
+ .form-label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
76
+ select.input { appearance: none; }
77
+
78
+ /* Modal */
79
+ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
80
+ .modal { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px; width: 440px; max-width: 90vw; }
81
+ .modal-title { font-size: 16px; font-weight: 700; margin-bottom: 16px; }
82
+ .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
83
+
84
+ /* Audit log */
85
+ .audit-item { padding: 12px; border-bottom: 1px solid var(--border); font-size: 13px; }
86
+ .audit-item:last-child { border-bottom: none; }
87
+ .audit-time { font-size: 11px; color: var(--text-muted); }
88
+ .audit-action { color: var(--primary); font-weight: 500; }
89
+
90
+ /* Toast */
91
+ .toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 20px; font-size: 13px; z-index: 200; animation: slideUp 0.2s ease; }
92
+ .toast.success { border-color: var(--success); }
93
+ .toast.error { border-color: var(--danger); }
94
+ @keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
95
+
96
+ /* Empty state */
97
+ .empty { text-align: center; padding: 48px 20px; color: var(--text-muted); }
98
+ .empty-icon { font-size: 32px; margin-bottom: 8px; }
99
+
100
+ /* Responsive */
101
+ @media (max-width: 768px) {
102
+ .sidebar { width: 60px; }
103
+ .sidebar-logo h1, .sidebar-logo p, .nav-item span, .nav-section { display: none; }
104
+ .nav-item { justify-content: center; padding: 12px; }
105
+ .main { margin-left: 60px; padding: 16px; }
106
+ }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div id="root"></div>
111
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
112
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
113
+ <script>
114
+ const { useState, useEffect, useCallback, createElement: h, Fragment } = React;
115
+
116
+ // ─── API Client ─────────────────────────────────────
117
+ const API_BASE = '/api';
118
+ let authToken = localStorage.getItem('am_token');
119
+
120
+ async function api(path, opts = {}) {
121
+ const headers = { 'Content-Type': 'application/json' };
122
+ if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
123
+ const resp = await fetch(`${API_BASE}${path}`, { ...opts, headers: { ...headers, ...opts.headers } });
124
+ const data = await resp.json();
125
+ if (!resp.ok) throw new Error(data.error || 'Request failed');
126
+ return data;
127
+ }
128
+
129
+ // ─── Toast ──────────────────────────────────────────
130
+ function Toast({ message, type, onDone }) {
131
+ useEffect(() => { const t = setTimeout(onDone, 3000); return () => clearTimeout(t); }, []);
132
+ return h('div', { className: `toast ${type}` }, message);
133
+ }
134
+
135
+ // ─── Login ──────────────────────────────────────────
136
+ function LoginPage({ onLogin }) {
137
+ const [email, setEmail] = useState('');
138
+ const [password, setPassword] = useState('');
139
+ const [error, setError] = useState('');
140
+ const [loading, setLoading] = useState(false);
141
+
142
+ async function handleSubmit(e) {
143
+ e.preventDefault();
144
+ setLoading(true); setError('');
145
+ try {
146
+ const data = await fetch('/auth/login', {
147
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({ email, password }),
149
+ }).then(r => r.json());
150
+ if (data.error) throw new Error(data.error);
151
+ authToken = data.token;
152
+ localStorage.setItem('am_token', data.token);
153
+ onLogin(data.user);
154
+ } catch (err) { setError(err.message); }
155
+ setLoading(false);
156
+ }
157
+
158
+ return h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: 20 } },
159
+ h('form', { onSubmit: handleSubmit, style: { width: 360 } },
160
+ h('div', { style: { textAlign: 'center', marginBottom: 32 } },
161
+ h('h1', { style: { fontSize: 20, fontWeight: 700 } }, '🏢 ', h('span', { style: { color: 'var(--primary)' } }, 'AgenticMail'), ' Enterprise'),
162
+ h('p', { style: { fontSize: 13, color: 'var(--text-dim)', marginTop: 4 } }, 'Sign in to your dashboard'),
163
+ ),
164
+ error && h('div', { style: { background: 'rgba(239,68,68,0.1)', border: '1px solid var(--danger)', borderRadius: 'var(--radius)', padding: '8px 12px', marginBottom: 16, fontSize: 13, color: 'var(--danger)' } }, error),
165
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Email'), h('input', { className: 'input', type: 'email', value: email, onChange: e => setEmail(e.target.value), required: true, autoFocus: true })),
166
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Password'), h('input', { className: 'input', type: 'password', value: password, onChange: e => setPassword(e.target.value), required: true })),
167
+ h('button', { className: 'btn btn-primary', type: 'submit', disabled: loading, style: { width: '100%', justifyContent: 'center', marginTop: 8 } }, loading ? 'Signing in...' : 'Sign In'),
168
+ )
169
+ );
170
+ }
171
+
172
+ // ─── Modal ──────────────────────────────────────────
173
+ function Modal({ title, children, onClose, actions }) {
174
+ return h('div', { className: 'modal-overlay', onClick: e => e.target === e.currentTarget && onClose() },
175
+ h('div', { className: 'modal' },
176
+ h('div', { className: 'modal-title' }, title),
177
+ children,
178
+ actions && h('div', { className: 'modal-actions' }, actions),
179
+ )
180
+ );
181
+ }
182
+
183
+ // ─── Dashboard Page ─────────────────────────────────
184
+ function DashboardPage() {
185
+ const [stats, setStats] = useState(null);
186
+ const [audit, setAudit] = useState([]);
187
+ useEffect(() => {
188
+ api('/stats').then(setStats).catch(() => {});
189
+ api('/audit?limit=10').then(d => setAudit(d.events || [])).catch(() => {});
190
+ }, []);
191
+ if (!stats) return h('div', { className: 'page-desc' }, 'Loading...');
192
+ return h(Fragment, null,
193
+ h('h2', { className: 'page-title' }, 'Dashboard'),
194
+ h('p', { className: 'page-desc' }, 'Overview of your AgenticMail instance'),
195
+ h('div', { className: 'stats-grid' },
196
+ h('div', { className: 'stat-card' }, h('div', { className: 'label' }, 'Total Agents'), h('div', { className: 'value primary' }, stats.totalAgents)),
197
+ h('div', { className: 'stat-card' }, h('div', { className: 'label' }, 'Active Agents'), h('div', { className: 'value success' }, stats.activeAgents)),
198
+ h('div', { className: 'stat-card' }, h('div', { className: 'label' }, 'Users'), h('div', { className: 'value' }, stats.totalUsers)),
199
+ h('div', { className: 'stat-card' }, h('div', { className: 'label' }, 'Audit Events'), h('div', { className: 'value' }, stats.totalAuditEvents)),
200
+ ),
201
+ h('div', { className: 'card' },
202
+ h('div', { className: 'card-title' }, 'Recent Activity'),
203
+ audit.length === 0
204
+ ? h('div', { className: 'empty' }, h('div', { className: 'empty-icon' }, '📋'), 'No activity yet')
205
+ : audit.map(e => h('div', { className: 'audit-item', key: e.id },
206
+ h('span', { className: 'audit-action' }, e.action), ' on ', h('span', null, e.resource),
207
+ h('div', { className: 'audit-time' }, new Date(e.timestamp).toLocaleString(), e.ip ? ` · ${e.ip}` : ''),
208
+ ))
209
+ ),
210
+ );
211
+ }
212
+
213
+ // ─── Agents Page ────────────────────────────────────
214
+ function AgentsPage({ showToast }) {
215
+ const [agents, setAgents] = useState([]);
216
+ const [showCreate, setShowCreate] = useState(false);
217
+ const [form, setForm] = useState({ name: '', email: '', role: 'assistant' });
218
+ const [loading, setLoading] = useState(false);
219
+
220
+ const load = useCallback(() => { api('/agents').then(d => setAgents(d.agents || [])).catch(() => {}); }, []);
221
+ useEffect(load, []);
222
+
223
+ async function handleCreate(e) {
224
+ e.preventDefault(); setLoading(true);
225
+ try {
226
+ await api('/agents', { method: 'POST', body: JSON.stringify(form) });
227
+ showToast('Agent created', 'success');
228
+ setShowCreate(false); setForm({ name: '', email: '', role: 'assistant' }); load();
229
+ } catch (err) { showToast(err.message, 'error'); }
230
+ setLoading(false);
231
+ }
232
+
233
+ async function archiveAgent(id) {
234
+ try { await api(`/agents/${id}/archive`, { method: 'POST' }); showToast('Agent archived', 'success'); load(); }
235
+ catch (err) { showToast(err.message, 'error'); }
236
+ }
237
+
238
+ return h(Fragment, null,
239
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 } },
240
+ h('div', null, h('h2', { className: 'page-title' }, 'Agents'), h('p', { className: 'page-desc', style: { marginBottom: 0 } }, 'Manage AI agent identities')),
241
+ h('button', { className: 'btn btn-primary', onClick: () => setShowCreate(true) }, '+ New Agent'),
242
+ ),
243
+ h('div', { className: 'card' },
244
+ h('div', { className: 'table-wrap' },
245
+ agents.length === 0
246
+ ? h('div', { className: 'empty' }, h('div', { className: 'empty-icon' }, '🤖'), 'No agents yet')
247
+ : h('table', null,
248
+ h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Email'), h('th', null, 'Role'), h('th', null, 'Status'), h('th', null, 'Created'), h('th', null, ''))),
249
+ h('tbody', null, agents.map(a => h('tr', { key: a.id },
250
+ h('td', { style: { fontWeight: 600 } }, a.name),
251
+ h('td', { style: { color: 'var(--text-dim)' } }, a.email),
252
+ h('td', null, a.role),
253
+ h('td', null, h('span', { className: `badge badge-${a.status}` }, a.status)),
254
+ h('td', { style: { color: 'var(--text-muted)', fontSize: 12 } }, new Date(a.createdAt).toLocaleDateString()),
255
+ h('td', null, a.status === 'active' && h('button', { className: 'btn btn-sm btn-danger', onClick: () => archiveAgent(a.id) }, 'Archive')),
256
+ )))
257
+ )
258
+ )
259
+ ),
260
+ showCreate && h(Modal, { title: 'Create Agent', onClose: () => setShowCreate(false),
261
+ actions: [
262
+ h('button', { className: 'btn', onClick: () => setShowCreate(false) }, 'Cancel'),
263
+ h('button', { className: 'btn btn-primary', onClick: handleCreate, disabled: loading }, loading ? 'Creating...' : 'Create'),
264
+ ] },
265
+ h('form', { onSubmit: handleCreate },
266
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Name'), h('input', { className: 'input', value: form.name, onChange: e => setForm({ ...form, name: e.target.value }), required: true, autoFocus: true, placeholder: 'e.g. researcher' })),
267
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Email (optional)'), h('input', { className: 'input', value: form.email, onChange: e => setForm({ ...form, email: e.target.value }), placeholder: 'auto-generated if blank' })),
268
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Role'), h('select', { className: 'input', value: form.role, onChange: e => setForm({ ...form, role: e.target.value }) },
269
+ ['assistant', 'secretary', 'researcher', 'writer', 'custom'].map(r => h('option', { key: r, value: r }, r))
270
+ )),
271
+ )
272
+ ),
273
+ );
274
+ }
275
+
276
+ // ─── Users Page ─────────────────────────────────────
277
+ function UsersPage({ showToast }) {
278
+ const [users, setUsers] = useState([]);
279
+ const [showCreate, setShowCreate] = useState(false);
280
+ const [form, setForm] = useState({ email: '', name: '', role: 'member', password: '' });
281
+ const [loading, setLoading] = useState(false);
282
+
283
+ const load = useCallback(() => { api('/users').then(d => setUsers(d.users || [])).catch(() => {}); }, []);
284
+ useEffect(load, []);
285
+
286
+ async function handleCreate(e) {
287
+ e.preventDefault(); setLoading(true);
288
+ try {
289
+ await api('/users', { method: 'POST', body: JSON.stringify(form) });
290
+ showToast('User created', 'success');
291
+ setShowCreate(false); setForm({ email: '', name: '', role: 'member', password: '' }); load();
292
+ } catch (err) { showToast(err.message, 'error'); }
293
+ setLoading(false);
294
+ }
295
+
296
+ return h(Fragment, null,
297
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 } },
298
+ h('div', null, h('h2', { className: 'page-title' }, 'Users'), h('p', { className: 'page-desc', style: { marginBottom: 0 } }, 'Manage team members')),
299
+ h('button', { className: 'btn btn-primary', onClick: () => setShowCreate(true) }, '+ New User'),
300
+ ),
301
+ h('div', { className: 'card' },
302
+ h('div', { className: 'table-wrap' },
303
+ users.length === 0
304
+ ? h('div', { className: 'empty' }, h('div', { className: 'empty-icon' }, '👥'), 'No users yet')
305
+ : h('table', null,
306
+ h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Email'), h('th', null, 'Role'), h('th', null, 'Last Login'))),
307
+ h('tbody', null, users.map(u => h('tr', { key: u.id },
308
+ h('td', { style: { fontWeight: 600 } }, u.name),
309
+ h('td', { style: { color: 'var(--text-dim)' } }, u.email),
310
+ h('td', null, h('span', { className: `badge badge-${u.role}` }, u.role)),
311
+ h('td', { style: { color: 'var(--text-muted)', fontSize: 12 } }, u.lastLoginAt ? new Date(u.lastLoginAt).toLocaleString() : 'Never'),
312
+ )))
313
+ )
314
+ )
315
+ ),
316
+ showCreate && h(Modal, { title: 'Create User', onClose: () => setShowCreate(false),
317
+ actions: [
318
+ h('button', { className: 'btn', onClick: () => setShowCreate(false) }, 'Cancel'),
319
+ h('button', { className: 'btn btn-primary', onClick: handleCreate, disabled: loading }, loading ? 'Creating...' : 'Create'),
320
+ ] },
321
+ h('form', { onSubmit: handleCreate },
322
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Name'), h('input', { className: 'input', value: form.name, onChange: e => setForm({ ...form, name: e.target.value }), required: true, autoFocus: true })),
323
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Email'), h('input', { className: 'input', type: 'email', value: form.email, onChange: e => setForm({ ...form, email: e.target.value }), required: true })),
324
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Role'), h('select', { className: 'input', value: form.role, onChange: e => setForm({ ...form, role: e.target.value }) },
325
+ ['owner', 'admin', 'member', 'viewer'].map(r => h('option', { key: r, value: r }, r))
326
+ )),
327
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Password'), h('input', { className: 'input', type: 'password', value: form.password, onChange: e => setForm({ ...form, password: e.target.value }), required: true, minLength: 8 })),
328
+ )
329
+ ),
330
+ );
331
+ }
332
+
333
+ // ─── API Keys Page ──────────────────────────────────
334
+ function ApiKeysPage({ showToast }) {
335
+ const [keys, setKeys] = useState([]);
336
+ const [showCreate, setShowCreate] = useState(false);
337
+ const [newKeyPlaintext, setNewKeyPlaintext] = useState('');
338
+ const [form, setForm] = useState({ name: '' });
339
+ const [loading, setLoading] = useState(false);
340
+
341
+ const load = useCallback(() => { api('/api-keys').then(d => setKeys(d.keys || [])).catch(() => {}); }, []);
342
+ useEffect(load, []);
343
+
344
+ async function handleCreate(e) {
345
+ e.preventDefault(); setLoading(true);
346
+ try {
347
+ const data = await api('/api-keys', { method: 'POST', body: JSON.stringify({ name: form.name }) });
348
+ setNewKeyPlaintext(data.plaintext);
349
+ showToast('API key created', 'success');
350
+ setForm({ name: '' }); load();
351
+ } catch (err) { showToast(err.message, 'error'); }
352
+ setLoading(false);
353
+ }
354
+
355
+ async function revokeKey(id) {
356
+ try { await api(`/api-keys/${id}`, { method: 'DELETE' }); showToast('Key revoked', 'success'); load(); }
357
+ catch (err) { showToast(err.message, 'error'); }
358
+ }
359
+
360
+ return h(Fragment, null,
361
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 } },
362
+ h('div', null, h('h2', { className: 'page-title' }, 'API Keys'), h('p', { className: 'page-desc', style: { marginBottom: 0 } }, 'Manage programmatic access')),
363
+ h('button', { className: 'btn btn-primary', onClick: () => setShowCreate(true) }, '+ New Key'),
364
+ ),
365
+ newKeyPlaintext && h('div', { className: 'card', style: { borderColor: 'var(--warning)', background: 'rgba(245,158,11,0.05)' } },
366
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--warning)', marginBottom: 8 } }, '⚠️ Copy this key now — it won\'t be shown again'),
367
+ h('code', { style: { display: 'block', background: 'var(--bg)', padding: '10px 14px', borderRadius: 'var(--radius)', fontSize: 13, wordBreak: 'break-all', cursor: 'pointer' }, onClick: () => { navigator.clipboard.writeText(newKeyPlaintext); showToast('Copied!', 'success'); } }, newKeyPlaintext),
368
+ h('button', { className: 'btn btn-sm', style: { marginTop: 8 }, onClick: () => setNewKeyPlaintext('') }, 'Dismiss'),
369
+ ),
370
+ h('div', { className: 'card' },
371
+ h('div', { className: 'table-wrap' },
372
+ keys.length === 0
373
+ ? h('div', { className: 'empty' }, h('div', { className: 'empty-icon' }, '🔑'), 'No API keys')
374
+ : h('table', null,
375
+ h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Key Prefix'), h('th', null, 'Scopes'), h('th', null, 'Last Used'), h('th', null, 'Status'), h('th', null, ''))),
376
+ h('tbody', null, keys.map(k => h('tr', { key: k.id },
377
+ h('td', { style: { fontWeight: 600 } }, k.name),
378
+ h('td', null, h('code', { style: { fontSize: 12 } }, k.keyPrefix + '...')),
379
+ h('td', { style: { fontSize: 12 } }, (k.scopes || []).join(', ') || '*'),
380
+ h('td', { style: { color: 'var(--text-muted)', fontSize: 12 } }, k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleString() : 'Never'),
381
+ h('td', null, h('span', { className: `badge ${k.revoked ? 'badge-archived' : 'badge-active'}` }, k.revoked ? 'revoked' : 'active')),
382
+ h('td', null, !k.revoked && h('button', { className: 'btn btn-sm btn-danger', onClick: () => revokeKey(k.id) }, 'Revoke')),
383
+ )))
384
+ )
385
+ )
386
+ ),
387
+ showCreate && h(Modal, { title: 'Create API Key', onClose: () => setShowCreate(false),
388
+ actions: [
389
+ h('button', { className: 'btn', onClick: () => setShowCreate(false) }, 'Cancel'),
390
+ h('button', { className: 'btn btn-primary', onClick: handleCreate, disabled: loading }, loading ? 'Creating...' : 'Create'),
391
+ ] },
392
+ h('form', { onSubmit: handleCreate },
393
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Key Name'), h('input', { className: 'input', value: form.name, onChange: e => setForm({ ...form, name: e.target.value }), required: true, autoFocus: true, placeholder: 'e.g. CI/CD pipeline' })),
394
+ )
395
+ ),
396
+ );
397
+ }
398
+
399
+ // ─── Audit Page ─────────────────────────────────────
400
+ function AuditPage() {
401
+ const [events, setEvents] = useState([]);
402
+ const [total, setTotal] = useState(0);
403
+ const [page, setPage] = useState(0);
404
+ const pageSize = 25;
405
+
406
+ useEffect(() => {
407
+ api(`/audit?limit=${pageSize}&offset=${page * pageSize}`).then(d => { setEvents(d.events || []); setTotal(d.total || 0); }).catch(() => {});
408
+ }, [page]);
409
+
410
+ return h(Fragment, null,
411
+ h('h2', { className: 'page-title' }, 'Audit Log'),
412
+ h('p', { className: 'page-desc' }, `${total} total events`),
413
+ h('div', { className: 'card' },
414
+ events.length === 0
415
+ ? h('div', { className: 'empty' }, h('div', { className: 'empty-icon' }, '📋'), 'No audit events')
416
+ : h(Fragment, null,
417
+ h('div', { className: 'table-wrap' },
418
+ h('table', null,
419
+ h('thead', null, h('tr', null, h('th', null, 'Time'), h('th', null, 'Actor'), h('th', null, 'Action'), h('th', null, 'Resource'), h('th', null, 'IP'))),
420
+ h('tbody', null, events.map(e => h('tr', { key: e.id },
421
+ h('td', { style: { fontSize: 12, color: 'var(--text-muted)', whiteSpace: 'nowrap' } }, new Date(e.timestamp).toLocaleString()),
422
+ h('td', null, e.actor),
423
+ h('td', null, h('span', { className: 'audit-action' }, e.action)),
424
+ h('td', { style: { fontSize: 12 } }, e.resource),
425
+ h('td', { style: { fontSize: 12, color: 'var(--text-muted)' } }, e.ip || '-'),
426
+ )))
427
+ )
428
+ ),
429
+ h('div', { style: { display: 'flex', gap: 8, justifyContent: 'center', marginTop: 16 } },
430
+ h('button', { className: 'btn btn-sm', disabled: page === 0, onClick: () => setPage(p => p - 1) }, '← Prev'),
431
+ h('span', { style: { padding: '4px 12px', fontSize: 12, color: 'var(--text-muted)' } }, `Page ${page + 1} of ${Math.ceil(total / pageSize) || 1}`),
432
+ h('button', { className: 'btn btn-sm', disabled: (page + 1) * pageSize >= total, onClick: () => setPage(p => p + 1) }, 'Next →'),
433
+ )
434
+ )
435
+ ),
436
+ );
437
+ }
438
+
439
+ // ─── Settings Page ──────────────────────────────────
440
+ function SettingsPage({ showToast }) {
441
+ const [settings, setSettings] = useState(null);
442
+ const [form, setForm] = useState({});
443
+ const [retention, setRetention] = useState(null);
444
+ const [loading, setLoading] = useState(false);
445
+
446
+ useEffect(() => {
447
+ api('/settings').then(d => { setSettings(d); setForm({ name: d.name || '', domain: d.domain || '', primaryColor: d.primaryColor || '#6366f1', logoUrl: d.logoUrl || '' }); }).catch(() => {});
448
+ api('/retention').then(setRetention).catch(() => {});
449
+ }, []);
450
+
451
+ async function saveSettings(e) {
452
+ e.preventDefault(); setLoading(true);
453
+ try { await api('/settings', { method: 'PATCH', body: JSON.stringify(form) }); showToast('Settings saved', 'success'); }
454
+ catch (err) { showToast(err.message, 'error'); }
455
+ setLoading(false);
456
+ }
457
+
458
+ if (!settings) return h('div', { className: 'page-desc' }, 'Loading...');
459
+
460
+ return h(Fragment, null,
461
+ h('h2', { className: 'page-title' }, 'Settings'),
462
+ h('p', { className: 'page-desc' }, 'Configure your organization'),
463
+ h('div', { className: 'card' },
464
+ h('div', { className: 'card-title' }, 'General'),
465
+ h('form', { onSubmit: saveSettings },
466
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 } },
467
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Organization Name'), h('input', { className: 'input', value: form.name, onChange: e => setForm({ ...form, name: e.target.value }) })),
468
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Domain'), h('input', { className: 'input', value: form.domain, onChange: e => setForm({ ...form, domain: e.target.value }), placeholder: 'agents.acme.com' })),
469
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Primary Color'), h('input', { className: 'input', type: 'color', value: form.primaryColor, onChange: e => setForm({ ...form, primaryColor: e.target.value }), style: { height: 36, padding: 4 } })),
470
+ h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Logo URL'), h('input', { className: 'input', value: form.logoUrl, onChange: e => setForm({ ...form, logoUrl: e.target.value }), placeholder: 'https://...' })),
471
+ ),
472
+ h('button', { className: 'btn btn-primary', type: 'submit', disabled: loading, style: { marginTop: 8 } }, loading ? 'Saving...' : 'Save'),
473
+ ),
474
+ ),
475
+ h('div', { className: 'card' },
476
+ h('div', { className: 'card-title' }, 'Plan'),
477
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 12 } },
478
+ h('span', { className: 'badge badge-active', style: { fontSize: 14, padding: '4px 12px' } }, (settings.plan || 'free').toUpperCase()),
479
+ h('span', { style: { fontSize: 13, color: 'var(--text-dim)' } }, `Subdomain: ${settings.subdomain || 'not set'}.agenticmail.cloud`),
480
+ ),
481
+ ),
482
+ retention && h('div', { className: 'card' },
483
+ h('div', { className: 'card-title' }, 'Data Retention'),
484
+ h('div', { style: { fontSize: 13 } },
485
+ h('div', null, 'Status: ', h('span', { style: { color: retention.enabled ? 'var(--success)' : 'var(--text-muted)' } }, retention.enabled ? 'Enabled' : 'Disabled')),
486
+ h('div', { style: { color: 'var(--text-dim)', marginTop: 4 } }, `Retain emails for ${retention.retainDays} days`, retention.archiveFirst ? ' (archive before delete)' : ''),
487
+ ),
488
+ ),
489
+ );
490
+ }
491
+
492
+ // ─── App ────────────────────────────────────────────
493
+ const PAGES = {
494
+ dashboard: { icon: '📊', label: 'Dashboard', component: DashboardPage },
495
+ agents: { icon: '🤖', label: 'Agents', component: AgentsPage },
496
+ users: { icon: '👥', label: 'Users', component: UsersPage },
497
+ 'api-keys': { icon: '🔑', label: 'API Keys', component: ApiKeysPage },
498
+ audit: { icon: '📋', label: 'Audit Log', component: AuditPage },
499
+ settings: { icon: '⚙️', label: 'Settings', component: SettingsPage },
500
+ };
501
+
502
+ function App() {
503
+ const [user, setUser] = useState(null);
504
+ const [page, setPage] = useState('dashboard');
505
+ const [toast, setToast] = useState(null);
506
+ const [checkingAuth, setCheckingAuth] = useState(true);
507
+
508
+ const showToast = useCallback((message, type = 'success') => setToast({ message, type, key: Date.now() }), []);
509
+
510
+ useEffect(() => {
511
+ if (authToken) {
512
+ fetch('/auth/me', { headers: { Authorization: `Bearer ${authToken}` } })
513
+ .then(r => r.ok ? r.json() : Promise.reject())
514
+ .then(u => { setUser(u); setCheckingAuth(false); })
515
+ .catch(() => { localStorage.removeItem('am_token'); authToken = null; setCheckingAuth(false); });
516
+ } else { setCheckingAuth(false); }
517
+ }, []);
518
+
519
+ if (checkingAuth) return h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', color: 'var(--text-dim)' } }, 'Loading...');
520
+ if (!user) return h(LoginPage, { onLogin: setUser });
521
+
522
+ const PageComponent = PAGES[page]?.component || DashboardPage;
523
+
524
+ return h(Fragment, null,
525
+ h('div', { className: 'sidebar' },
526
+ h('div', { className: 'sidebar-logo' },
527
+ h('h1', null, '🏢 ', h('span', null, 'Agentic'), 'Mail'),
528
+ h('p', null, 'Enterprise'),
529
+ ),
530
+ h('div', { className: 'nav-section' }, 'Overview'),
531
+ Object.entries(PAGES).slice(0, 1).map(([key, { icon, label }]) =>
532
+ h('button', { key, className: `nav-item ${page === key ? 'active' : ''}`, onClick: () => setPage(key) }, icon, ' ', h('span', null, label))
533
+ ),
534
+ h('div', { className: 'nav-section' }, 'Manage'),
535
+ Object.entries(PAGES).slice(1, 4).map(([key, { icon, label }]) =>
536
+ h('button', { key, className: `nav-item ${page === key ? 'active' : ''}`, onClick: () => setPage(key) }, icon, ' ', h('span', null, label))
537
+ ),
538
+ h('div', { className: 'nav-section' }, 'System'),
539
+ Object.entries(PAGES).slice(4).map(([key, { icon, label }]) =>
540
+ h('button', { key, className: `nav-item ${page === key ? 'active' : ''}`, onClick: () => setPage(key) }, icon, ' ', h('span', null, label))
541
+ ),
542
+ h('div', { style: { marginTop: 'auto', padding: '16px 20px', borderTop: '1px solid var(--border)', fontSize: 12 } },
543
+ h('div', { style: { color: 'var(--text-dim)' } }, user.name),
544
+ h('div', { style: { color: 'var(--text-muted)', fontSize: 11 } }, user.email),
545
+ h('button', { style: { marginTop: 8, background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', fontSize: 11, padding: 0 },
546
+ onClick: () => { localStorage.removeItem('am_token'); authToken = null; setUser(null); }
547
+ }, 'Sign out'),
548
+ ),
549
+ ),
550
+ h('div', { className: 'main' }, h(PageComponent, { showToast })),
551
+ toast && h(Toast, { ...toast, onDone: () => setToast(null) }),
552
+ );
553
+ }
554
+
555
+ ReactDOM.createRoot(document.getElementById('root')).render(h(App));
556
+ </script>
557
+ </body>
558
+ </html>