@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.
- package/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgenticMail Enterprise Dashboard — Python/Flask Edition
|
|
3
|
+
|
|
4
|
+
Setup:
|
|
5
|
+
pip install flask requests
|
|
6
|
+
python app.py
|
|
7
|
+
|
|
8
|
+
Or with environment variable:
|
|
9
|
+
AGENTICMAIL_URL=https://your-company.agenticmail.cloud python app.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os, requests
|
|
13
|
+
from flask import Flask, render_template_string, request, session, redirect, url_for, flash
|
|
14
|
+
from functools import wraps
|
|
15
|
+
|
|
16
|
+
app = Flask(__name__)
|
|
17
|
+
app.secret_key = os.urandom(32)
|
|
18
|
+
API_URL = os.getenv('AGENTICMAIL_URL', 'http://localhost:3000')
|
|
19
|
+
|
|
20
|
+
# ─── API Client ──────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
def api(path, method='GET', json=None):
|
|
23
|
+
headers = {'Content-Type': 'application/json'}
|
|
24
|
+
token = session.get('token')
|
|
25
|
+
if token:
|
|
26
|
+
headers['Authorization'] = f'Bearer {token}'
|
|
27
|
+
try:
|
|
28
|
+
r = requests.request(method, f'{API_URL}{path}', headers=headers, json=json, timeout=10)
|
|
29
|
+
return r.json()
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return {'error': str(e)}
|
|
32
|
+
|
|
33
|
+
def login_required(f):
|
|
34
|
+
@wraps(f)
|
|
35
|
+
def decorated(*args, **kwargs):
|
|
36
|
+
if 'token' not in session:
|
|
37
|
+
return redirect(url_for('login'))
|
|
38
|
+
return f(*args, **kwargs)
|
|
39
|
+
return decorated
|
|
40
|
+
|
|
41
|
+
# ─── Routes ──────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@app.route('/login', methods=['GET', 'POST'])
|
|
44
|
+
def login():
|
|
45
|
+
error = None
|
|
46
|
+
if request.method == 'POST':
|
|
47
|
+
data = api('/auth/login', 'POST', {
|
|
48
|
+
'email': request.form['email'],
|
|
49
|
+
'password': request.form['password'],
|
|
50
|
+
})
|
|
51
|
+
if 'token' in data:
|
|
52
|
+
session['token'] = data['token']
|
|
53
|
+
session['user'] = data['user']
|
|
54
|
+
return redirect(url_for('dashboard'))
|
|
55
|
+
error = data.get('error', 'Login failed')
|
|
56
|
+
return render_template_string(LOGIN_TEMPLATE, error=error, api_url=API_URL)
|
|
57
|
+
|
|
58
|
+
@app.route('/logout')
|
|
59
|
+
def logout():
|
|
60
|
+
session.clear()
|
|
61
|
+
return redirect(url_for('login'))
|
|
62
|
+
|
|
63
|
+
@app.route('/')
|
|
64
|
+
@login_required
|
|
65
|
+
def dashboard():
|
|
66
|
+
stats = api('/api/stats')
|
|
67
|
+
audit = api('/api/audit?limit=8')
|
|
68
|
+
return render_page('dashboard', stats=stats, audit=audit.get('events', []))
|
|
69
|
+
|
|
70
|
+
@app.route('/agents')
|
|
71
|
+
@login_required
|
|
72
|
+
def agents():
|
|
73
|
+
data = api('/api/agents')
|
|
74
|
+
return render_page('agents', agents=data.get('agents', []))
|
|
75
|
+
|
|
76
|
+
@app.route('/agents/create', methods=['POST'])
|
|
77
|
+
@login_required
|
|
78
|
+
def create_agent():
|
|
79
|
+
body = {'name': request.form['name'], 'role': request.form.get('role', 'assistant')}
|
|
80
|
+
if request.form.get('email'):
|
|
81
|
+
body['email'] = request.form['email']
|
|
82
|
+
result = api('/api/agents', 'POST', body)
|
|
83
|
+
flash('Agent created!' if 'id' in result else result.get('error', 'Failed'))
|
|
84
|
+
return redirect(url_for('agents'))
|
|
85
|
+
|
|
86
|
+
@app.route('/agents/<id>/archive', methods=['POST'])
|
|
87
|
+
@login_required
|
|
88
|
+
def archive_agent(id):
|
|
89
|
+
api(f'/api/agents/{id}/archive', 'POST')
|
|
90
|
+
flash('Agent archived')
|
|
91
|
+
return redirect(url_for('agents'))
|
|
92
|
+
|
|
93
|
+
@app.route('/users')
|
|
94
|
+
@login_required
|
|
95
|
+
def users():
|
|
96
|
+
data = api('/api/users')
|
|
97
|
+
return render_page('users', users=data.get('users', []))
|
|
98
|
+
|
|
99
|
+
@app.route('/users/create', methods=['POST'])
|
|
100
|
+
@login_required
|
|
101
|
+
def create_user():
|
|
102
|
+
result = api('/api/users', 'POST', {
|
|
103
|
+
'name': request.form['name'], 'email': request.form['email'],
|
|
104
|
+
'role': request.form.get('role', 'member'), 'password': request.form['password'],
|
|
105
|
+
})
|
|
106
|
+
flash('User created!' if 'id' in result else result.get('error', 'Failed'))
|
|
107
|
+
return redirect(url_for('users'))
|
|
108
|
+
|
|
109
|
+
@app.route('/api-keys')
|
|
110
|
+
@login_required
|
|
111
|
+
def api_keys():
|
|
112
|
+
data = api('/api/api-keys')
|
|
113
|
+
return render_page('api_keys', keys=data.get('keys', []))
|
|
114
|
+
|
|
115
|
+
@app.route('/api-keys/create', methods=['POST'])
|
|
116
|
+
@login_required
|
|
117
|
+
def create_api_key():
|
|
118
|
+
result = api('/api/api-keys', 'POST', {'name': request.form['name']})
|
|
119
|
+
if 'plaintext' in result:
|
|
120
|
+
flash(f"Key created: {result['plaintext']} — SAVE THIS NOW!")
|
|
121
|
+
else:
|
|
122
|
+
flash(result.get('error', 'Failed'))
|
|
123
|
+
return redirect(url_for('api_keys'))
|
|
124
|
+
|
|
125
|
+
@app.route('/api-keys/<id>/revoke', methods=['POST'])
|
|
126
|
+
@login_required
|
|
127
|
+
def revoke_api_key(id):
|
|
128
|
+
api(f'/api/api-keys/{id}', 'DELETE')
|
|
129
|
+
flash('Key revoked')
|
|
130
|
+
return redirect(url_for('api_keys'))
|
|
131
|
+
|
|
132
|
+
@app.route('/audit')
|
|
133
|
+
@login_required
|
|
134
|
+
def audit():
|
|
135
|
+
page = max(0, int(request.args.get('p', 0)))
|
|
136
|
+
data = api(f'/api/audit?limit=25&offset={page * 25}')
|
|
137
|
+
return render_page('audit', events=data.get('events', []), total=data.get('total', 0), page=page)
|
|
138
|
+
|
|
139
|
+
@app.route('/settings', methods=['GET', 'POST'])
|
|
140
|
+
@login_required
|
|
141
|
+
def settings():
|
|
142
|
+
if request.method == 'POST':
|
|
143
|
+
result = api('/api/settings', 'PATCH', {
|
|
144
|
+
'name': request.form.get('name', ''),
|
|
145
|
+
'domain': request.form.get('domain', ''),
|
|
146
|
+
'primaryColor': request.form.get('primaryColor', '#6366f1'),
|
|
147
|
+
})
|
|
148
|
+
flash('Settings saved!' if 'error' not in result else result['error'])
|
|
149
|
+
settings_data = api('/api/settings')
|
|
150
|
+
retention = api('/api/retention')
|
|
151
|
+
return render_page('settings', settings=settings_data, retention=retention)
|
|
152
|
+
|
|
153
|
+
# ─── Template Rendering ──────────────────────────────────
|
|
154
|
+
|
|
155
|
+
def render_page(page, **kwargs):
|
|
156
|
+
return render_template_string(
|
|
157
|
+
APP_TEMPLATE,
|
|
158
|
+
page=page,
|
|
159
|
+
user=session.get('user', {}),
|
|
160
|
+
flashes=request.args.get('_flash'),
|
|
161
|
+
**kwargs,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# ─── Templates ───────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
LOGIN_TEMPLATE = '''<!DOCTYPE html>
|
|
167
|
+
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
168
|
+
<title>AgenticMail Enterprise</title>
|
|
169
|
+
<style>*{box-sizing:border-box;margin:0;padding:0}:root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e4e4ef;--dim:#8888a0;--muted:#55556a;--primary:#6366f1;--danger:#ef4444}body{font-family:-apple-system,sans-serif;background:var(--bg);color:var(--text);display:flex;align-items:center;justify-content:center;min-height:100vh}.box{width:380px;max-width:90vw}h1{text-align:center;font-size:22px;margin-bottom:4px}h1 em{font-style:normal;color:var(--primary)}.sub{text-align:center;color:var(--dim);font-size:13px;margin-bottom:32px}.err{background:rgba(239,68,68,0.1);border:1px solid var(--danger);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:var(--danger)}.fg{margin-bottom:14px}.fl{display:block;font-size:12px;color:var(--dim);margin-bottom:4px}.input{width:100%;padding:10px 14px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;outline:none}.input:focus{border-color:var(--primary)}.btn{width:100%;padding:10px;background:var(--primary);border:none;border-radius:8px;color:#fff;font-size:14px;font-weight:600;cursor:pointer}.btn:hover{background:#818cf8}.info{text-align:center;margin-top:16px;font-size:11px;color:var(--muted)}</style></head>
|
|
170
|
+
<body><div class="box"><h1>🏢 <em>AgenticMail</em> Enterprise</h1><p class="sub">Sign in · Python Dashboard</p>
|
|
171
|
+
{% if error %}<div class="err">{{ error }}</div>{% endif %}
|
|
172
|
+
<form method="POST"><div class="fg"><label class="fl">Email</label><input class="input" type="email" name="email" required autofocus></div>
|
|
173
|
+
<div class="fg"><label class="fl">Password</label><input class="input" type="password" name="password" required></div>
|
|
174
|
+
<button class="btn" type="submit">Sign In</button></form>
|
|
175
|
+
<p class="info">Connected to: {{ api_url }}</p></div></body></html>'''
|
|
176
|
+
|
|
177
|
+
APP_TEMPLATE = '''<!DOCTYPE html>
|
|
178
|
+
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
179
|
+
<title>AgenticMail Enterprise — Python</title>
|
|
180
|
+
<style>*{box-sizing:border-box;margin:0;padding:0}:root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e4e4ef;--dim:#8888a0;--muted:#55556a;--primary:#6366f1;--success:#22c55e;--danger:#ef4444;--warning:#f59e0b}body{font-family:-apple-system,sans-serif;background:var(--bg);color:var(--text)}.layout{display:flex;min-height:100vh}.sidebar{width:240px;background:var(--surface);border-right:1px solid var(--border);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column}.sidebar-h{padding:20px;border-bottom:1px solid var(--border)}.sidebar-h h2{font-size:16px}.sidebar-h h2 em{font-style:normal;color:var(--primary)}.sidebar-h small{font-size:11px;color:var(--muted);display:block;margin-top:2px}.nav{flex:1;padding:8px 0}.ns{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);padding:12px 20px 4px}.nav a{display:flex;align-items:center;gap:10px;padding:10px 20px;color:var(--dim);text-decoration:none;font-size:13px}.nav a:hover{color:var(--text);background:rgba(255,255,255,0.03)}.nav a.active{color:var(--primary);background:rgba(99,102,241,0.12);border-right:2px solid var(--primary)}.sf{padding:16px 20px;border-top:1px solid var(--border);font-size:12px}.content{flex:1;margin-left:240px;padding:32px;max-width:1100px}h2.t{font-size:22px;font-weight:700;margin-bottom:4px}.desc{font-size:13px;color:var(--dim);margin-bottom:24px}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:24px}.stat{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px}.stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em}.stat .v{font-size:30px;font-weight:700;margin-top:4px}.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}.ct{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:12px}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;padding:10px 12px;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid var(--border)}td{padding:12px;border-bottom:1px solid var(--border)}tr:hover td{background:rgba(255,255,255,0.015)}.btn{display:inline-flex;align-items:center;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--text);text-decoration:none}.btn:hover{background:rgba(255,255,255,0.05)}.btn-p{background:var(--primary);border-color:var(--primary);color:#fff}.btn-p:hover{background:#818cf8}.btn-d{color:var(--danger);border-color:var(--danger)}.btn-sm{padding:4px 10px;font-size:12px}.input{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;outline:none}.input:focus{border-color:var(--primary)}.fg{margin-bottom:14px}.fl{display:block;font-size:12px;color:var(--dim);margin-bottom:4px;font-weight:500}.badge{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600}.b-active{background:rgba(34,197,94,0.12);color:var(--success)}.b-archived{background:rgba(136,136,160,0.1);color:var(--dim)}.b-owner{background:rgba(245,158,11,0.12);color:var(--warning)}.b-admin{background:rgba(99,102,241,0.12);color:var(--primary)}.b-member{background:rgba(136,136,160,0.08);color:var(--dim)}.empty{text-align:center;padding:48px 20px;color:var(--muted)}.empty-i{font-size:36px;margin-bottom:10px}.flash{padding:12px 16px;border-radius:8px;margin-bottom:16px;font-size:13px;background:rgba(34,197,94,0.1);border:1px solid var(--success);color:var(--success)}</style></head>
|
|
181
|
+
<body><div class="layout">
|
|
182
|
+
<div class="sidebar"><div class="sidebar-h"><h2>🏢 <em>Agentic</em>Mail</h2><small>Enterprise · Python</small></div>
|
|
183
|
+
<div class="nav"><div class="ns">Overview</div>
|
|
184
|
+
<a href="/" class="{{ 'active' if page=='dashboard' }}">📊 Dashboard</a>
|
|
185
|
+
<div class="ns">Manage</div>
|
|
186
|
+
<a href="/agents" class="{{ 'active' if page=='agents' }}">🤖 Agents</a>
|
|
187
|
+
<a href="/users" class="{{ 'active' if page=='users' }}">👥 Users</a>
|
|
188
|
+
<a href="/api-keys" class="{{ 'active' if page=='api_keys' }}">🔑 API Keys</a>
|
|
189
|
+
<div class="ns">System</div>
|
|
190
|
+
<a href="/audit" class="{{ 'active' if page=='audit' }}">📋 Audit Log</a>
|
|
191
|
+
<a href="/settings" class="{{ 'active' if page=='settings' }}">⚙️ Settings</a></div>
|
|
192
|
+
<div class="sf"><div style="color:var(--dim)">{{ user.name }}</div><div style="color:var(--muted);font-size:11px">{{ user.email }}</div><a href="/logout" style="color:var(--muted);font-size:11px;margin-top:6px;display:inline-block">Sign out</a></div></div>
|
|
193
|
+
<div class="content">
|
|
194
|
+
{% with messages = get_flashed_messages() %}{% if messages %}{% for m in messages %}<div class="flash">{{ m }}</div>{% endfor %}{% endif %}{% endwith %}
|
|
195
|
+
|
|
196
|
+
{% if page == 'dashboard' %}
|
|
197
|
+
<h2 class="t">Dashboard</h2><p class="desc">Overview of your AgenticMail instance</p>
|
|
198
|
+
<div class="stats">
|
|
199
|
+
<div class="stat"><div class="l">Total Agents</div><div class="v" style="color:var(--primary)">{{ stats.totalAgents|default(0) }}</div></div>
|
|
200
|
+
<div class="stat"><div class="l">Active Agents</div><div class="v" style="color:var(--success)">{{ stats.activeAgents|default(0) }}</div></div>
|
|
201
|
+
<div class="stat"><div class="l">Users</div><div class="v">{{ stats.totalUsers|default(0) }}</div></div>
|
|
202
|
+
<div class="stat"><div class="l">Audit Events</div><div class="v">{{ stats.totalAuditEvents|default(0) }}</div></div></div>
|
|
203
|
+
<div class="card"><div class="ct">Recent Activity</div>
|
|
204
|
+
{% if audit %}{% for e in audit %}<div style="padding:10px 0;border-bottom:1px solid var(--border);font-size:13px"><span style="color:var(--primary);font-weight:500">{{ e.action }}</span> on {{ e.resource }}<div style="font-size:11px;color:var(--muted)">{{ e.timestamp }}{{ ' · ' + e.ip if e.ip else '' }}</div></div>{% endfor %}
|
|
205
|
+
{% else %}<div class="empty"><div class="empty-i">📋</div>No activity yet</div>{% endif %}</div>
|
|
206
|
+
|
|
207
|
+
{% elif page == 'agents' %}
|
|
208
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px"><div><h2 class="t">Agents</h2><p class="desc" style="margin:0">Manage AI agent identities</p></div></div>
|
|
209
|
+
<div class="card" style="margin-bottom:16px"><div class="ct">Create Agent</div>
|
|
210
|
+
<form method="POST" action="/agents/create" style="display:flex;gap:10px;align-items:end">
|
|
211
|
+
<div class="fg" style="flex:1;margin:0"><label class="fl">Name</label><input class="input" name="name" required placeholder="e.g. researcher"></div>
|
|
212
|
+
<div class="fg" style="margin:0"><label class="fl">Role</label><select class="input" name="role"><option>assistant</option><option>researcher</option><option>writer</option><option>secretary</option></select></div>
|
|
213
|
+
<button class="btn btn-p" type="submit">Create</button></form></div>
|
|
214
|
+
<div class="card">{% if agents %}<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th></th></tr></thead><tbody>
|
|
215
|
+
{% for a in agents %}<tr><td style="font-weight:600">{{ a.name }}</td><td style="color:var(--dim)">{{ a.email }}</td><td>{{ a.role }}</td><td><span class="badge b-{{ a.status }}">{{ a.status }}</span></td><td>{% if a.status == 'active' %}<form method="POST" action="/agents/{{ a.id }}/archive" style="display:inline"><button class="btn btn-sm btn-d" type="submit">Archive</button></form>{% endif %}</td></tr>{% endfor %}
|
|
216
|
+
</tbody></table>{% else %}<div class="empty"><div class="empty-i">🤖</div>No agents yet</div>{% endif %}</div>
|
|
217
|
+
|
|
218
|
+
{% elif page == 'users' %}
|
|
219
|
+
<h2 class="t">Users</h2><p class="desc">Manage team members</p>
|
|
220
|
+
<div class="card" style="margin-bottom:16px"><div class="ct">Create User</div>
|
|
221
|
+
<form method="POST" action="/users/create" style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
222
|
+
<div class="fg"><label class="fl">Name</label><input class="input" name="name" required></div>
|
|
223
|
+
<div class="fg"><label class="fl">Email</label><input class="input" type="email" name="email" required></div>
|
|
224
|
+
<div class="fg"><label class="fl">Role</label><select class="input" name="role"><option>member</option><option>admin</option><option>owner</option></select></div>
|
|
225
|
+
<div class="fg"><label class="fl">Password</label><input class="input" type="password" name="password" required minlength="8"></div>
|
|
226
|
+
<div><button class="btn btn-p" type="submit">Create</button></div></form></div>
|
|
227
|
+
<div class="card">{% if users %}<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Last Login</th></tr></thead><tbody>
|
|
228
|
+
{% for u in users %}<tr><td style="font-weight:600">{{ u.name }}</td><td style="color:var(--dim)">{{ u.email }}</td><td><span class="badge b-{{ u.role }}">{{ u.role }}</span></td><td style="color:var(--muted);font-size:12px">{{ u.lastLoginAt or 'Never' }}</td></tr>{% endfor %}
|
|
229
|
+
</tbody></table>{% else %}<div class="empty"><div class="empty-i">👥</div>No users yet</div>{% endif %}</div>
|
|
230
|
+
|
|
231
|
+
{% elif page == 'api_keys' %}
|
|
232
|
+
<h2 class="t">API Keys</h2><p class="desc">Manage programmatic access</p>
|
|
233
|
+
<div class="card" style="margin-bottom:16px"><div class="ct">Create Key</div>
|
|
234
|
+
<form method="POST" action="/api-keys/create" style="display:flex;gap:10px;align-items:end">
|
|
235
|
+
<div class="fg" style="flex:1;margin:0"><label class="fl">Key Name</label><input class="input" name="name" required placeholder="e.g. CI/CD pipeline"></div>
|
|
236
|
+
<button class="btn btn-p" type="submit">Create</button></form></div>
|
|
237
|
+
<div class="card">{% if keys %}<table><thead><tr><th>Name</th><th>Key</th><th>Last Used</th><th>Status</th><th></th></tr></thead><tbody>
|
|
238
|
+
{% for k in keys %}<tr><td style="font-weight:600">{{ k.name }}</td><td><code style="font-size:12px">{{ k.keyPrefix }}...</code></td><td style="color:var(--muted);font-size:12px">{{ k.lastUsedAt or 'Never' }}</td><td><span class="badge {{ 'b-archived' if k.revoked else 'b-active' }}">{{ 'revoked' if k.revoked else 'active' }}</span></td><td>{% if not k.revoked %}<form method="POST" action="/api-keys/{{ k.id }}/revoke" style="display:inline"><button class="btn btn-sm btn-d" type="submit">Revoke</button></form>{% endif %}</td></tr>{% endfor %}
|
|
239
|
+
</tbody></table>{% else %}<div class="empty"><div class="empty-i">🔑</div>No API keys</div>{% endif %}</div>
|
|
240
|
+
|
|
241
|
+
{% elif page == 'audit' %}
|
|
242
|
+
<h2 class="t">Audit Log</h2><p class="desc">{{ total }} total events</p>
|
|
243
|
+
<div class="card">{% if events %}<table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Resource</th><th>IP</th></tr></thead><tbody>
|
|
244
|
+
{% for e in events %}<tr><td style="font-size:12px;color:var(--muted)">{{ e.timestamp }}</td><td>{{ e.actor }}</td><td style="color:var(--primary);font-weight:500">{{ e.action }}</td><td style="font-size:12px">{{ e.resource }}</td><td style="font-size:12px;color:var(--muted)">{{ e.ip or '-' }}</td></tr>{% endfor %}
|
|
245
|
+
</tbody></table>
|
|
246
|
+
<div style="display:flex;gap:8px;justify-content:center;margin-top:16px">
|
|
247
|
+
{% if page_num > 0 %}<a class="btn btn-sm" href="/audit?p={{ page_num - 1 }}">← Prev</a>{% endif %}
|
|
248
|
+
<span style="padding:6px 12px;font-size:12px;color:var(--muted)">Page {{ page_num + 1 }}</span>
|
|
249
|
+
{% if (page_num + 1) * 25 < total %}<a class="btn btn-sm" href="/audit?p={{ page_num + 1 }}">Next →</a>{% endif %}
|
|
250
|
+
</div>{% else %}<div class="empty"><div class="empty-i">📋</div>No audit events yet</div>{% endif %}</div>
|
|
251
|
+
|
|
252
|
+
{% elif page == 'settings' %}
|
|
253
|
+
<h2 class="t">Settings</h2><p class="desc">Configure your organization</p>
|
|
254
|
+
<div class="card"><div class="ct">General</div>
|
|
255
|
+
<form method="POST" style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
|
256
|
+
<div class="fg"><label class="fl">Organization Name</label><input class="input" name="name" value="{{ settings.name|default('') }}"></div>
|
|
257
|
+
<div class="fg"><label class="fl">Domain</label><input class="input" name="domain" value="{{ settings.domain|default('') }}" placeholder="agents.acme.com"></div>
|
|
258
|
+
<div class="fg"><label class="fl">Primary Color</label><input class="input" type="color" name="primaryColor" value="{{ settings.primaryColor|default('#6366f1') }}" style="height:38px;padding:4px"></div>
|
|
259
|
+
<div></div><div><button class="btn btn-p" type="submit">Save Settings</button></div></form></div>
|
|
260
|
+
<div class="card"><div class="ct">Plan</div><span class="badge b-active" style="font-size:14px;padding:4px 12px">{{ (settings.plan or 'free')|upper }}</span>
|
|
261
|
+
<span style="font-size:13px;color:var(--dim);margin-left:12px">Subdomain: {{ settings.subdomain|default('not set') }}.agenticmail.cloud</span></div>
|
|
262
|
+
{% if retention %}<div class="card"><div class="ct">Data Retention</div><div style="font-size:13px">
|
|
263
|
+
Status: <span style="color:{{ 'var(--success)' if retention.enabled else 'var(--muted)' }}">{{ 'Enabled' if retention.enabled else 'Disabled' }}</span><br>
|
|
264
|
+
<span style="color:var(--dim)">Retain emails for {{ retention.retainDays|default(365) }} days</span></div></div>{% endif %}
|
|
265
|
+
{% endif %}
|
|
266
|
+
|
|
267
|
+
</div></div></body></html>'''
|
|
268
|
+
|
|
269
|
+
if __name__ == '__main__':
|
|
270
|
+
print(f'\n🏢 AgenticMail Enterprise Dashboard (Python/Flask)')
|
|
271
|
+
print(f' API: {API_URL}')
|
|
272
|
+
print(f' Dashboard: http://localhost:5000\n')
|
|
273
|
+
app.run(debug=True, port=5000)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# AgenticMail Enterprise Dashboard — Ruby/Sinatra Edition
|
|
2
|
+
#
|
|
3
|
+
# Setup:
|
|
4
|
+
# gem install sinatra json
|
|
5
|
+
# ruby app.rb
|
|
6
|
+
#
|
|
7
|
+
# Or: AGENTICMAIL_URL=https://your-company.agenticmail.cloud ruby app.rb
|
|
8
|
+
|
|
9
|
+
require 'sinatra'
|
|
10
|
+
require 'json'
|
|
11
|
+
require 'net/http'
|
|
12
|
+
require 'uri'
|
|
13
|
+
require 'securerandom'
|
|
14
|
+
|
|
15
|
+
enable :sessions
|
|
16
|
+
set :session_secret, SecureRandom.hex(32)
|
|
17
|
+
set :port, 4567
|
|
18
|
+
|
|
19
|
+
API_URL = ENV['AGENTICMAIL_URL'] || 'http://localhost:3000'
|
|
20
|
+
|
|
21
|
+
# ─── API Client ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
def api(path, method: :get, body: nil)
|
|
24
|
+
uri = URI("#{API_URL}#{path}")
|
|
25
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
26
|
+
http.use_ssl = uri.scheme == 'https'
|
|
27
|
+
http.open_timeout = 5
|
|
28
|
+
http.read_timeout = 10
|
|
29
|
+
|
|
30
|
+
req = case method
|
|
31
|
+
when :get then Net::HTTP::Get.new(uri)
|
|
32
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
33
|
+
when :patch then Net::HTTP::Patch.new(uri)
|
|
34
|
+
when :delete then Net::HTTP::Delete.new(uri)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
req['Content-Type'] = 'application/json'
|
|
38
|
+
req['Authorization'] = "Bearer #{session[:token]}" if session[:token]
|
|
39
|
+
req.body = body.to_json if body
|
|
40
|
+
|
|
41
|
+
resp = http.request(req)
|
|
42
|
+
JSON.parse(resp.body) rescue { 'error' => 'Invalid response' }
|
|
43
|
+
rescue => e
|
|
44
|
+
{ 'error' => e.message }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def badge(status)
|
|
48
|
+
colors = { 'active' => '#22c55e', 'archived' => '#888', 'suspended' => '#ef4444',
|
|
49
|
+
'owner' => '#f59e0b', 'admin' => '#6366f1', 'member' => '#888', 'viewer' => '#555' }
|
|
50
|
+
c = colors[status.to_s] || '#888'
|
|
51
|
+
"<span style='display:inline-block;padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;background:#{c}20;color:#{c}'>#{status}</span>"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# ─── Shared Layout ───────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def layout(page, &block)
|
|
57
|
+
user = session[:user] || {}
|
|
58
|
+
content = yield
|
|
59
|
+
erb_str = <<~HTML
|
|
60
|
+
<!DOCTYPE html>
|
|
61
|
+
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
62
|
+
<title>AgenticMail Enterprise — Ruby</title>
|
|
63
|
+
<style>*{box-sizing:border-box;margin:0;padding:0}:root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e4e4ef;--dim:#8888a0;--muted:#55556a;--primary:#6366f1;--success:#22c55e;--danger:#ef4444;--warning:#f59e0b}body{font-family:-apple-system,sans-serif;background:var(--bg);color:var(--text)}.layout{display:flex;min-height:100vh}.sidebar{width:240px;background:var(--surface);border-right:1px solid var(--border);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column}.sh{padding:20px;border-bottom:1px solid var(--border)}.sh h2{font-size:16px}.sh h2 em{font-style:normal;color:var(--primary)}.sh small{font-size:11px;color:var(--muted);display:block;margin-top:2px}.nav{flex:1;padding:8px 0}.ns{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);padding:12px 20px 4px}.nav a{display:flex;align-items:center;gap:10px;padding:10px 20px;color:var(--dim);text-decoration:none;font-size:13px}.nav a:hover{color:var(--text);background:rgba(255,255,255,0.03)}.nav a.on{color:var(--primary);background:rgba(99,102,241,0.12);border-right:2px solid var(--primary)}.sf{padding:16px 20px;border-top:1px solid var(--border);font-size:12px}.content{flex:1;margin-left:240px;padding:32px;max-width:1100px}h2.t{font-size:22px;font-weight:700;margin-bottom:4px}.desc{font-size:13px;color:var(--dim);margin-bottom:24px}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:24px}.stat{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px}.stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em}.stat .v{font-size:30px;font-weight:700;margin-top:4px}.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}.ct{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:12px}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;padding:10px 12px;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid var(--border)}td{padding:12px;border-bottom:1px solid var(--border)}tr:hover td{background:rgba(255,255,255,0.015)}.btn{display:inline-flex;align-items:center;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--text);text-decoration:none}.btn:hover{background:rgba(255,255,255,0.05)}.btn-p{background:var(--primary);border-color:var(--primary);color:#fff}.btn-d{color:var(--danger);border-color:var(--danger)}.btn-sm{padding:4px 10px;font-size:12px}.input{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px}.fg{margin-bottom:14px}.fl{display:block;font-size:12px;color:var(--dim);margin-bottom:4px}.empty{text-align:center;padding:48px 20px;color:var(--muted)}.flash{padding:12px 16px;border-radius:8px;margin-bottom:16px;font-size:13px;background:rgba(34,197,94,0.1);border:1px solid var(--success);color:var(--success)}</style></head>
|
|
64
|
+
<body><div class="layout">
|
|
65
|
+
<div class="sidebar"><div class="sh"><h2>🏢 <em>Agentic</em>Mail</h2><small>Enterprise · Ruby</small></div>
|
|
66
|
+
<div class="nav"><div class="ns">Overview</div><a href="/" class="#{page == 'dashboard' ? 'on' : ''}">📊 Dashboard</a>
|
|
67
|
+
<div class="ns">Manage</div><a href="/agents" class="#{page == 'agents' ? 'on' : ''}">🤖 Agents</a>
|
|
68
|
+
<a href="/users" class="#{page == 'users' ? 'on' : ''}">👥 Users</a><a href="/api-keys" class="#{page == 'keys' ? 'on' : ''}">🔑 API Keys</a>
|
|
69
|
+
<div class="ns">System</div><a href="/audit" class="#{page == 'audit' ? 'on' : ''}">📋 Audit Log</a>
|
|
70
|
+
<a href="/settings" class="#{page == 'settings' ? 'on' : ''}">⚙️ Settings</a></div>
|
|
71
|
+
<div class="sf"><div style="color:var(--dim)">#{Rack::Utils.escape_html(user['name'].to_s)}</div><div style="color:var(--muted);font-size:11px">#{Rack::Utils.escape_html(user['email'].to_s)}</div><a href="/logout" style="color:var(--muted);font-size:11px;margin-top:6px;display:inline-block">Sign out</a></div></div>
|
|
72
|
+
<div class="content">#{content}</div></div></body></html>
|
|
73
|
+
HTML
|
|
74
|
+
erb_str
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# ─── Auth ────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
before do
|
|
80
|
+
pass if ['/login', '/logout'].include?(request.path_info)
|
|
81
|
+
redirect '/login' unless session[:token]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
get '/login' do
|
|
85
|
+
<<~HTML
|
|
86
|
+
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>AgenticMail Enterprise</title>
|
|
87
|
+
<style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,sans-serif;background:#0a0a0f;color:#e4e4ef;display:flex;align-items:center;justify-content:center;min-height:100vh}.box{width:380px}h1{text-align:center;font-size:22px;margin-bottom:4px}h1 em{font-style:normal;color:#6366f1}.sub{text-align:center;color:#8888a0;font-size:13px;margin-bottom:32px}.fg{margin-bottom:14px}.fl{display:block;font-size:12px;color:#8888a0;margin-bottom:4px}.input{width:100%;padding:10px 14px;background:#12121a;border:1px solid #1e1e2e;border-radius:8px;color:#e4e4ef;font-size:14px;outline:none}.input:focus{border-color:#6366f1}.btn{width:100%;padding:10px;background:#6366f1;border:none;border-radius:8px;color:#fff;font-size:14px;font-weight:600;cursor:pointer}</style></head>
|
|
88
|
+
<body><div class="box"><h1>🏢 <em>AgenticMail</em> Enterprise</h1><p class="sub">Sign in · Ruby Dashboard</p>
|
|
89
|
+
<form method="POST" action="/login"><div class="fg"><label class="fl">Email</label><input class="input" type="email" name="email" required></div>
|
|
90
|
+
<div class="fg"><label class="fl">Password</label><input class="input" type="password" name="password" required></div>
|
|
91
|
+
<button class="btn" type="submit">Sign In</button></form></div></body></html>
|
|
92
|
+
HTML
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
post '/login' do
|
|
96
|
+
data = api('/auth/login', method: :post, body: { email: params[:email], password: params[:password] })
|
|
97
|
+
if data['token']
|
|
98
|
+
session[:token] = data['token']
|
|
99
|
+
session[:user] = data['user']
|
|
100
|
+
redirect '/'
|
|
101
|
+
else
|
|
102
|
+
"Login failed: #{data['error']}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
get '/logout' do
|
|
107
|
+
session.clear
|
|
108
|
+
redirect '/login'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# ─── Pages ───────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
get '/' do
|
|
114
|
+
stats = api('/api/stats')
|
|
115
|
+
audit = api('/api/audit?limit=8')
|
|
116
|
+
events = (audit['events'] || []).map { |e|
|
|
117
|
+
"<div style='padding:10px 0;border-bottom:1px solid var(--border);font-size:13px'><span style='color:var(--primary);font-weight:500'>#{Rack::Utils.escape_html(e['action'])}</span> on #{Rack::Utils.escape_html(e['resource'])}<div style='font-size:11px;color:var(--muted)'>#{e['timestamp']}</div></div>"
|
|
118
|
+
}.join
|
|
119
|
+
|
|
120
|
+
layout('dashboard') {
|
|
121
|
+
"<h2 class='t'>Dashboard</h2><p class='desc'>Overview</p>" +
|
|
122
|
+
"<div class='stats'><div class='stat'><div class='l'>Total Agents</div><div class='v' style='color:var(--primary)'>#{stats['totalAgents']}</div></div>" +
|
|
123
|
+
"<div class='stat'><div class='l'>Active Agents</div><div class='v' style='color:var(--success)'>#{stats['activeAgents']}</div></div>" +
|
|
124
|
+
"<div class='stat'><div class='l'>Users</div><div class='v'>#{stats['totalUsers']}</div></div>" +
|
|
125
|
+
"<div class='stat'><div class='l'>Audit Events</div><div class='v'>#{stats['totalAuditEvents']}</div></div></div>" +
|
|
126
|
+
"<div class='card'><div class='ct'>Recent Activity</div>#{events.empty? ? "<div class='empty'>No activity yet</div>" : events}</div>"
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
get '/agents' do
|
|
131
|
+
data = api('/api/agents')
|
|
132
|
+
agents = data['agents'] || []
|
|
133
|
+
rows = agents.map { |a|
|
|
134
|
+
"<tr><td style='font-weight:600'>#{Rack::Utils.escape_html(a['name'])}</td><td style='color:var(--dim)'>#{Rack::Utils.escape_html(a['email'])}</td><td>#{a['role']}</td><td>#{badge(a['status'])}</td><td>#{a['status'] == 'active' ? "<a class='btn btn-sm btn-d' href='/agents/#{a['id']}/archive'>Archive</a>" : ''}</td></tr>"
|
|
135
|
+
}.join
|
|
136
|
+
layout('agents') {
|
|
137
|
+
"<h2 class='t'>Agents</h2><p class='desc'>Manage AI agent identities</p>" +
|
|
138
|
+
"<div class='card'>#{agents.empty? ? "<div class='empty'>🤖 No agents yet</div>" : "<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th></th></tr></thead><tbody>#{rows}</tbody></table>"}</div>"
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
get '/agents/:id/archive' do
|
|
143
|
+
api("/api/agents/#{params[:id]}/archive", method: :post)
|
|
144
|
+
redirect '/agents'
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
get '/users' do
|
|
148
|
+
data = api('/api/users')
|
|
149
|
+
users = data['users'] || []
|
|
150
|
+
rows = users.map { |u|
|
|
151
|
+
"<tr><td style='font-weight:600'>#{Rack::Utils.escape_html(u['name'])}</td><td style='color:var(--dim)'>#{Rack::Utils.escape_html(u['email'])}</td><td>#{badge(u['role'])}</td><td style='color:var(--muted);font-size:12px'>#{u['lastLoginAt'] || 'Never'}</td></tr>"
|
|
152
|
+
}.join
|
|
153
|
+
layout('users') {
|
|
154
|
+
"<h2 class='t'>Users</h2><p class='desc'>Manage team members</p>" +
|
|
155
|
+
"<div class='card'>#{users.empty? ? "<div class='empty'>👥 No users yet</div>" : "<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Last Login</th></tr></thead><tbody>#{rows}</tbody></table>"}</div>"
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
get '/api-keys' do
|
|
160
|
+
data = api('/api/api-keys')
|
|
161
|
+
keys = data['keys'] || []
|
|
162
|
+
rows = keys.map { |k|
|
|
163
|
+
"<tr><td style='font-weight:600'>#{Rack::Utils.escape_html(k['name'])}</td><td><code style='font-size:12px'>#{k['keyPrefix']}...</code></td><td style='color:var(--muted);font-size:12px'>#{k['lastUsedAt'] || 'Never'}</td><td>#{badge(k['revoked'] ? 'archived' : 'active')}</td></tr>"
|
|
164
|
+
}.join
|
|
165
|
+
layout('keys') {
|
|
166
|
+
"<h2 class='t'>API Keys</h2><p class='desc'>Manage programmatic access</p>" +
|
|
167
|
+
"<div class='card'>#{keys.empty? ? "<div class='empty'>🔑 No API keys</div>" : "<table><thead><tr><th>Name</th><th>Key</th><th>Last Used</th><th>Status</th></tr></thead><tbody>#{rows}</tbody></table>"}</div>"
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
get '/audit' do
|
|
172
|
+
p = [0, (params[:p] || 0).to_i].max
|
|
173
|
+
data = api("/api/audit?limit=25&offset=#{p * 25}")
|
|
174
|
+
events = data['events'] || []
|
|
175
|
+
total = data['total'] || 0
|
|
176
|
+
rows = events.map { |e|
|
|
177
|
+
"<tr><td style='font-size:12px;color:var(--muted)'>#{e['timestamp']}</td><td>#{Rack::Utils.escape_html(e['actor'])}</td><td style='color:var(--primary);font-weight:500'>#{Rack::Utils.escape_html(e['action'])}</td><td style='font-size:12px'>#{Rack::Utils.escape_html(e['resource'])}</td><td style='font-size:12px;color:var(--muted)'>#{e['ip'] || '-'}</td></tr>"
|
|
178
|
+
}.join
|
|
179
|
+
layout('audit') {
|
|
180
|
+
"<h2 class='t'>Audit Log</h2><p class='desc'>#{total} total events</p>" +
|
|
181
|
+
"<div class='card'>#{events.empty? ? "<div class='empty'>📋 No audit events</div>" : "<table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Resource</th><th>IP</th></tr></thead><tbody>#{rows}</tbody></table>"}</div>"
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
get '/settings' do
|
|
186
|
+
s = api('/api/settings')
|
|
187
|
+
layout('settings') {
|
|
188
|
+
"<h2 class='t'>Settings</h2><p class='desc'>Configure your organization</p>" +
|
|
189
|
+
"<div class='card'><div class='ct'>General</div><div style='font-size:13px'>Name: #{Rack::Utils.escape_html(s['name'].to_s)}<br>Domain: #{Rack::Utils.escape_html(s['domain'].to_s)}<br>Plan: #{badge((s['plan'] || 'free').upcase)}</div></div>"
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
puts "\n🏢 AgenticMail Enterprise Dashboard (Ruby/Sinatra)"
|
|
194
|
+
puts " API: #{API_URL}"
|
|
195
|
+
puts " Dashboard: http://localhost:4567\n"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/db/sql-schema.ts
|
|
2
|
+
var TABLES = {
|
|
3
|
+
company: `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS company_settings (
|
|
5
|
+
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
6
|
+
name TEXT NOT NULL,
|
|
7
|
+
domain TEXT,
|
|
8
|
+
subdomain TEXT NOT NULL UNIQUE,
|
|
9
|
+
smtp_host TEXT,
|
|
10
|
+
smtp_port INTEGER,
|
|
11
|
+
smtp_user TEXT,
|
|
12
|
+
smtp_pass TEXT,
|
|
13
|
+
dkim_private_key TEXT,
|
|
14
|
+
logo_url TEXT,
|
|
15
|
+
primary_color TEXT DEFAULT '#6366f1',
|
|
16
|
+
plan TEXT NOT NULL DEFAULT 'free',
|
|
17
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
18
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
19
|
+
)`,
|
|
20
|
+
agents: `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
name TEXT NOT NULL UNIQUE,
|
|
24
|
+
email TEXT NOT NULL UNIQUE,
|
|
25
|
+
role TEXT NOT NULL DEFAULT 'assistant',
|
|
26
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
27
|
+
metadata TEXT DEFAULT '{}',
|
|
28
|
+
created_by TEXT NOT NULL,
|
|
29
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
31
|
+
)`,
|
|
32
|
+
users: `
|
|
33
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
email TEXT NOT NULL UNIQUE,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
38
|
+
password_hash TEXT,
|
|
39
|
+
sso_provider TEXT,
|
|
40
|
+
sso_subject TEXT,
|
|
41
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
43
|
+
last_login_at TIMESTAMP
|
|
44
|
+
)`,
|
|
45
|
+
audit_log: `
|
|
46
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
49
|
+
actor TEXT NOT NULL,
|
|
50
|
+
actor_type TEXT NOT NULL DEFAULT 'user',
|
|
51
|
+
action TEXT NOT NULL,
|
|
52
|
+
resource TEXT NOT NULL,
|
|
53
|
+
details TEXT DEFAULT '{}',
|
|
54
|
+
ip TEXT
|
|
55
|
+
)`,
|
|
56
|
+
api_keys: `
|
|
57
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
name TEXT NOT NULL,
|
|
60
|
+
key_hash TEXT NOT NULL,
|
|
61
|
+
key_prefix TEXT NOT NULL,
|
|
62
|
+
scopes TEXT NOT NULL DEFAULT '[]',
|
|
63
|
+
created_by TEXT NOT NULL,
|
|
64
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
65
|
+
last_used_at TIMESTAMP,
|
|
66
|
+
expires_at TIMESTAMP,
|
|
67
|
+
revoked INTEGER NOT NULL DEFAULT 0
|
|
68
|
+
)`,
|
|
69
|
+
email_rules: `
|
|
70
|
+
CREATE TABLE IF NOT EXISTS email_rules (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
name TEXT NOT NULL,
|
|
73
|
+
agent_id TEXT,
|
|
74
|
+
conditions TEXT NOT NULL DEFAULT '{}',
|
|
75
|
+
actions TEXT NOT NULL DEFAULT '{}',
|
|
76
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
78
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
79
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
80
|
+
)`,
|
|
81
|
+
retention_policy: `
|
|
82
|
+
CREATE TABLE IF NOT EXISTS retention_policy (
|
|
83
|
+
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
84
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
retain_days INTEGER NOT NULL DEFAULT 365,
|
|
86
|
+
exclude_tags TEXT DEFAULT '[]',
|
|
87
|
+
archive_first INTEGER NOT NULL DEFAULT 1
|
|
88
|
+
)`,
|
|
89
|
+
// Indexes
|
|
90
|
+
indexes: [
|
|
91
|
+
"CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)",
|
|
92
|
+
"CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)",
|
|
93
|
+
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
|
|
94
|
+
"CREATE INDEX IF NOT EXISTS idx_users_sso ON users(sso_provider, sso_subject)",
|
|
95
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp)",
|
|
96
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log(actor)",
|
|
97
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action)",
|
|
98
|
+
"CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)",
|
|
99
|
+
"CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix)",
|
|
100
|
+
"CREATE INDEX IF NOT EXISTS idx_email_rules_agent ON email_rules(agent_id)"
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
function getAllCreateStatements() {
|
|
104
|
+
const stmts = [];
|
|
105
|
+
for (const [key, value] of Object.entries(TABLES)) {
|
|
106
|
+
if (key === "indexes") continue;
|
|
107
|
+
stmts.push(value);
|
|
108
|
+
}
|
|
109
|
+
stmts.push(...TABLES.indexes);
|
|
110
|
+
return stmts;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
getAllCreateStatements
|
|
115
|
+
};
|