@agenticmail/enterprise 0.2.2 → 0.3.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.
@@ -6,553 +6,876 @@
6
6
  <title>AgenticMail Enterprise</title>
7
7
  <style>
8
8
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ /* ── Design Tokens ─────────────────────────────── */
9
11
  :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;
12
+ --font: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
13
+ --mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
14
+ --radius-sm: 4px; --radius: 6px; --radius-lg: 10px; --radius-xl: 14px;
15
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
16
+ --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
17
+ --shadow-lg: 0 4px 12px rgba(0,0,0,0.1), 0 1px 3px rgba(0,0,0,0.06);
18
+ --transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
19
+ color-scheme: light dark;
20
+ }
21
+
22
+ /* ── Light Mode (default) ──────────────────────── */
23
+ :root, [data-theme="light"] {
24
+ --bg-0: #f8f9fa; --bg-1: #ffffff; --bg-2: #f1f3f5; --bg-3: #e9ecef;
25
+ --border: #dee2e6; --border-subtle: #e9ecef; --border-strong: #ced4da;
26
+ --text-0: #212529; --text-1: #495057; --text-2: #868e96; --text-3: #adb5bd;
27
+ --accent: #e84393; --accent-hover: #d63384; --accent-subtle: rgba(232,67,147,0.08); --accent-text: #ffffff;
28
+ --success: #2b8a3e; --success-subtle: rgba(43,138,62,0.08);
29
+ --warning: #e67700; --warning-subtle: rgba(230,119,0,0.08);
30
+ --danger: #c92a2a; --danger-subtle: rgba(201,42,42,0.08);
31
+ --info: #1971c2; --info-subtle: rgba(25,113,194,0.08);
32
+ --overlay: rgba(0,0,0,0.4);
33
+ }
34
+
35
+ /* ── Dark Mode ─────────────────────────────────── */
36
+ [data-theme="dark"] {
37
+ --bg-0: #0f1114; --bg-1: #16181d; --bg-2: #1c1f26; --bg-3: #252930;
38
+ --border: #2c3038; --border-subtle: #23272e; --border-strong: #3b414c;
39
+ --text-0: #e1e4e8; --text-1: #b0b8c4; --text-2: #6b7280; --text-3: #454d5a;
40
+ --accent: #f06595; --accent-hover: #f783ac; --accent-subtle: rgba(240,101,149,0.1); --accent-text: #0f1114;
41
+ --success: #37b24d; --success-subtle: rgba(55,178,77,0.1);
42
+ --warning: #f08c00; --warning-subtle: rgba(240,140,0,0.1);
43
+ --danger: #f03e3e; --danger-subtle: rgba(240,62,62,0.1);
44
+ --info: #4dabf7; --info-subtle: rgba(77,171,247,0.1);
45
+ --overlay: rgba(0,0,0,0.65);
46
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
47
+ --shadow: 0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2);
48
+ --shadow-lg: 0 4px 12px rgba(0,0,0,0.4), 0 1px 3px rgba(0,0,0,0.3);
49
+ }
50
+
51
+ @media (prefers-color-scheme: dark) {
52
+ :root:not([data-theme="light"]) {
53
+ --bg-0: #0f1114; --bg-1: #16181d; --bg-2: #1c1f26; --bg-3: #252930;
54
+ --border: #2c3038; --border-subtle: #23272e; --border-strong: #3b414c;
55
+ --text-0: #e1e4e8; --text-1: #b0b8c4; --text-2: #6b7280; --text-3: #454d5a;
56
+ --accent: #f06595; --accent-hover: #f783ac; --accent-subtle: rgba(240,101,149,0.1); --accent-text: #0f1114;
57
+ --success: #37b24d; --success-subtle: rgba(55,178,77,0.1);
58
+ --warning: #f08c00; --warning-subtle: rgba(240,140,0,0.1);
59
+ --danger: #f03e3e; --danger-subtle: rgba(240,62,62,0.1);
60
+ --info: #4dabf7; --info-subtle: rgba(77,171,247,0.1);
61
+ --overlay: rgba(0,0,0,0.65);
62
+ }
15
63
  }
16
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
64
+
65
+ /* ── Base ───────────────────────────────────────── */
66
+ body { font-family: var(--font); background: var(--bg-0); color: var(--text-0); line-height: 1.5; font-size: 14px; -webkit-font-smoothing: antialiased; }
17
67
  #root { display: flex; min-height: 100vh; }
18
68
 
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; }
69
+ /* ── Sidebar ───────────────────────────────────── */
70
+ .sidebar { width: 220px; background: var(--bg-1); border-right: 1px solid var(--border); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 10; }
71
+ .sidebar-header { padding: 16px 16px 12px; border-bottom: 1px solid var(--border-subtle); }
72
+ .sidebar-header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.01em; display: flex; align-items: center; gap: 6px; }
73
+ .sidebar-header p { font-size: 11px; color: var(--text-2); margin-top: 2px; }
74
+ .sidebar-nav { flex: 1; padding: 8px 0; overflow-y: auto; }
75
+ .nav-group { padding: 12px 12px 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-3); }
76
+ .nav-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; margin: 1px 8px; font-size: 13px; color: var(--text-1); cursor: pointer; border-radius: var(--radius); border: none; background: none; width: calc(100% - 16px); text-align: left; transition: all var(--transition); }
77
+ .nav-item:hover { background: var(--bg-2); color: var(--text-0); }
78
+ .nav-item.active { background: var(--accent-subtle); color: var(--accent); font-weight: 500; }
79
+ .nav-item svg { width: 16px; height: 16px; flex-shrink: 0; }
80
+ .sidebar-footer { padding: 12px; border-top: 1px solid var(--border-subtle); }
81
+
82
+ /* ── Main ───────────────────────────────────────── */
83
+ .main { flex: 1; margin-left: 220px; min-height: 100vh; }
84
+ .topbar { height: 48px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: var(--bg-1); position: sticky; top: 0; z-index: 5; }
85
+ .topbar-left { display: flex; align-items: center; gap: 12px; }
86
+ .topbar-right { display: flex; align-items: center; gap: 8px; }
87
+ .content { padding: 24px; max-width: 1280px; }
88
+ .page-header { margin-bottom: 20px; }
89
+ .page-title { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
90
+ .page-desc { font-size: 13px; color: var(--text-2); margin-top: 2px; }
91
+
92
+ /* ── Cards ──────────────────────────────────────── */
93
+ .card { background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
94
+ .card + .card { margin-top: 16px; }
95
+ .card-header { padding: 14px 16px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; justify-content: space-between; }
96
+ .card-header h3 { font-size: 13px; font-weight: 600; }
97
+ .card-body { padding: 16px; }
98
+ .card-body-flush { padding: 0; }
99
+
100
+ /* ── Stats Grid ─────────────────────────────────── */
101
+ .stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 20px; }
102
+ .stat { background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; }
103
+ .stat-label { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-2); }
104
+ .stat-value { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; margin-top: 2px; }
105
+ .stat-value.accent { color: var(--accent); }
106
+ .stat-value.success { color: var(--success); }
107
+ .stat-value.warning { color: var(--warning); }
108
+ .stat-sub { font-size: 11px; color: var(--text-2); margin-top: 2px; }
109
+
110
+ /* ── Table ──────────────────────────────────────── */
49
111
  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); }
112
+ th { text-align: left; padding: 8px 16px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-2); border-bottom: 1px solid var(--border); background: var(--bg-2); }
113
+ td { padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); vertical-align: middle; }
114
+ tr:last-child td { border-bottom: none; }
115
+ tr:hover td { background: var(--bg-2); }
116
+ .table-empty { text-align: center; padding: 32px 16px; color: var(--text-2); }
117
+
118
+ /* ── Badge ──────────────────────────────────────── */
119
+ .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; }
120
+ .badge-success { background: var(--success-subtle); color: var(--success); }
121
+ .badge-warning { background: var(--warning-subtle); color: var(--warning); }
122
+ .badge-danger { background: var(--danger-subtle); color: var(--danger); }
123
+ .badge-info { background: var(--info-subtle); color: var(--info); }
124
+ .badge-neutral { background: var(--bg-3); color: var(--text-2); }
125
+ .badge-dot { width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; display: inline-block; }
126
+ .badge-dot.green { background: var(--success); }
127
+ .badge-dot.yellow { background: var(--warning); }
128
+ .badge-dot.red { background: var(--danger); }
129
+
130
+ /* ── Buttons ────────────────────────────────────── */
131
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 7px 14px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--bg-1); color: var(--text-0); transition: all var(--transition); white-space: nowrap; }
132
+ .btn:hover { border-color: var(--border-strong); background: var(--bg-2); }
133
+ .btn:active { transform: scale(0.98); }
134
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
135
+ .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
68
136
  .btn-danger { color: var(--danger); }
137
+ .btn-danger:hover { background: var(--danger-subtle); border-color: var(--danger); }
138
+ .btn-ghost { border: none; background: none; color: var(--text-1); padding: 6px 8px; }
139
+ .btn-ghost:hover { background: var(--bg-2); color: var(--text-0); }
69
140
  .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 */
141
+ .btn-icon { padding: 6px; width: 32px; height: 32px; }
142
+ .btn-group { display: flex; gap: 6px; }
143
+
144
+ /* ── Inputs ─────────────────────────────────────── */
145
+ .input { padding: 7px 10px; background: var(--bg-0); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-0); font-size: 13px; font-family: var(--font); width: 100%; outline: none; transition: border-color var(--transition), box-shadow var(--transition); }
146
+ .input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-subtle); }
147
+ .input::placeholder { color: var(--text-3); }
148
+ select.input { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; padding-right: 28px; }
149
+ .field { margin-bottom: 12px; }
150
+ .field-label { display: block; font-size: 12px; font-weight: 500; color: var(--text-1); margin-bottom: 4px; }
151
+ textarea.input { resize: vertical; min-height: 72px; }
152
+ .search-input { max-width: 240px; }
153
+
154
+ /* ── Modal ──────────────────────────────────────── */
155
+ .modal-overlay { position: fixed; inset: 0; background: var(--overlay); display: flex; align-items: center; justify-content: center; z-index: 100; animation: fadeIn 0.15s; }
156
+ .modal { background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius-xl); padding: 0; width: 480px; max-width: 92vw; max-height: 85vh; overflow-y: auto; box-shadow: var(--shadow-lg); animation: scaleIn 0.15s; }
157
+ .modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border-subtle); }
158
+ .modal-header h2 { font-size: 15px; font-weight: 700; }
159
+ .modal-body { padding: 16px 20px; }
160
+ .modal-footer { padding: 12px 20px; border-top: 1px solid var(--border-subtle); display: flex; justify-content: flex-end; gap: 8px; }
161
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
162
+ @keyframes scaleIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
163
+
164
+ /* ── Toast ──────────────────────────────────────── */
165
+ .toast { position: fixed; bottom: 20px; right: 20px; background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 16px; font-size: 13px; z-index: 200; box-shadow: var(--shadow-lg); animation: slideUp 0.2s; display: flex; align-items: center; gap: 8px; }
166
+ .toast-success { border-left: 3px solid var(--success); }
167
+ .toast-error { border-left: 3px solid var(--danger); }
168
+ .toast-info { border-left: 3px solid var(--accent); }
169
+ @keyframes slideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
170
+
171
+ /* ── Tabs ───────────────────────────────────────── */
172
+ .tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
173
+ .tab { padding: 8px 14px; font-size: 13px; font-weight: 500; color: var(--text-2); cursor: pointer; border: none; background: none; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: all var(--transition); }
174
+ .tab:hover { color: var(--text-0); }
175
+ .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
176
+
177
+ /* ── Activity / Timeline ────────────────────────── */
178
+ .activity-item { display: flex; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); font-size: 13px; }
179
+ .activity-item:last-child { border-bottom: none; }
180
+ .activity-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); margin-top: 6px; flex-shrink: 0; }
181
+ .activity-time { font-size: 11px; color: var(--text-3); white-space: nowrap; }
182
+ .activity-text { flex: 1; color: var(--text-1); }
183
+ .activity-text strong { color: var(--text-0); font-weight: 600; }
184
+
185
+ /* ── Code / JSON ───────────────────────────────── */
186
+ .code-block { background: var(--bg-0); border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: 12px; font-family: var(--mono); font-size: 12px; overflow-x: auto; white-space: pre-wrap; color: var(--text-1); }
187
+ .inline-code { font-family: var(--mono); font-size: 12px; background: var(--bg-2); padding: 1px 5px; border-radius: 3px; }
188
+
189
+ /* ── Detail Row ─────────────────────────────────── */
190
+ .detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
191
+ .detail-item { }
192
+ .detail-label { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-2); }
193
+ .detail-value { font-size: 13px; margin-top: 1px; }
194
+
195
+ /* ── Empty State ────────────────────────────────── */
196
+ .empty { text-align: center; padding: 40px 20px; color: var(--text-2); }
197
+ .empty-title { font-size: 14px; font-weight: 600; color: var(--text-1); margin-bottom: 4px; }
198
+ .empty-desc { font-size: 13px; }
199
+
200
+ /* ── Responsive ─────────────────────────────────── */
101
201
  @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; }
202
+ .sidebar { width: 56px; } .sidebar-header h1 span, .sidebar-header p, .nav-item span, .nav-group { display: none; }
203
+ .nav-item { justify-content: center; padding: 10px; margin: 1px 4px; }
204
+ .main { margin-left: 56px; } .content { padding: 16px; }
205
+ .stats-row { grid-template-columns: 1fr 1fr; }
206
+ .detail-grid { grid-template-columns: 1fr; }
106
207
  }
208
+
209
+ /* ── Utility ────────────────────────────────────── */
210
+ .flex { display: flex; } .flex-col { flex-direction: column; }
211
+ .items-center { align-items: center; } .justify-between { justify-content: space-between; }
212
+ .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .gap-4 { gap: 16px; }
213
+ .mt-2 { margin-top: 8px; } .mt-3 { margin-top: 12px; } .mt-4 { margin-top: 16px; }
214
+ .mb-2 { margin-bottom: 8px; } .mb-3 { margin-bottom: 12px; }
215
+ .text-sm { font-size: 12px; } .text-xs { font-size: 11px; }
216
+ .text-muted { color: var(--text-2); } .text-accent { color: var(--accent); }
217
+ .font-mono { font-family: var(--mono); } .font-semibold { font-weight: 600; }
218
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
+ .w-full { width: 100%; }
220
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
107
221
  </style>
108
222
  </head>
109
223
  <body>
110
224
  <div id="root"></div>
225
+
226
+ <!-- API Documentation: All endpoints return JSON. Companies can build their own frontend. -->
227
+ <!-- See /health for server status, /auth/login for JWT tokens, /api/* for all CRUD operations. -->
228
+ <!-- Full API reference: https://github.com/agenticmail/agenticmail/tree/main/packages/enterprise -->
229
+
111
230
  <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
112
231
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
113
232
  <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
- ))
233
+ const { useState, useEffect, useCallback, useMemo, useRef, createElement: h, Fragment } = React;
234
+
235
+ // ════════════════════════════════════════════════════════
236
+ // API CLIENT
237
+ // ════════════════════════════════════════════════════════
238
+
239
+ const API = '/api';
240
+ let token = localStorage.getItem('am_token');
241
+
242
+ async function api(path, opts = {}) {
243
+ const headers = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...opts.headers };
244
+ const res = await fetch(`${API}${path}`, { ...opts, headers });
245
+ if (res.status === 401) { token = null; localStorage.removeItem('am_token'); location.reload(); }
246
+ const data = await res.json();
247
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
248
+ return data;
249
+ }
250
+
251
+ // ════════════════════════════════════════════════════════
252
+ // ICONS (inline SVG, no external deps)
253
+ // ════════════════════════════════════════════════════════
254
+
255
+ const Icon = (d, size = 16) => h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, ...d.map(p => h('path', { d: p })));
256
+ const Icons = {
257
+ dashboard: () => Icon(['M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z', 'M9 22V12h6v10']),
258
+ agents: () => Icon(['M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4-4v2', 'M23 21v-2a4 4 0 00-3-3.87', 'M16 3.13a4 4 0 010 7.75']),
259
+ skills: () => Icon(['M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z']),
260
+ kb: () => Icon(['M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z', 'M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z']),
261
+ activity: () => Icon(['M22 12h-4l-3 9L9 3l-3 9H2']),
262
+ approvals: () => Icon(['M9 11l3 3L22 4', 'M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11']),
263
+ settings: () => Icon(['M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z']),
264
+ sun: () => Icon(['M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42', 'M12 17a5 5 0 110-10 5 5 0 010 10z']),
265
+ moon: () => Icon(['M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z']),
266
+ logout: () => Icon(['M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4', 'M16 17l5-5-5-5', 'M21 12H9']),
267
+ plus: () => Icon(['M12 5v14M5 12h14']),
268
+ search: () => Icon(['M11 19a8 8 0 100-16 8 8 0 000 16z', 'M21 21l-4.35-4.35']),
269
+ refresh: () => Icon(['M1 4v6h6', 'M23 20v-6h-6', 'M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15']),
270
+ check: () => Icon(['M20 6L9 17l-5-5']),
271
+ x: () => Icon(['M18 6L6 18', 'M6 6l12 12']),
272
+ api: () => Icon(['M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71', 'M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71']),
273
+ db: () => Icon(['M12 2C6.48 2 2 4.02 2 6.5v11C2 19.98 6.48 22 12 22s10-2.02 10-4.5v-11C22 4.02 17.52 2 12 2z', 'M2 6.5C2 8.98 6.48 11 12 11s10-2.02 10-4.5', 'M2 12c0 2.48 4.48 4.5 10 4.5s10-2.02 10-4.5']),
274
+ org: () => Icon(['M3 21h18', 'M3 7v1a3 3 0 006 0V7m0 1a3 3 0 006 0V7m0 1a3 3 0 006 0V7', 'M5 21V10.9', 'M19 21V10.9', 'M12 21V10.9', 'M5 7l7-4 7 4']),
275
+ };
276
+
277
+ // ════════════════════════════════════════════════════════
278
+ // THEME
279
+ // ════════════════════════════════════════════════════════
280
+
281
+ function useTheme() {
282
+ const [theme, setTheme] = useState(() => localStorage.getItem('am_theme') || 'system');
283
+ useEffect(() => {
284
+ const effective = theme === 'system' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
285
+ document.documentElement.setAttribute('data-theme', effective);
286
+ localStorage.setItem('am_theme', theme);
287
+ }, [theme]);
288
+ const toggle = () => setTheme(t => t === 'dark' ? 'light' : t === 'light' ? 'system' : 'dark');
289
+ const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
290
+ return { theme, toggle, isDark };
291
+ }
292
+
293
+ // ════════════════════════════════════════════════════════
294
+ // SHARED COMPONENTS
295
+ // ════════════════════════════════════════════════════════
296
+
297
+ function Toast({ message, type = 'info', onDone }) {
298
+ useEffect(() => { const t = setTimeout(onDone, 3500); return () => clearTimeout(t); }, []);
299
+ return h('div', { className: `toast toast-${type}` }, message);
300
+ }
301
+
302
+ function Modal({ title, children, onClose, footer }) {
303
+ return h('div', { className: 'modal-overlay', onClick: e => e.target === e.currentTarget && onClose() },
304
+ h('div', { className: 'modal' },
305
+ h('div', { className: 'modal-header' },
306
+ h('div', { className: 'flex items-center justify-between' },
307
+ h('h2', null, title),
308
+ h('button', { className: 'btn-ghost btn-icon', onClick: onClose }, Icons.x())
309
+ )
209
310
  ),
210
- );
311
+ h('div', { className: 'modal-body' }, children),
312
+ footer && h('div', { className: 'modal-footer' }, footer)
313
+ )
314
+ );
315
+ }
316
+
317
+ function Stat({ label, value, color, sub }) {
318
+ return h('div', { className: 'stat' },
319
+ h('div', { className: 'stat-label' }, label),
320
+ h('div', { className: `stat-value ${color || ''}` }, value),
321
+ sub && h('div', { className: 'stat-sub' }, sub)
322
+ );
323
+ }
324
+
325
+ function Badge({ type, label, dot }) {
326
+ if (dot) return h('span', { className: `badge badge-${type}` }, h('span', { className: `badge-dot ${dot}` }), label);
327
+ return h('span', { className: `badge badge-${type}` }, label);
328
+ }
329
+
330
+ function EmptyState({ title, desc }) {
331
+ return h('div', { className: 'empty' }, h('div', { className: 'empty-title' }, title), h('div', { className: 'empty-desc' }, desc));
332
+ }
333
+
334
+ // ════════════════════════════════════════════════════════
335
+ // LOGIN PAGE
336
+ // ════════════════════════════════════════════════════════
337
+
338
+ function LoginPage({ onLogin }) {
339
+ const [email, setEmail] = useState('');
340
+ const [password, setPassword] = useState('');
341
+ const [error, setError] = useState('');
342
+ const [loading, setLoading] = useState(false);
343
+
344
+ async function submit(e) {
345
+ e.preventDefault(); setLoading(true); setError('');
346
+ try {
347
+ const data = await fetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }).then(r => r.json());
348
+ if (data.error) throw new Error(data.error);
349
+ token = data.token; localStorage.setItem('am_token', token); onLogin(data);
350
+ } catch (err) { setError(err.message); } finally { setLoading(false); }
211
351
  }
212
352
 
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'),
353
+ return h('div', { style: { display: 'flex', minHeight: '100vh', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-0)' } },
354
+ h('div', { style: { width: 360, padding: 32 } },
355
+ h('div', { style: { textAlign: 'center', marginBottom: 28 } },
356
+ h('h1', { style: { fontSize: 20, fontWeight: 700, marginBottom: 4 } }, '🎀 AgenticMail Enterprise'),
357
+ h('p', { className: 'text-muted text-sm' }, 'Sign in to manage your AI agents')
242
358
  ),
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
- )
359
+ h('form', { onSubmit: submit },
360
+ h('div', { className: 'field' },
361
+ h('label', { className: 'field-label' }, 'Email'),
362
+ h('input', { className: 'input', type: 'email', value: email, onChange: e => setEmail(e.target.value), placeholder: 'admin@company.com', required: true, autoFocus: true })
363
+ ),
364
+ h('div', { className: 'field' },
365
+ h('label', { className: 'field-label' }, 'Password'),
366
+ h('input', { className: 'input', type: 'password', value: password, onChange: e => setPassword(e.target.value), placeholder: '••••••••', required: true })
367
+ ),
368
+ error && h('div', { style: { color: 'var(--danger)', fontSize: 13, marginBottom: 12 } }, error),
369
+ h('button', { className: 'btn btn-primary w-full', type: 'submit', disabled: loading, style: { marginTop: 8, justifyContent: 'center' } }, loading ? 'Signing in...' : 'Sign In')
259
370
  ),
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
- )),
371
+ h('div', { style: { marginTop: 24, padding: 12, background: 'var(--bg-2)', borderRadius: 'var(--radius)', fontSize: 12, color: 'var(--text-2)' } },
372
+ h('strong', null, 'Build your own UI? '),
373
+ 'All data is available via REST API. ',
374
+ h('span', { className: 'inline-code' }, 'GET /api/agents'), ', ',
375
+ h('span', { className: 'inline-code' }, 'GET /api/stats'), ', etc. ',
376
+ 'See docs at ', h('a', { href: 'https://github.com/agenticmail/agenticmail', target: '_blank', style: { color: 'var(--accent)' } }, 'GitHub'), '.'
377
+ )
378
+ )
379
+ );
380
+ }
381
+
382
+ // ════════════════════════════════════════════════════════
383
+ // DASHBOARD PAGE
384
+ // ════════════════════════════════════════════════════════
385
+
386
+ function DashboardPage() {
387
+ const [stats, setStats] = useState(null);
388
+ const [recent, setRecent] = useState([]);
389
+ const [loading, setLoading] = useState(true);
390
+
391
+ useEffect(() => {
392
+ Promise.all([
393
+ api('/stats').catch(() => ({ agents: 0, users: 0, emails: 0, events: 0 })),
394
+ api('/audit?limit=10').catch(() => ({ events: [] })),
395
+ ]).then(([s, a]) => { setStats(s); setRecent(a.events || []); setLoading(false); });
396
+ }, []);
397
+
398
+ if (loading) return h('div', { className: 'text-muted', style: { padding: 40 } }, 'Loading...');
399
+
400
+ return h(Fragment, null,
401
+ h('div', { className: 'page-header' }, h('h1', { className: 'page-title' }, 'Dashboard'), h('p', { className: 'page-desc' }, 'System overview and recent activity')),
402
+ h('div', { className: 'stats-row' },
403
+ h(Stat, { label: 'Agents', value: stats?.agents ?? 0, color: 'accent', sub: 'Deployed' }),
404
+ h(Stat, { label: 'Users', value: stats?.users ?? 0 }),
405
+ h(Stat, { label: 'Emails Today', value: stats?.emails ?? 0, color: 'success' }),
406
+ h(Stat, { label: 'Events', value: stats?.events ?? 0 }),
407
+ ),
408
+ h('div', { className: 'card' },
409
+ h('div', { className: 'card-header' }, h('h3', null, 'Recent Activity')),
410
+ h('div', { className: 'card-body-flush' },
411
+ recent.length === 0
412
+ ? h(EmptyState, { title: 'No recent activity', desc: 'Activity will appear here as agents operate' })
413
+ : recent.map((e, i) => h('div', { key: i, className: 'activity-item' },
414
+ h('div', { className: 'activity-dot' }),
415
+ h('div', { className: 'activity-text' },
416
+ h('strong', null, e.action || e.type || 'Event'), ' ',
417
+ e.details || e.description || JSON.stringify(e).slice(0, 80)
418
+ ),
419
+ h('div', { className: 'activity-time' }, e.timestamp ? new Date(e.timestamp).toLocaleString() : '')
420
+ ))
421
+ )
422
+ ),
423
+ h('div', { className: 'card mt-3' },
424
+ h('div', { className: 'card-header' }, h('h3', null, 'API Endpoints')),
425
+ h('div', { className: 'card-body' },
426
+ h('p', { className: 'text-sm text-muted mb-2' }, 'Build your own dashboard or integrate with your existing tools:'),
427
+ h('div', { className: 'code-block' },
428
+ 'GET /health # Server health\n' +
429
+ 'POST /auth/login # Get JWT token\n' +
430
+ 'GET /api/stats # Dashboard stats\n' +
431
+ 'GET /api/agents # List all agents\n' +
432
+ 'POST /api/agents # Create agent\n' +
433
+ 'GET /api/agents/:id # Agent details\n' +
434
+ 'GET /api/audit # Audit log\n' +
435
+ 'GET /api/api-keys # API key management\n' +
436
+ 'GET /api/engine/skills # Available skills\n' +
437
+ 'GET /api/engine/profiles/presets # Permission presets\n' +
438
+ 'POST /api/engine/config/workspace # Generate agent config\n' +
439
+ 'POST /api/engine/agents # Deploy managed agent\n' +
440
+ 'GET /api/engine/stats/:orgId # Org stats\n'
271
441
  )
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
- )))
442
+ )
443
+ )
444
+ );
445
+ }
446
+
447
+ // ════════════════════════════════════════════════════════
448
+ // AGENTS PAGE
449
+ // ════════════════════════════════════════════════════════
450
+
451
+ function AgentsPage({ showToast }) {
452
+ const [agents, setAgents] = useState([]);
453
+ const [loading, setLoading] = useState(true);
454
+ const [search, setSearch] = useState('');
455
+ const [showCreate, setShowCreate] = useState(false);
456
+
457
+ const load = () => { setLoading(true); api('/agents').then(d => setAgents(d.agents || d || [])).catch(() => setAgents([])).finally(() => setLoading(false)); };
458
+ useEffect(load, []);
459
+
460
+ const filtered = agents.filter(a => !search || a.name?.toLowerCase().includes(search.toLowerCase()) || a.email?.toLowerCase().includes(search.toLowerCase()));
461
+
462
+ return h(Fragment, null,
463
+ h('div', { className: 'page-header flex items-center justify-between' },
464
+ h('div', null, h('h1', { className: 'page-title' }, 'Agents'), h('p', { className: 'page-desc' }, `${agents.length} agent${agents.length !== 1 ? 's' : ''} registered`)),
465
+ h('button', { className: 'btn btn-primary', onClick: () => setShowCreate(true) }, Icons.plus(), ' New Agent')
466
+ ),
467
+ h('div', { className: 'mb-3' }, h('input', { className: 'input search-input', placeholder: 'Search agents...', value: search, onChange: e => setSearch(e.target.value) })),
468
+ h('div', { className: 'card' },
469
+ h('div', { className: 'card-body-flush' },
470
+ loading ? h('div', { className: 'table-empty' }, 'Loading...') :
471
+ filtered.length === 0 ? h(EmptyState, { title: 'No agents', desc: search ? 'No agents match your search' : 'Create your first agent to get started' }) :
472
+ h('div', { style: { overflowX: 'auto' } },
473
+ h('table', null,
474
+ 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'))),
475
+ h('tbody', null, filtered.map(a =>
476
+ h('tr', { key: a.id || a.name },
477
+ h('td', null, h('strong', null, a.name || a.displayName)),
478
+ h('td', null, h('span', { className: 'font-mono text-sm' }, a.email)),
479
+ h('td', null, a.role || '-'),
480
+ h('td', null, h(Badge, { type: a.status === 'active' ? 'success' : 'neutral', dot: a.status === 'active' ? 'green' : '', label: a.status || 'active' })),
481
+ h('td', { className: 'text-muted text-sm' }, a.createdAt ? new Date(a.createdAt).toLocaleDateString() : '-')
313
482
  )
483
+ ))
484
+ )
314
485
  )
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
- );
486
+ )
487
+ ),
488
+ showCreate && h(CreateAgentModal, { onClose: () => setShowCreate(false), onCreated: () => { setShowCreate(false); load(); showToast('Agent created', 'success'); } })
489
+ );
490
+ }
491
+
492
+ function CreateAgentModal({ onClose, onCreated }) {
493
+ const [name, setName] = useState('');
494
+ const [role, setRole] = useState('assistant');
495
+ const [loading, setLoading] = useState(false);
496
+ const [error, setError] = useState('');
497
+
498
+ async function submit(e) {
499
+ e.preventDefault(); setLoading(true); setError('');
500
+ try { await api('/agents', { method: 'POST', body: JSON.stringify({ name, role }) }); onCreated(); }
501
+ catch (err) { setError(err.message); } finally { setLoading(false); }
331
502
  }
332
503
 
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
- )
504
+ return h(Modal, { title: 'Create Agent', onClose, footer: h(Fragment, null,
505
+ h('button', { className: 'btn', onClick: onClose }, 'Cancel'),
506
+ h('button', { className: 'btn btn-primary', onClick: submit, disabled: loading || !name }, loading ? 'Creating...' : 'Create Agent')
507
+ ) },
508
+ h('form', { onSubmit: submit },
509
+ h('div', { className: 'field' }, h('label', { className: 'field-label' }, 'Agent Name'), h('input', { className: 'input', value: name, onChange: e => setName(e.target.value), placeholder: 'researcher', autoFocus: true })),
510
+ h('div', { className: 'field' }, h('label', { className: 'field-label' }, 'Role'), h('select', { className: 'input', value: role, onChange: e => setRole(e.target.value) },
511
+ h('option', { value: 'secretary' }, 'Secretary'), h('option', { value: 'assistant' }, 'Assistant'),
512
+ h('option', { value: 'researcher' }, 'Researcher'), h('option', { value: 'writer' }, 'Writer'), h('option', { value: 'custom' }, 'Custom')
513
+ )),
514
+ error && h('div', { style: { color: 'var(--danger)', fontSize: 13, marginTop: 8 } }, error)
515
+ )
516
+ );
517
+ }
518
+
519
+ // ════════════════════════════════════════════════════════
520
+ // SKILLS PAGE
521
+ // ════════════════════════════════════════════════════════
522
+
523
+ function SkillsPage() {
524
+ const [skills, setSkills] = useState([]);
525
+ const [presets, setPresets] = useState([]);
526
+ const [tab, setTab] = useState('skills');
527
+ const [search, setSearch] = useState('');
528
+
529
+ useEffect(() => {
530
+ api('/engine/skills').then(d => setSkills(d.skills || Object.values(d) || [])).catch(() => {});
531
+ api('/engine/profiles/presets').then(d => setPresets(d.presets || Object.values(d) || [])).catch(() => {});
532
+ }, []);
533
+
534
+ const filtered = skills.filter(s => !search || (s.name || s.id || '').toLowerCase().includes(search.toLowerCase()) || (s.category || '').toLowerCase().includes(search.toLowerCase()));
535
+ const byCategory = {};
536
+ filtered.forEach(s => { const cat = s.category || 'Other'; (byCategory[cat] = byCategory[cat] || []).push(s); });
537
+
538
+ return h(Fragment, null,
539
+ h('div', { className: 'page-header' }, h('h1', { className: 'page-title' }, 'Skills & Permissions'), h('p', { className: 'page-desc' }, 'Configure what tools each agent can access')),
540
+ h('div', { className: 'tabs' },
541
+ h('button', { className: `tab ${tab === 'skills' ? 'active' : ''}`, onClick: () => setTab('skills') }, `Skills (${skills.length})`),
542
+ h('button', { className: `tab ${tab === 'presets' ? 'active' : ''}`, onClick: () => setTab('presets') }, `Presets (${presets.length})`),
543
+ h('button', { className: `tab ${tab === 'api' ? 'active' : ''}`, onClick: () => setTab('api') }, 'API Reference'),
544
+ ),
545
+ tab === 'skills' && h(Fragment, null,
546
+ h('div', { className: 'mb-3' }, h('input', { className: 'input search-input', placeholder: 'Search skills...', value: search, onChange: e => setSearch(e.target.value) })),
547
+ Object.entries(byCategory).map(([cat, items]) =>
548
+ h('div', { key: cat, className: 'card mb-3' },
549
+ h('div', { className: 'card-header' }, h('h3', null, cat, h('span', { className: 'text-muted text-sm', style: { marginLeft: 8, fontWeight: 400 } }, `${items.length} skill${items.length !== 1 ? 's' : ''}`))),
550
+ h('div', { className: 'card-body-flush' },
551
+ h('table', null,
552
+ h('thead', null, h('tr', null, h('th', null, 'Skill'), h('th', null, 'Risk'), h('th', null, 'Tools'), h('th', null, 'Status'))),
553
+ h('tbody', null, items.map(s =>
554
+ h('tr', { key: s.id || s.name },
555
+ h('td', null, h('strong', null, s.name || s.id), s.description && h('div', { className: 'text-xs text-muted', style: { maxWidth: 300 } }, s.description.slice(0, 80))),
556
+ h('td', null, h(Badge, { type: s.risk === 'high' ? 'danger' : s.risk === 'medium' ? 'warning' : 'success', label: s.risk || 'low' })),
557
+ h('td', { className: 'text-muted text-sm' }, s.tools?.length || 0),
558
+ h('td', null, h(Badge, { type: 'success', dot: 'green', label: 'Available' }))
559
+ )
560
+ ))
561
+ )
562
+ )
385
563
  )
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' })),
564
+ )
565
+ ),
566
+ tab === 'presets' && h('div', { className: 'card' },
567
+ h('div', { className: 'card-header' }, h('h3', null, 'Permission Presets')),
568
+ h('div', { className: 'card-body-flush' },
569
+ presets.length === 0 ? h(EmptyState, { title: 'No presets loaded', desc: 'Engine may not be initialized' }) :
570
+ h('table', null,
571
+ h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Description'), h('th', null, 'Allowed Skills'))),
572
+ h('tbody', null, presets.map((p, i) =>
573
+ h('tr', { key: i },
574
+ h('td', null, h('strong', null, p.name || p.id)),
575
+ h('td', { className: 'text-muted text-sm' }, p.description || '-'),
576
+ h('td', { className: 'text-sm' }, p.allowedSkills?.length || p.skills?.length || '-')
577
+ )
578
+ ))
394
579
  )
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
- )))
580
+ )
581
+ ),
582
+ tab === 'api' && h('div', { className: 'card' },
583
+ h('div', { className: 'card-header' }, h('h3', null, 'Skills & Permissions API')),
584
+ h('div', { className: 'card-body' },
585
+ h('div', { className: 'code-block' },
586
+ '# List all available skills\n' +
587
+ 'GET /api/engine/skills\n' +
588
+ '→ { skills: [{ id, name, category, description, risk, tools: [...] }] }\n\n' +
589
+ '# List permission presets\n' +
590
+ 'GET /api/engine/profiles/presets\n' +
591
+ '→ { presets: [{ name, description, allowedSkills, riskThreshold }] }\n\n' +
592
+ '# Check if agent has permission for a tool\n' +
593
+ 'POST /api/engine/permissions/check\n' +
594
+ '→ { allowed: boolean, reason: string }\n\n' +
595
+ '# Get agent\'s effective tool policy\n' +
596
+ 'GET /api/engine/permissions/:agentId/policy\n' +
597
+ ' { allowedTools: [...], blockedTools: [...] }'
598
+ )
599
+ )
600
+ )
601
+ );
602
+ }
603
+
604
+ // ════════════════════════════════════════════════════════
605
+ // KNOWLEDGE BASE PAGE
606
+ // ════════════════════════════════════════════════════════
607
+
608
+ function KnowledgeBasePage() {
609
+ const [kbs, setKbs] = useState([]);
610
+ useEffect(() => { api('/engine/knowledge').then(d => setKbs(d.bases || [])).catch(() => {}); }, []);
611
+
612
+ return h(Fragment, null,
613
+ h('div', { className: 'page-header flex items-center justify-between' },
614
+ h('div', null, h('h1', { className: 'page-title' }, 'Knowledge Bases'), h('p', { className: 'page-desc' }, 'Document ingestion and RAG retrieval for agents')),
615
+ h('button', { className: 'btn btn-primary' }, Icons.plus(), ' New Knowledge Base')
616
+ ),
617
+ kbs.length === 0
618
+ ? h('div', { className: 'card' }, h('div', { className: 'card-body' }, h(EmptyState, { title: 'No knowledge bases', desc: 'Create a knowledge base to give agents access to your documents' })))
619
+ : h('div', { className: 'card' },
620
+ h('div', { className: 'card-body-flush' },
621
+ h('table', null,
622
+ h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Documents'), h('th', null, 'Chunks'), h('th', null, 'Agents'), h('th', null, 'Created'))),
623
+ h('tbody', null, kbs.map(kb =>
624
+ h('tr', { key: kb.id },
625
+ h('td', null, h('strong', null, kb.name)),
626
+ h('td', null, kb.documentCount || 0),
627
+ h('td', null, kb.chunkCount || 0),
628
+ h('td', null, kb.agentIds?.length || 0),
629
+ h('td', { className: 'text-muted text-sm' }, kb.createdAt ? new Date(kb.createdAt).toLocaleDateString() : '-')
427
630
  )
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
- )
631
+ ))
434
632
  )
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://...' })),
633
+ )
634
+ )
635
+ );
636
+ }
637
+
638
+ // ════════════════════════════════════════════════════════
639
+ // ACTIVITY PAGE
640
+ // ════════════════════════════════════════════════════════
641
+
642
+ function ActivityPage() {
643
+ const [events, setEvents] = useState([]);
644
+ const [tab, setTab] = useState('events');
645
+
646
+ useEffect(() => { api('/audit?limit=50').then(d => setEvents(d.events || [])).catch(() => {}); }, []);
647
+
648
+ return h(Fragment, null,
649
+ h('div', { className: 'page-header' }, h('h1', { className: 'page-title' }, 'Activity & Audit'), h('p', { className: 'page-desc' }, 'Real-time monitoring and audit trail')),
650
+ h('div', { className: 'tabs' },
651
+ h('button', { className: `tab ${tab === 'events' ? 'active' : ''}`, onClick: () => setTab('events') }, 'Events'),
652
+ h('button', { className: `tab ${tab === 'api' ? 'active' : ''}`, onClick: () => setTab('api') }, 'API Reference'),
653
+ ),
654
+ tab === 'events' && h('div', { className: 'card' },
655
+ h('div', { className: 'card-header' }, h('h3', null, 'Audit Log'), h('button', { className: 'btn btn-sm btn-ghost', onClick: () => api('/audit?limit=50').then(d => setEvents(d.events || [])) }, Icons.refresh(), ' Refresh')),
656
+ h('div', { className: 'card-body-flush' },
657
+ events.length === 0 ? h(EmptyState, { title: 'No events yet', desc: 'Activity will appear here as the system operates' }) :
658
+ events.map((e, i) => h('div', { key: i, className: 'activity-item' },
659
+ h('div', { className: 'activity-dot', style: { background: e.action?.includes('delete') ? 'var(--danger)' : e.action?.includes('create') ? 'var(--success)' : 'var(--accent)' } }),
660
+ h('div', { style: { flex: 1 } },
661
+ h('div', null, h('strong', null, e.action || e.type), ' ', h('span', { className: 'text-muted' }, e.resource || '')),
662
+ e.userId && h('div', { className: 'text-xs text-muted' }, 'by ', e.userId)
471
663
  ),
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`),
664
+ h('div', { className: 'activity-time' }, e.timestamp ? new Date(e.timestamp).toLocaleString() : '')
665
+ ))
666
+ )
667
+ ),
668
+ tab === 'api' && h('div', { className: 'card' },
669
+ h('div', { className: 'card-header' }, h('h3', null, 'Activity & Monitoring API')),
670
+ h('div', { className: 'card-body' },
671
+ h('div', { className: 'code-block' },
672
+ '# Audit log\n' +
673
+ 'GET /api/audit?limit=50\n' +
674
+ '→ { events: [{ action, resource, userId, timestamp, details }] }\n\n' +
675
+ '# Engine activity events\n' +
676
+ 'GET /api/engine/activity/:orgId/events?limit=100\n' +
677
+ '→ { events: [{ agentId, type, data, createdAt }] }\n\n' +
678
+ '# Tool call records\n' +
679
+ 'GET /api/engine/activity/:orgId/tools?agentId=&limit=50\n' +
680
+ '→ { calls: [{ toolName, timing, cost, permission }] }\n\n' +
681
+ '# Real-time SSE stream\n' +
682
+ 'GET /api/engine/activity/:orgId/stream\n' +
683
+ '→ Server-Sent Events (text/event-stream)\n\n' +
684
+ '# Agent timeline\n' +
685
+ 'GET /api/engine/activity/:orgId/timeline/:agentId?date=YYYY-MM-DD\n' +
686
+ '→ { entries: [{ time, type, summary }] }'
687
+ )
688
+ )
689
+ )
690
+ );
691
+ }
692
+
693
+ // ════════════════════════════════════════════════════════
694
+ // APPROVALS PAGE
695
+ // ════════════════════════════════════════════════════════
696
+
697
+ function ApprovalsPage({ showToast }) {
698
+ const [pending, setPending] = useState([]);
699
+ useEffect(() => { api('/engine/approvals/pending').then(d => setPending(d.requests || [])).catch(() => {}); }, []);
700
+
701
+ return h(Fragment, null,
702
+ h('div', { className: 'page-header' }, h('h1', { className: 'page-title' }, 'Approvals'), h('p', { className: 'page-desc' }, 'Review and approve sensitive agent actions')),
703
+ h('div', { className: 'card' },
704
+ h('div', { className: 'card-header' }, h('h3', null, `Pending (${pending.length})`)),
705
+ h('div', { className: 'card-body-flush' },
706
+ pending.length === 0 ? h(EmptyState, { title: 'No pending approvals', desc: 'When agents attempt sensitive operations, they\'ll appear here for review' }) :
707
+ pending.map(r => h('div', { key: r.id, style: { padding: 16, borderBottom: '1px solid var(--border-subtle)' } },
708
+ h('div', { className: 'flex items-center justify-between' },
709
+ h('div', null,
710
+ h('strong', null, r.toolName || r.tool_name), ' — ',
711
+ h('span', { className: 'text-muted' }, r.agentName || r.agent_name),
712
+ h('div', { className: 'text-xs text-muted mt-2' }, r.reason)
713
+ ),
714
+ h('div', { className: 'btn-group' },
715
+ h('button', { className: 'btn btn-sm btn-primary', onClick: () => api(`/engine/approvals/${r.id}/decide`, { method: 'POST', body: JSON.stringify({ decision: 'approved' }) }).then(() => { setPending(p => p.filter(x => x.id !== r.id)); showToast('Approved', 'success'); }) }, Icons.check(), ' Approve'),
716
+ h('button', { className: 'btn btn-sm btn-danger', onClick: () => api(`/engine/approvals/${r.id}/decide`, { method: 'POST', body: JSON.stringify({ decision: 'denied' }) }).then(() => { setPending(p => p.filter(x => x.id !== r.id)); showToast('Denied', 'info'); }) }, Icons.x(), ' Deny')
717
+ )
718
+ )
719
+ ))
720
+ )
721
+ )
722
+ );
723
+ }
724
+
725
+ // ════════════════════════════════════════════════════════
726
+ // SETTINGS PAGE
727
+ // ════════════════════════════════════════════════════════
728
+
729
+ function SettingsPage() {
730
+ const [apiKeys, setApiKeys] = useState([]);
731
+ const [tab, setTab] = useState('general');
732
+ useEffect(() => { api('/api-keys').then(d => setApiKeys(d.keys || d || [])).catch(() => {}); }, []);
733
+
734
+ return h(Fragment, null,
735
+ h('div', { className: 'page-header' }, h('h1', { className: 'page-title' }, 'Settings')),
736
+ h('div', { className: 'tabs' },
737
+ h('button', { className: `tab ${tab === 'general' ? 'active' : ''}`, onClick: () => setTab('general') }, 'General'),
738
+ h('button', { className: `tab ${tab === 'keys' ? 'active' : ''}`, onClick: () => setTab('keys') }, 'API Keys'),
739
+ h('button', { className: `tab ${tab === 'api' ? 'active' : ''}`, onClick: () => setTab('api') }, 'Integration Guide'),
740
+ ),
741
+ tab === 'general' && h('div', { className: 'card' },
742
+ h('div', { className: 'card-header' }, h('h3', null, 'Server Info')),
743
+ h('div', { className: 'card-body' },
744
+ h('div', { className: 'detail-grid' },
745
+ h('div', { className: 'detail-item' }, h('div', { className: 'detail-label' }, 'Version'), h('div', { className: 'detail-value' }, '0.2.2')),
746
+ h('div', { className: 'detail-item' }, h('div', { className: 'detail-label' }, 'API Base'), h('div', { className: 'detail-value font-mono' }, window.location.origin + '/api')),
747
+ h('div', { className: 'detail-item' }, h('div', { className: 'detail-label' }, 'Health'), h('div', { className: 'detail-value font-mono' }, window.location.origin + '/health')),
748
+ h('div', { className: 'detail-item' }, h('div', { className: 'detail-label' }, 'Engine'), h('div', { className: 'detail-value font-mono' }, window.location.origin + '/api/engine')),
749
+ )
750
+ )
751
+ ),
752
+ tab === 'keys' && h('div', { className: 'card' },
753
+ h('div', { className: 'card-header' }, h('h3', null, 'API Keys'), h('button', { className: 'btn btn-sm btn-primary' }, Icons.plus(), ' Create Key')),
754
+ h('div', { className: 'card-body-flush' },
755
+ apiKeys.length === 0 ? h(EmptyState, { title: 'No API keys', desc: 'Create an API key to access the REST API programmatically' }) :
756
+ h('table', null,
757
+ h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Key'), h('th', null, 'Scopes'), h('th', null, 'Created'))),
758
+ h('tbody', null, apiKeys.map(k =>
759
+ h('tr', { key: k.id },
760
+ h('td', null, h('strong', null, k.name)),
761
+ h('td', null, h('span', { className: 'font-mono text-sm' }, k.key?.slice(0, 12) + '...')),
762
+ h('td', null, (k.scopes || []).join(', ') || 'all'),
763
+ h('td', { className: 'text-muted text-sm' }, k.createdAt ? new Date(k.createdAt).toLocaleDateString() : '-')
764
+ )
765
+ ))
766
+ )
767
+ )
768
+ ),
769
+ tab === 'api' && h('div', { className: 'card' },
770
+ h('div', { className: 'card-header' }, h('h3', null, 'Build Your Own Frontend')),
771
+ h('div', { className: 'card-body' },
772
+ h('p', { className: 'text-sm mb-3' }, 'AgenticMail Enterprise exposes a complete REST API. You can build your own dashboard in any language or framework.'),
773
+ h('div', { className: 'code-block mb-3' },
774
+ '// Example: Fetch agents from your React/Vue/Svelte app\n' +
775
+ 'const response = await fetch("' + window.location.origin + '/api/agents", {\n' +
776
+ ' headers: {\n' +
777
+ ' "Authorization": "Bearer " + token,\n' +
778
+ ' "Content-Type": "application/json"\n' +
779
+ ' }\n' +
780
+ '});\n' +
781
+ 'const { agents } = await response.json();\n' +
782
+ '// → [{ id, name, email, role, status, createdAt }]'
480
783
  ),
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)' : ''),
784
+ h('p', { className: 'text-sm mb-2', style: { fontWeight: 600 } }, 'Authentication'),
785
+ h('div', { className: 'code-block mb-3' },
786
+ '// Option 1: JWT token (from /auth/login)\n' +
787
+ 'headers: { "Authorization": "Bearer eyJhbGci..." }\n\n' +
788
+ '// Option 2: API key (from Settings > API Keys)\n' +
789
+ 'headers: { "X-API-Key": "ak_your_key_here" }'
487
790
  ),
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 },
791
+ h('p', { className: 'text-sm mb-2', style: { fontWeight: 600 } }, 'Response Format'),
792
+ h('p', { className: 'text-sm text-muted mb-2' }, 'All endpoints return JSON. Errors include an "error" field with a human-readable message. Successful responses return the data directly.'),
793
+ h('p', { className: 'text-sm' }, 'Full API reference and example implementations in 8 languages at ', h('a', { href: 'https://github.com/agenticmail/agenticmail/tree/main/packages/enterprise/dashboards', target: '_blank', style: { color: 'var(--accent)' } }, 'GitHub'))
794
+ )
795
+ )
796
+ );
797
+ }
798
+
799
+ // ════════════════════════════════════════════════════════
800
+ // APP SHELL
801
+ // ════════════════════════════════════════════════════════
802
+
803
+ function App() {
804
+ const [authed, setAuthed] = useState(!!token);
805
+ const [page, setPage] = useState('dashboard');
806
+ const [toast, setToast] = useState(null);
807
+ const { theme, toggle, isDark } = useTheme();
808
+
809
+ const showToast = (message, type = 'info') => setToast({ message, type });
810
+
811
+ if (!authed) return h(LoginPage, { onLogin: () => setAuthed(true) });
812
+
813
+ const nav = [
814
+ { section: 'Overview' },
815
+ { id: 'dashboard', label: 'Dashboard', icon: Icons.dashboard },
816
+ { id: 'agents', label: 'Agents', icon: Icons.agents },
817
+ { section: 'Configuration' },
818
+ { id: 'skills', label: 'Skills & Permissions', icon: Icons.skills },
819
+ { id: 'knowledge', label: 'Knowledge Bases', icon: Icons.kb },
820
+ { section: 'Operations' },
821
+ { id: 'activity', label: 'Activity & Audit', icon: Icons.activity },
822
+ { id: 'approvals', label: 'Approvals', icon: Icons.approvals },
823
+ { section: 'System' },
824
+ { id: 'settings', label: 'Settings', icon: Icons.settings },
825
+ ];
826
+
827
+ const pages = {
828
+ dashboard: h(DashboardPage),
829
+ agents: h(AgentsPage, { showToast }),
830
+ skills: h(SkillsPage),
831
+ knowledge: h(KnowledgeBasePage),
832
+ activity: h(ActivityPage),
833
+ approvals: h(ApprovalsPage, { showToast }),
834
+ settings: h(SettingsPage),
500
835
  };
501
836
 
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'),
837
+ return h(Fragment, null,
838
+ // Sidebar
839
+ h('nav', { className: 'sidebar' },
840
+ h('div', { className: 'sidebar-header' },
841
+ h('h1', null, '🎀 AgenticMail ', h('span', { className: 'text-muted', style: { fontWeight: 400 } }, 'Enterprise')),
842
+ h('p', null, 'AI Agent Management')
843
+ ),
844
+ h('div', { className: 'sidebar-nav' },
845
+ nav.map((item, i) =>
846
+ item.section ? h('div', { key: i, className: 'nav-group' }, item.section) :
847
+ h('button', { key: item.id, className: `nav-item ${page === item.id ? 'active' : ''}`, onClick: () => setPage(item.id) },
848
+ item.icon(), h('span', null, item.label)
849
+ )
850
+ )
851
+ ),
852
+ h('div', { className: 'sidebar-footer' },
853
+ h('button', { className: 'nav-item', onClick: toggle, title: `Theme: ${theme}` }, isDark ? Icons.sun() : Icons.moon(), h('span', null, isDark ? 'Light Mode' : 'Dark Mode')),
854
+ h('button', { className: 'nav-item', onClick: () => { token = null; localStorage.removeItem('am_token'); setAuthed(false); } }, Icons.logout(), h('span', null, 'Sign Out'))
855
+ )
856
+ ),
857
+ // Main
858
+ h('div', { className: 'main' },
859
+ h('div', { className: 'topbar' },
860
+ h('div', { className: 'topbar-left' },
861
+ h('span', { className: 'text-sm text-muted' }, nav.find(n => n.id === page)?.label || 'Dashboard')
548
862
  ),
863
+ h('div', { className: 'topbar-right' },
864
+ h('a', { href: 'https://github.com/agenticmail/agenticmail', target: '_blank', className: 'btn btn-ghost btn-sm', title: 'Documentation' }, Icons.api(), h('span', { className: 'text-sm' }, 'API Docs'))
865
+ )
549
866
  ),
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));
867
+ h('div', { className: 'content' }, pages[page] || h('div', null, 'Page not found'))
868
+ ),
869
+ // Toast
870
+ toast && h(Toast, { ...toast, onDone: () => setToast(null) })
871
+ );
872
+ }
873
+
874
+ // ════════════════════════════════════════════════════════
875
+ // MOUNT
876
+ // ════════════════════════════════════════════════════════
877
+
878
+ ReactDOM.createRoot(document.getElementById('root')).render(h(App));
556
879
  </script>
557
880
  </body>
558
881
  </html>