@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,513 @@
|
|
|
1
|
+
// AgenticMail Enterprise Dashboard — Go Edition
|
|
2
|
+
//
|
|
3
|
+
// ZERO dependencies beyond the standard library. No frameworks.
|
|
4
|
+
//
|
|
5
|
+
// Setup:
|
|
6
|
+
// go run main.go
|
|
7
|
+
//
|
|
8
|
+
// Or:
|
|
9
|
+
// AGENTICMAIL_URL=https://your-company.agenticmail.cloud go run main.go
|
|
10
|
+
|
|
11
|
+
package main
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"bytes"
|
|
15
|
+
"encoding/json"
|
|
16
|
+
"fmt"
|
|
17
|
+
"html"
|
|
18
|
+
"html/template"
|
|
19
|
+
"io"
|
|
20
|
+
"log"
|
|
21
|
+
"net/http"
|
|
22
|
+
"os"
|
|
23
|
+
"strings"
|
|
24
|
+
"sync"
|
|
25
|
+
"time"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
var apiURL = "http://localhost:3000"
|
|
29
|
+
|
|
30
|
+
// ─── Session Store (in-memory) ──────────────────────────
|
|
31
|
+
|
|
32
|
+
type Session struct {
|
|
33
|
+
Token string
|
|
34
|
+
User map[string]interface{}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var (
|
|
38
|
+
sessions = map[string]*Session{}
|
|
39
|
+
sessMu sync.RWMutex
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
func getSession(r *http.Request) *Session {
|
|
43
|
+
c, err := r.Cookie("am_session")
|
|
44
|
+
if err != nil {
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
sessMu.RLock()
|
|
48
|
+
defer sessMu.RUnlock()
|
|
49
|
+
return sessions[c.Value]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func setSession(w http.ResponseWriter, s *Session) string {
|
|
53
|
+
id := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
54
|
+
sessMu.Lock()
|
|
55
|
+
sessions[id] = s
|
|
56
|
+
sessMu.Unlock()
|
|
57
|
+
http.SetCookie(w, &http.Cookie{Name: "am_session", Value: id, Path: "/", HttpOnly: true, MaxAge: 86400})
|
|
58
|
+
return id
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func clearSession(w http.ResponseWriter, r *http.Request) {
|
|
62
|
+
c, err := r.Cookie("am_session")
|
|
63
|
+
if err == nil {
|
|
64
|
+
sessMu.Lock()
|
|
65
|
+
delete(sessions, c.Value)
|
|
66
|
+
sessMu.Unlock()
|
|
67
|
+
}
|
|
68
|
+
http.SetCookie(w, &http.Cookie{Name: "am_session", Value: "", Path: "/", MaxAge: -1})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── API Client ─────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
func apiCall(path, method, token string, body interface{}) (map[string]interface{}, error) {
|
|
74
|
+
var reqBody io.Reader
|
|
75
|
+
if body != nil {
|
|
76
|
+
b, _ := json.Marshal(body)
|
|
77
|
+
reqBody = bytes.NewReader(b)
|
|
78
|
+
}
|
|
79
|
+
req, err := http.NewRequest(method, apiURL+path, reqBody)
|
|
80
|
+
if err != nil {
|
|
81
|
+
return nil, err
|
|
82
|
+
}
|
|
83
|
+
req.Header.Set("Content-Type", "application/json")
|
|
84
|
+
if token != "" {
|
|
85
|
+
req.Header.Set("Authorization", "Bearer "+token)
|
|
86
|
+
}
|
|
87
|
+
client := &http.Client{Timeout: 10 * time.Second}
|
|
88
|
+
resp, err := client.Do(req)
|
|
89
|
+
if err != nil {
|
|
90
|
+
return nil, err
|
|
91
|
+
}
|
|
92
|
+
defer resp.Body.Close()
|
|
93
|
+
var result map[string]interface{}
|
|
94
|
+
json.NewDecoder(resp.Body).Decode(&result)
|
|
95
|
+
return result, nil
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
func esc(s interface{}) string {
|
|
101
|
+
if s == nil {
|
|
102
|
+
return ""
|
|
103
|
+
}
|
|
104
|
+
return html.EscapeString(fmt.Sprintf("%v", s))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func badge(status string) string {
|
|
108
|
+
colors := map[string]string{
|
|
109
|
+
"active": "#22c55e", "archived": "#888", "suspended": "#ef4444",
|
|
110
|
+
"owner": "#f59e0b", "admin": "#6366f1", "member": "#888", "viewer": "#555",
|
|
111
|
+
}
|
|
112
|
+
c := colors[status]
|
|
113
|
+
if c == "" {
|
|
114
|
+
c = "#888"
|
|
115
|
+
}
|
|
116
|
+
return fmt.Sprintf(`<span style="display:inline-block;padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;background:%s20;color:%s">%s</span>`, c, c, esc(status))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func intVal(m map[string]interface{}, key string) int {
|
|
120
|
+
if v, ok := m[key]; ok {
|
|
121
|
+
switch n := v.(type) {
|
|
122
|
+
case float64:
|
|
123
|
+
return int(n)
|
|
124
|
+
case int:
|
|
125
|
+
return n
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func strVal(m map[string]interface{}, key string) string {
|
|
132
|
+
if v, ok := m[key]; ok && v != nil {
|
|
133
|
+
return fmt.Sprintf("%v", v)
|
|
134
|
+
}
|
|
135
|
+
return ""
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Layout ─────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
func layout(page string, user map[string]interface{}, content string) string {
|
|
141
|
+
navItem := func(href, icon, label, key string) string {
|
|
142
|
+
cls := ""
|
|
143
|
+
if page == key {
|
|
144
|
+
cls = " on"
|
|
145
|
+
}
|
|
146
|
+
return fmt.Sprintf(`<a href="%s" class="%s">%s <span>%s</span></a>`, href, cls, icon, label)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
userName := ""
|
|
150
|
+
userEmail := ""
|
|
151
|
+
if user != nil {
|
|
152
|
+
userName = strVal(user, "name")
|
|
153
|
+
userEmail = strVal(user, "email")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
157
|
+
<title>AgenticMail Enterprise — Go</title>
|
|
158
|
+
<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)}.empty-i{font-size:36px;margin-bottom:10px}select.input{appearance:auto}@media(max-width:768px){.sidebar{width:56px}.sh h2,.sh small,.nav a span,.ns,.sf{display:none}.nav a{justify-content:center;padding:14px 0;font-size:18px}.content{margin-left:56px;padding:16px}}</style></head>
|
|
159
|
+
<body><div class="layout">
|
|
160
|
+
<div class="sidebar"><div class="sh"><h2>🏢 <em>Agentic</em>Mail</h2><small>Enterprise · Go</small></div>
|
|
161
|
+
<div class="nav"><div class="ns">Overview</div>%s
|
|
162
|
+
<div class="ns">Manage</div>%s%s%s
|
|
163
|
+
<div class="ns">System</div>%s%s</div>
|
|
164
|
+
<div class="sf"><div style="color:var(--dim)">%s</div><div style="color:var(--muted);font-size:11px">%s</div><a href="/logout" style="color:var(--muted);font-size:11px;margin-top:6px;display:inline-block">Sign out</a></div></div>
|
|
165
|
+
<div class="content">%s</div></div></body></html>`,
|
|
166
|
+
navItem("/", "📊", "Dashboard", "dashboard"),
|
|
167
|
+
navItem("/agents", "🤖", "Agents", "agents"),
|
|
168
|
+
navItem("/users", "👥", "Users", "users"),
|
|
169
|
+
navItem("/api-keys", "🔑", "API Keys", "keys"),
|
|
170
|
+
navItem("/audit", "📋", "Audit Log", "audit"),
|
|
171
|
+
navItem("/settings", "⚙️", "Settings", "settings"),
|
|
172
|
+
esc(userName), esc(userEmail), content)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func loginPage() string {
|
|
176
|
+
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>AgenticMail Enterprise</title>
|
|
177
|
+
<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}.btn:hover{background:#818cf8}</style></head>
|
|
178
|
+
<body><div class="box"><h1>🏢 <em>AgenticMail</em> Enterprise</h1><p class="sub">Sign in · Go Dashboard</p>
|
|
179
|
+
<form method="POST" action="/login"><div class="fg"><label class="fl">Email</label><input class="input" type="email" name="email" required autofocus></div>
|
|
180
|
+
<div class="fg"><label class="fl">Password</label><input class="input" type="password" name="password" required></div>
|
|
181
|
+
<button class="btn" type="submit">Sign In</button></form></div></body></html>`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Handlers ───────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
|
187
|
+
return func(w http.ResponseWriter, r *http.Request) {
|
|
188
|
+
if getSession(r) == nil {
|
|
189
|
+
http.Redirect(w, r, "/login", http.StatusFound)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
next(w, r)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
197
|
+
if r.Method == "GET" {
|
|
198
|
+
w.Header().Set("Content-Type", "text/html")
|
|
199
|
+
fmt.Fprint(w, loginPage())
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
r.ParseForm()
|
|
203
|
+
data, err := apiCall("/auth/login", "POST", "", map[string]string{
|
|
204
|
+
"email": r.FormValue("email"), "password": r.FormValue("password"),
|
|
205
|
+
})
|
|
206
|
+
if err != nil || data["token"] == nil {
|
|
207
|
+
errMsg := "Login failed"
|
|
208
|
+
if data != nil && data["error"] != nil {
|
|
209
|
+
errMsg = fmt.Sprintf("%v", data["error"])
|
|
210
|
+
}
|
|
211
|
+
w.Header().Set("Content-Type", "text/html")
|
|
212
|
+
fmt.Fprintf(w, `<html><body style="background:#0a0a0f;color:#ef4444;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh"><div>%s <a href="/login" style="color:#6366f1">Try again</a></div></body></html>`, esc(errMsg))
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
user, _ := data["user"].(map[string]interface{})
|
|
216
|
+
setSession(w, &Session{Token: fmt.Sprintf("%v", data["token"]), User: user})
|
|
217
|
+
http.Redirect(w, r, "/", http.StatusFound)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
221
|
+
clearSession(w, r)
|
|
222
|
+
http.Redirect(w, r, "/login", http.StatusFound)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|
226
|
+
s := getSession(r)
|
|
227
|
+
stats, _ := apiCall("/api/stats", "GET", s.Token, nil)
|
|
228
|
+
audit, _ := apiCall("/api/audit?limit=8", "GET", s.Token, nil)
|
|
229
|
+
|
|
230
|
+
var eventsHTML string
|
|
231
|
+
if events, ok := audit["events"].([]interface{}); ok && len(events) > 0 {
|
|
232
|
+
for _, ev := range events {
|
|
233
|
+
e := ev.(map[string]interface{})
|
|
234
|
+
eventsHTML += fmt.Sprintf(`<div style="padding:10px 0;border-bottom:1px solid var(--border);font-size:13px"><span style="color:var(--primary);font-weight:500">%s</span> on %s<div style="font-size:11px;color:var(--muted)">%s</div></div>`,
|
|
235
|
+
esc(e["action"]), esc(e["resource"]), esc(e["timestamp"]))
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
eventsHTML = `<div class="empty"><div class="empty-i">📋</div>No activity yet</div>`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if stats == nil {
|
|
242
|
+
stats = map[string]interface{}{}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
content := fmt.Sprintf(`<h2 class="t">Dashboard</h2><p class="desc">Overview of your AgenticMail instance</p>
|
|
246
|
+
<div class="stats">
|
|
247
|
+
<div class="stat"><div class="l">Total Agents</div><div class="v" style="color:var(--primary)">%d</div></div>
|
|
248
|
+
<div class="stat"><div class="l">Active Agents</div><div class="v" style="color:var(--success)">%d</div></div>
|
|
249
|
+
<div class="stat"><div class="l">Users</div><div class="v">%d</div></div>
|
|
250
|
+
<div class="stat"><div class="l">Audit Events</div><div class="v">%d</div></div></div>
|
|
251
|
+
<div class="card"><div class="ct">Recent Activity</div>%s</div>`,
|
|
252
|
+
intVal(stats, "totalAgents"), intVal(stats, "activeAgents"),
|
|
253
|
+
intVal(stats, "totalUsers"), intVal(stats, "totalAuditEvents"), eventsHTML)
|
|
254
|
+
|
|
255
|
+
w.Header().Set("Content-Type", "text/html")
|
|
256
|
+
fmt.Fprint(w, layout("dashboard", s.User, content))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func handleAgents(w http.ResponseWriter, r *http.Request) {
|
|
260
|
+
s := getSession(r)
|
|
261
|
+
|
|
262
|
+
// Handle archive
|
|
263
|
+
if strings.Contains(r.URL.Path, "/archive") {
|
|
264
|
+
parts := strings.Split(r.URL.Path, "/")
|
|
265
|
+
if len(parts) >= 3 {
|
|
266
|
+
apiCall("/api/agents/"+parts[2]+"/archive", "POST", s.Token, nil)
|
|
267
|
+
}
|
|
268
|
+
http.Redirect(w, r, "/agents", http.StatusFound)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Handle create
|
|
273
|
+
if r.Method == "POST" {
|
|
274
|
+
r.ParseForm()
|
|
275
|
+
body := map[string]string{"name": r.FormValue("name"), "role": r.FormValue("role")}
|
|
276
|
+
if email := r.FormValue("email"); email != "" {
|
|
277
|
+
body["email"] = email
|
|
278
|
+
}
|
|
279
|
+
apiCall("/api/agents", "POST", s.Token, body)
|
|
280
|
+
http.Redirect(w, r, "/agents", http.StatusFound)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
data, _ := apiCall("/api/agents", "GET", s.Token, nil)
|
|
285
|
+
var tableHTML string
|
|
286
|
+
if agents, ok := data["agents"].([]interface{}); ok && len(agents) > 0 {
|
|
287
|
+
rows := ""
|
|
288
|
+
for _, ag := range agents {
|
|
289
|
+
a := ag.(map[string]interface{})
|
|
290
|
+
archiveBtn := ""
|
|
291
|
+
if strVal(a, "status") == "active" {
|
|
292
|
+
archiveBtn = fmt.Sprintf(`<a class="btn btn-sm btn-d" href="/agents/%s/archive">Archive</a>`, esc(a["id"]))
|
|
293
|
+
}
|
|
294
|
+
rows += fmt.Sprintf(`<tr><td style="font-weight:600">%s</td><td style="color:var(--dim)">%s</td><td>%s</td><td>%s</td><td>%s</td></tr>`,
|
|
295
|
+
esc(a["name"]), esc(a["email"]), esc(a["role"]), badge(strVal(a, "status")), archiveBtn)
|
|
296
|
+
}
|
|
297
|
+
tableHTML = `<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th></th></tr></thead><tbody>` + rows + `</tbody></table>`
|
|
298
|
+
} else {
|
|
299
|
+
tableHTML = `<div class="empty"><div class="empty-i">🤖</div>No agents yet</div>`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
content := fmt.Sprintf(`<h2 class="t">Agents</h2><p class="desc">Manage AI agent identities</p>
|
|
303
|
+
<div class="card" style="margin-bottom:16px"><div class="ct">Create Agent</div>
|
|
304
|
+
<form method="POST" action="/agents" style="display:flex;gap:10px;align-items:end">
|
|
305
|
+
<div class="fg" style="flex:1;margin:0"><label class="fl">Name</label><input class="input" name="name" required placeholder="e.g. researcher"></div>
|
|
306
|
+
<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>
|
|
307
|
+
<button class="btn btn-p" type="submit">Create</button></form></div>
|
|
308
|
+
<div class="card">%s</div>`, tableHTML)
|
|
309
|
+
|
|
310
|
+
w.Header().Set("Content-Type", "text/html")
|
|
311
|
+
fmt.Fprint(w, layout("agents", s.User, content))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
func handleUsers(w http.ResponseWriter, r *http.Request) {
|
|
315
|
+
s := getSession(r)
|
|
316
|
+
|
|
317
|
+
if r.Method == "POST" {
|
|
318
|
+
r.ParseForm()
|
|
319
|
+
apiCall("/api/users", "POST", s.Token, map[string]string{
|
|
320
|
+
"name": r.FormValue("name"), "email": r.FormValue("email"),
|
|
321
|
+
"role": r.FormValue("role"), "password": r.FormValue("password"),
|
|
322
|
+
})
|
|
323
|
+
http.Redirect(w, r, "/users", http.StatusFound)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
data, _ := apiCall("/api/users", "GET", s.Token, nil)
|
|
328
|
+
var tableHTML string
|
|
329
|
+
if users, ok := data["users"].([]interface{}); ok && len(users) > 0 {
|
|
330
|
+
rows := ""
|
|
331
|
+
for _, us := range users {
|
|
332
|
+
u := us.(map[string]interface{})
|
|
333
|
+
lastLogin := "Never"
|
|
334
|
+
if v := strVal(u, "lastLoginAt"); v != "" {
|
|
335
|
+
lastLogin = v
|
|
336
|
+
}
|
|
337
|
+
rows += fmt.Sprintf(`<tr><td style="font-weight:600">%s</td><td style="color:var(--dim)">%s</td><td>%s</td><td style="color:var(--muted);font-size:12px">%s</td></tr>`,
|
|
338
|
+
esc(u["name"]), esc(u["email"]), badge(strVal(u, "role")), esc(lastLogin))
|
|
339
|
+
}
|
|
340
|
+
tableHTML = `<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Last Login</th></tr></thead><tbody>` + rows + `</tbody></table>`
|
|
341
|
+
} else {
|
|
342
|
+
tableHTML = `<div class="empty"><div class="empty-i">👥</div>No users yet</div>`
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
content := fmt.Sprintf(`<h2 class="t">Users</h2><p class="desc">Manage team members</p>
|
|
346
|
+
<div class="card" style="margin-bottom:16px"><div class="ct">Create User</div>
|
|
347
|
+
<form method="POST" action="/users" style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
348
|
+
<div class="fg"><label class="fl">Name</label><input class="input" name="name" required></div>
|
|
349
|
+
<div class="fg"><label class="fl">Email</label><input class="input" type="email" name="email" required></div>
|
|
350
|
+
<div class="fg"><label class="fl">Role</label><select class="input" name="role"><option>member</option><option>admin</option><option>owner</option></select></div>
|
|
351
|
+
<div class="fg"><label class="fl">Password</label><input class="input" type="password" name="password" required minlength="8"></div>
|
|
352
|
+
<div><button class="btn btn-p" type="submit">Create</button></div></form></div>
|
|
353
|
+
<div class="card">%s</div>`, tableHTML)
|
|
354
|
+
|
|
355
|
+
w.Header().Set("Content-Type", "text/html")
|
|
356
|
+
fmt.Fprint(w, layout("users", s.User, content))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
func handleAPIKeys(w http.ResponseWriter, r *http.Request) {
|
|
360
|
+
s := getSession(r)
|
|
361
|
+
data, _ := apiCall("/api/api-keys", "GET", s.Token, nil)
|
|
362
|
+
var tableHTML string
|
|
363
|
+
if keys, ok := data["keys"].([]interface{}); ok && len(keys) > 0 {
|
|
364
|
+
rows := ""
|
|
365
|
+
for _, ky := range keys {
|
|
366
|
+
k := ky.(map[string]interface{})
|
|
367
|
+
status := "active"
|
|
368
|
+
if revoked, ok := k["revoked"].(bool); ok && revoked {
|
|
369
|
+
status = "revoked"
|
|
370
|
+
}
|
|
371
|
+
lastUsed := "Never"
|
|
372
|
+
if v := strVal(k, "lastUsedAt"); v != "" {
|
|
373
|
+
lastUsed = v
|
|
374
|
+
}
|
|
375
|
+
rows += fmt.Sprintf(`<tr><td style="font-weight:600">%s</td><td><code style="font-size:12px">%s...</code></td><td style="color:var(--muted);font-size:12px">%s</td><td>%s</td></tr>`,
|
|
376
|
+
esc(k["name"]), esc(k["keyPrefix"]), esc(lastUsed), badge(status))
|
|
377
|
+
}
|
|
378
|
+
tableHTML = `<table><thead><tr><th>Name</th><th>Key</th><th>Last Used</th><th>Status</th></tr></thead><tbody>` + rows + `</tbody></table>`
|
|
379
|
+
} else {
|
|
380
|
+
tableHTML = `<div class="empty"><div class="empty-i">🔑</div>No API keys</div>`
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
content := fmt.Sprintf(`<h2 class="t">API Keys</h2><p class="desc">Manage programmatic access</p><div class="card">%s</div>`, tableHTML)
|
|
384
|
+
w.Header().Set("Content-Type", "text/html")
|
|
385
|
+
fmt.Fprint(w, layout("keys", s.User, content))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
func handleAudit(w http.ResponseWriter, r *http.Request) {
|
|
389
|
+
s := getSession(r)
|
|
390
|
+
p := 0
|
|
391
|
+
fmt.Sscanf(r.URL.Query().Get("p"), "%d", &p)
|
|
392
|
+
if p < 0 {
|
|
393
|
+
p = 0
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
data, _ := apiCall(fmt.Sprintf("/api/audit?limit=25&offset=%d", p*25), "GET", s.Token, nil)
|
|
397
|
+
total := intVal(data, "total")
|
|
398
|
+
var tableHTML string
|
|
399
|
+
if events, ok := data["events"].([]interface{}); ok && len(events) > 0 {
|
|
400
|
+
rows := ""
|
|
401
|
+
for _, ev := range events {
|
|
402
|
+
e := ev.(map[string]interface{})
|
|
403
|
+
ip := strVal(e, "ip")
|
|
404
|
+
if ip == "" {
|
|
405
|
+
ip = "-"
|
|
406
|
+
}
|
|
407
|
+
rows += fmt.Sprintf(`<tr><td style="font-size:12px;color:var(--muted);white-space:nowrap">%s</td><td>%s</td><td style="color:var(--primary);font-weight:500">%s</td><td style="font-size:12px">%s</td><td style="font-size:12px;color:var(--muted)">%s</td></tr>`,
|
|
408
|
+
esc(e["timestamp"]), esc(e["actor"]), esc(e["action"]), esc(e["resource"]), esc(ip))
|
|
409
|
+
}
|
|
410
|
+
pages := (total + 24) / 25
|
|
411
|
+
nav := fmt.Sprintf(`<div style="display:flex;gap:8px;justify-content:center;margin-top:16px"><span style="padding:6px 12px;font-size:12px;color:var(--muted)">Page %d of %d</span></div>`, p+1, pages)
|
|
412
|
+
if p > 0 {
|
|
413
|
+
nav = fmt.Sprintf(`<div style="display:flex;gap:8px;justify-content:center;margin-top:16px"><a class="btn btn-sm" href="/audit?p=%d">← Prev</a><span style="padding:6px 12px;font-size:12px;color:var(--muted)">Page %d of %d</span>`, p-1, p+1, pages)
|
|
414
|
+
if (p+1)*25 < total {
|
|
415
|
+
nav += fmt.Sprintf(`<a class="btn btn-sm" href="/audit?p=%d">Next →</a>`, p+1)
|
|
416
|
+
}
|
|
417
|
+
nav += `</div>`
|
|
418
|
+
} else if (p+1)*25 < total {
|
|
419
|
+
nav = fmt.Sprintf(`<div style="display:flex;gap:8px;justify-content:center;margin-top:16px"><span style="padding:6px 12px;font-size:12px;color:var(--muted)">Page %d of %d</span><a class="btn btn-sm" href="/audit?p=%d">Next →</a></div>`, p+1, pages, p+1)
|
|
420
|
+
}
|
|
421
|
+
tableHTML = `<table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Resource</th><th>IP</th></tr></thead><tbody>` + rows + `</tbody></table>` + nav
|
|
422
|
+
} else {
|
|
423
|
+
tableHTML = `<div class="empty"><div class="empty-i">📋</div>No audit events yet</div>`
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
content := fmt.Sprintf(`<h2 class="t">Audit Log</h2><p class="desc">%d total events</p><div class="card">%s</div>`, total, tableHTML)
|
|
427
|
+
w.Header().Set("Content-Type", "text/html")
|
|
428
|
+
fmt.Fprint(w, layout("audit", s.User, content))
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
func handleSettings(w http.ResponseWriter, r *http.Request) {
|
|
432
|
+
s := getSession(r)
|
|
433
|
+
|
|
434
|
+
if r.Method == "POST" {
|
|
435
|
+
r.ParseForm()
|
|
436
|
+
apiCall("/api/settings", "PATCH", s.Token, map[string]string{
|
|
437
|
+
"name": r.FormValue("name"),
|
|
438
|
+
"domain": r.FormValue("domain"),
|
|
439
|
+
"primaryColor": r.FormValue("primaryColor"),
|
|
440
|
+
})
|
|
441
|
+
http.Redirect(w, r, "/settings", http.StatusFound)
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
settings, _ := apiCall("/api/settings", "GET", s.Token, nil)
|
|
446
|
+
retention, _ := apiCall("/api/retention", "GET", s.Token, nil)
|
|
447
|
+
if settings == nil {
|
|
448
|
+
settings = map[string]interface{}{}
|
|
449
|
+
}
|
|
450
|
+
if retention == nil {
|
|
451
|
+
retention = map[string]interface{}{}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
retEnabled := "Disabled"
|
|
455
|
+
retColor := "var(--muted)"
|
|
456
|
+
if enabled, ok := retention["enabled"].(bool); ok && enabled {
|
|
457
|
+
retEnabled = "Enabled"
|
|
458
|
+
retColor = "var(--success)"
|
|
459
|
+
}
|
|
460
|
+
retDays := intVal(retention, "retainDays")
|
|
461
|
+
if retDays == 0 {
|
|
462
|
+
retDays = 365
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
content := fmt.Sprintf(`<h2 class="t">Settings</h2><p class="desc">Configure your organization</p>
|
|
466
|
+
<div class="card"><div class="ct">General</div>
|
|
467
|
+
<form method="POST" action="/settings" style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
|
468
|
+
<div class="fg"><label class="fl">Organization Name</label><input class="input" name="name" value="%s"></div>
|
|
469
|
+
<div class="fg"><label class="fl">Domain</label><input class="input" name="domain" value="%s" placeholder="agents.acme.com"></div>
|
|
470
|
+
<div class="fg"><label class="fl">Primary Color</label><input class="input" type="color" name="primaryColor" value="%s" style="height:38px;padding:4px"></div>
|
|
471
|
+
<div></div><div><button class="btn btn-p" type="submit">Save Settings</button></div></form></div>
|
|
472
|
+
<div class="card"><div class="ct">Plan</div>%s <span style="font-size:13px;color:var(--dim);margin-left:12px">Subdomain: %s.agenticmail.cloud</span></div>
|
|
473
|
+
<div class="card"><div class="ct">Data Retention</div><div style="font-size:13px">Status: <span style="color:%s">%s</span><br><span style="color:var(--dim)">Retain emails for %d days</span></div></div>`,
|
|
474
|
+
esc(settings["name"]), esc(settings["domain"]),
|
|
475
|
+
esc(settings["primaryColor"]),
|
|
476
|
+
badge(strings.ToUpper(strVal(settings, "plan"))),
|
|
477
|
+
esc(settings["subdomain"]),
|
|
478
|
+
retColor, retEnabled, retDays)
|
|
479
|
+
|
|
480
|
+
w.Header().Set("Content-Type", "text/html")
|
|
481
|
+
fmt.Fprint(w, layout("settings", s.User, content))
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ─── Main ───────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
func main() {
|
|
487
|
+
if url := os.Getenv("AGENTICMAIL_URL"); url != "" {
|
|
488
|
+
apiURL = url
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
_ = template.New("") // ensure html/template is used
|
|
492
|
+
|
|
493
|
+
http.HandleFunc("/login", handleLogin)
|
|
494
|
+
http.HandleFunc("/logout", handleLogout)
|
|
495
|
+
http.HandleFunc("/", requireAuth(handleDashboard))
|
|
496
|
+
http.HandleFunc("/agents", requireAuth(handleAgents))
|
|
497
|
+
http.HandleFunc("/agents/", requireAuth(handleAgents))
|
|
498
|
+
http.HandleFunc("/users", requireAuth(handleUsers))
|
|
499
|
+
http.HandleFunc("/api-keys", requireAuth(handleAPIKeys))
|
|
500
|
+
http.HandleFunc("/audit", requireAuth(handleAudit))
|
|
501
|
+
http.HandleFunc("/settings", requireAuth(handleSettings))
|
|
502
|
+
|
|
503
|
+
port := os.Getenv("PORT")
|
|
504
|
+
if port == "" {
|
|
505
|
+
port = "8080"
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fmt.Printf("\n🏢 AgenticMail Enterprise Dashboard (Go)\n")
|
|
509
|
+
fmt.Printf(" API: %s\n", apiURL)
|
|
510
|
+
fmt.Printf(" Dashboard: http://localhost:%s\n\n", port)
|
|
511
|
+
|
|
512
|
+
log.Fatal(http.ListenAndServe(":"+port, nil))
|
|
513
|
+
}
|