@agenticmail/enterprise 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. package/tsconfig.json +14 -0
@@ -0,0 +1,183 @@
1
+ # AgenticMail Enterprise Architecture
2
+
3
+ ## Vision
4
+ Company installs `npx agenticmail-enterprise` → runs setup wizard → gets a cloud-hosted admin dashboard URL → manages AI agent identities, email, auth, compliance from that dashboard.
5
+
6
+ ## Core Principles
7
+ 1. **Cloud-first**: No local servers. Deploys to user's cloud or our managed infra.
8
+ 2. **Bring your own database**: Support Postgres, MySQL, SQLite, MongoDB, DynamoDB, CockroachDB, PlanetScale, Turso, Supabase, Neon — anything.
9
+ 3. **Bring your own cloud**: Deploy to Fly.io, Railway, Render, AWS, GCP, Azure, Vercel, or managed by us.
10
+ 4. **Auto-provisioned URL**: Instant `<company>.agenticmail.cloud` subdomain, with custom domain support.
11
+ 5. **Admin UI**: Web dashboard for agent CRUD, audit logs, rules, compliance.
12
+
13
+ ## User Journey
14
+
15
+ ```
16
+ $ npx agenticmail-enterprise
17
+
18
+ šŸ¢ AgenticMail Enterprise Setup
19
+
20
+ ? Company name: Acme Corp
21
+ ? Admin email: admin@acme.com
22
+ ? Database: (choose one)
23
+ ā–ø PostgreSQL (connection string)
24
+ MySQL (connection string)
25
+ MongoDB (connection string)
26
+ SQLite (embedded)
27
+ Turso (LibSQL)
28
+ PlanetScale (MySQL-compatible)
29
+ Supabase (Postgres)
30
+ Neon (Postgres)
31
+ DynamoDB (AWS)
32
+ CockroachDB
33
+
34
+ ? Database connection: postgresql://...
35
+ ? Deploy to: (choose one)
36
+ ā–ø AgenticMail Cloud (managed, free tier)
37
+ Fly.io
38
+ Railway
39
+ Render
40
+ Docker (self-hosted)
41
+
42
+ ? Custom domain (optional): mail.acme.com
43
+
44
+ ā³ Provisioning...
45
+ āœ“ Database schema created
46
+ āœ“ Admin account created
47
+ āœ“ DKIM/SPF/DMARC configured
48
+ āœ“ Deployed to agenticmail.cloud
49
+
50
+ šŸŽ‰ Your dashboard is live!
51
+ URL: https://acme.agenticmail.cloud
52
+ Admin: admin@acme.com (check email for password)
53
+
54
+ Add custom domain later:
55
+ CNAME mail.acme.com → acme.agenticmail.cloud
56
+ ```
57
+
58
+ ## Package Structure
59
+
60
+ ```
61
+ @agenticmail/enterprise
62
+ ā”œā”€ā”€ src/
63
+ │ ā”œā”€ā”€ index.ts # CLI entry point
64
+ │ ā”œā”€ā”€ setup/
65
+ │ │ ā”œā”€ā”€ wizard.ts # Interactive setup flow
66
+ │ │ ā”œā”€ā”€ database.ts # DB adapter factory
67
+ │ │ └── deploy.ts # Cloud deployment orchestrator
68
+ │ ā”œā”€ā”€ db/
69
+ │ │ ā”œā”€ā”€ adapter.ts # Abstract DB interface
70
+ │ │ ā”œā”€ā”€ postgres.ts # PostgreSQL adapter
71
+ │ │ ā”œā”€ā”€ mysql.ts # MySQL adapter
72
+ │ │ ā”œā”€ā”€ mongodb.ts # MongoDB adapter
73
+ │ │ ā”œā”€ā”€ sqlite.ts # SQLite adapter (dev/small teams)
74
+ │ │ ā”œā”€ā”€ turso.ts # Turso/LibSQL adapter
75
+ │ │ ā”œā”€ā”€ dynamodb.ts # DynamoDB adapter
76
+ │ │ └── migrations/ # Schema migrations (per adapter)
77
+ │ ā”œā”€ā”€ auth/
78
+ │ │ ā”œā”€ā”€ saml.ts # SAML 2.0 SP
79
+ │ │ ā”œā”€ā”€ oidc.ts # OAuth 2.0 / OpenID Connect
80
+ │ │ ā”œā”€ā”€ scim.ts # SCIM provisioning
81
+ │ │ ā”œā”€ā”€ api-keys.ts # API key management
82
+ │ │ └── sessions.ts # Session management
83
+ │ ā”œā”€ā”€ admin/
84
+ │ │ ā”œā”€ā”€ dashboard.ts # Admin API routes
85
+ │ │ ā”œā”€ā”€ agents.ts # Agent CRUD
86
+ │ │ ā”œā”€ā”€ audit.ts # Audit log viewer
87
+ │ │ ā”œā”€ā”€ rules.ts # Email rules management
88
+ │ │ ā”œā”€ā”€ compliance.ts # DLP, retention policies
89
+ │ │ └── billing.ts # Usage tracking, plans
90
+ │ ā”œā”€ā”€ deploy/
91
+ │ │ ā”œā”€ā”€ fly.ts # Fly.io deployment
92
+ │ │ ā”œā”€ā”€ railway.ts # Railway deployment
93
+ │ │ ā”œā”€ā”€ render.ts # Render deployment
94
+ │ │ ā”œā”€ā”€ docker.ts # Docker/self-hosted
95
+ │ │ └── managed.ts # AgenticMail Cloud (our infra)
96
+ │ ā”œā”€ā”€ ui/ # Admin dashboard (React/Next.js)
97
+ │ │ ā”œā”€ā”€ app/
98
+ │ │ │ ā”œā”€ā”€ layout.tsx
99
+ │ │ │ ā”œā”€ā”€ page.tsx # Dashboard home
100
+ │ │ │ ā”œā”€ā”€ agents/ # Agent management
101
+ │ │ │ ā”œā”€ā”€ audit/ # Audit logs
102
+ │ │ │ ā”œā”€ā”€ settings/ # Company settings
103
+ │ │ │ ā”œā”€ā”€ auth/ # SSO config
104
+ │ │ │ └── compliance/ # DLP, retention
105
+ │ │ └── components/
106
+ │ └── server.ts # Express/Hono server (API + UI)
107
+ └── package.json
108
+ ```
109
+
110
+ ## Database Adapter Interface
111
+
112
+ ```typescript
113
+ interface DatabaseAdapter {
114
+ // Connection
115
+ connect(config: DatabaseConfig): Promise<void>;
116
+ disconnect(): Promise<void>;
117
+ migrate(): Promise<void>;
118
+
119
+ // Agents
120
+ createAgent(agent: AgentInput): Promise<Agent>;
121
+ getAgent(id: string): Promise<Agent | null>;
122
+ listAgents(filters?: AgentFilters): Promise<Agent[]>;
123
+ updateAgent(id: string, updates: Partial<Agent>): Promise<Agent>;
124
+ deleteAgent(id: string): Promise<void>;
125
+ archiveAgent(id: string): Promise<DeletionReport>;
126
+
127
+ // Email
128
+ storeEmail(email: StoredEmail): Promise<void>;
129
+ getEmail(uid: number): Promise<StoredEmail | null>;
130
+ searchEmails(query: SearchQuery): Promise<StoredEmail[]>;
131
+
132
+ // Audit
133
+ logEvent(event: AuditEvent): Promise<void>;
134
+ queryAuditLog(filters: AuditFilters): Promise<AuditEvent[]>;
135
+
136
+ // Auth
137
+ createUser(user: UserInput): Promise<User>;
138
+ getUser(id: string): Promise<User | null>;
139
+ getUserByEmail(email: string): Promise<User | null>;
140
+
141
+ // API Keys
142
+ createApiKey(key: ApiKeyInput): Promise<ApiKey>;
143
+ validateApiKey(key: string): Promise<ApiKey | null>;
144
+ revokeApiKey(id: string): Promise<void>;
145
+
146
+ // Rules & Compliance
147
+ createRule(rule: RuleInput): Promise<Rule>;
148
+ getRules(): Promise<Rule[]>;
149
+ getRetentionPolicy(): Promise<RetentionPolicy>;
150
+ setRetentionPolicy(policy: RetentionPolicy): Promise<void>;
151
+ }
152
+ ```
153
+
154
+ ## Deployment Architecture
155
+
156
+ ### AgenticMail Cloud (Managed)
157
+ - Fly.io multi-region (us-east, eu-west, ap-southeast)
158
+ - Customer gets `<company>.agenticmail.cloud` subdomain
159
+ - Wildcard TLS via Fly.io
160
+ - Shared Fly.io org, isolated apps per customer
161
+ - Customer can add custom domain (CNAME → our subdomain)
162
+
163
+ ### Self-Hosted
164
+ - Single Docker image: `docker run -p 3000:3000 agenticmail/enterprise`
165
+ - Or via CLI: `npx agenticmail-enterprise start`
166
+ - Env vars for DB connection, SMTP, domain
167
+
168
+ ## Admin Dashboard Pages
169
+
170
+ 1. **Dashboard** — Overview: active agents, emails sent/received, health
171
+ 2. **Agents** — Create, edit, archive, delete. Role assignment. Email config per agent.
172
+ 3. **Audit Log** — Who did what, when. Filterable, exportable.
173
+ 4. **Authentication** — SAML/OIDC setup, user management, API keys
174
+ 5. **Compliance** — DLP rules, retention policies, outbound guards
175
+ 6. **Email Rules** — Server-side rules (auto-tag, auto-move, auto-reply)
176
+ 7. **Settings** — Company info, domain, SMTP config, billing
177
+ 8. **Integrations** — Slack, Teams, Discord notifications
178
+
179
+ ## Pricing Tiers (Future)
180
+ - **Free**: 3 agents, 1K emails/mo, community support
181
+ - **Team**: 25 agents, 50K emails/mo, SSO, audit logs — $49/mo
182
+ - **Enterprise**: Unlimited, SCIM, DLP, retention, SLA, dedicated support — $299/mo
183
+ - **Self-Hosted**: Unlimited, your infra — $99/mo license
Binary file
@@ -0,0 +1,120 @@
1
+ # AgenticMail Enterprise Dashboards
2
+
3
+ **Admin dashboards in every popular language.** Pick the one that matches your stack — or use the zero-code HTML version if you're not a developer.
4
+
5
+ All dashboards connect to the same AgenticMail Enterprise API. They're interchangeable.
6
+
7
+ ---
8
+
9
+ ## šŸŽÆ Quick Start by Language
10
+
11
+ | Language | File | Dependencies | Run Command |
12
+ |----------|------|-------------|-------------|
13
+ | **HTML** (zero-code) | `html/index.html` | None! | Just open in browser |
14
+ | **PHP** | `php/index.php` | PHP 7.4+ | `php -S localhost:8080 index.php` |
15
+ | **Python** | `python/app.py` | Flask, requests | `pip install flask requests && python app.py` |
16
+ | **Ruby** | `ruby/app.rb` | Sinatra | `gem install sinatra && ruby app.rb` |
17
+ | **Express.js** | `express/app.js` | Express | `npm install express express-session && node app.js` |
18
+ | **React** (built-in) | _Served at `/dashboard`_ | None (bundled) | Comes with the enterprise server |
19
+
20
+ | **Go** | `go/main.go` | None (stdlib only) | `go run main.go` |
21
+ | **Java** | `java/AgenticMailDashboard.java` | JDK 11+ | `javac AgenticMailDashboard.java && java AgenticMailDashboard` |
22
+ | **C# / .NET** | `dotnet/Program.cs` | .NET 8+ | `dotnet new web && dotnet run` |
23
+
24
+ ### Coming Soon
25
+ | Language | Status |
26
+ |----------|--------|
27
+ | **Laravel** (PHP framework) | Planned |
28
+ | **Django** (Python framework) | Planned |
29
+ | **Rails** (Ruby framework) | Planned |
30
+ | **Svelte** | Planned |
31
+ | **Vue.js** | Planned |
32
+ | **Angular** | Planned |
33
+
34
+ ---
35
+
36
+ ## šŸš€ Non-Coders: Start Here
37
+
38
+ **You don't need to know how to code.** Use the HTML dashboard:
39
+
40
+ 1. Download `html/index.html`
41
+ 2. Open it in any web browser (Chrome, Safari, Firefox, Edge)
42
+ 3. Enter your AgenticMail Enterprise server URL when prompted
43
+ 4. Login with your admin credentials
44
+
45
+ That's it. No installation, no terminal, no coding.
46
+
47
+ ---
48
+
49
+ ## šŸ”Œ How It Works
50
+
51
+ Every dashboard talks to the **AgenticMail Enterprise REST API**:
52
+
53
+ ```
54
+ Your Dashboard → REST API → AgenticMail Enterprise Server
55
+ (any language) (JSON) (your database)
56
+ ```
57
+
58
+ The API endpoints:
59
+ - `POST /auth/login` — Get JWT token
60
+ - `GET /api/stats` — Dashboard stats
61
+ - `GET /api/agents` — List agents
62
+ - `POST /api/agents` — Create agent
63
+ - `GET /api/users` — List users
64
+ - `GET /api/api-keys` — List API keys
65
+ - `GET /api/audit` — Audit log
66
+ - `GET /api/settings` — Organization settings
67
+ - `GET /health` — Health check
68
+
69
+ Full API docs at your server's `/health` endpoint.
70
+
71
+ ---
72
+
73
+ ## šŸŽØ All Dashboards Include
74
+
75
+ - **Dark theme** — Easy on the eyes
76
+ - **Responsive** — Works on mobile, tablet, desktop
77
+ - **Authentication** — Login with email/password, JWT sessions
78
+ - **Dashboard** — Stats overview with agent/user/event counts
79
+ - **Agents** — Create, list, archive AI agents
80
+ - **Users** — Manage team members with roles (owner/admin/member/viewer)
81
+ - **API Keys** — Create, list, revoke programmatic access keys
82
+ - **Audit Log** — Paginated event history
83
+ - **Settings** — Organization name, domain, branding
84
+
85
+ ---
86
+
87
+ ## šŸ”§ Configuration
88
+
89
+ All dashboards use one environment variable:
90
+
91
+ ```bash
92
+ AGENTICMAIL_URL=https://your-company.agenticmail.cloud
93
+ ```
94
+
95
+ Or edit the `API_URL` variable at the top of each file.
96
+
97
+ ---
98
+
99
+ ## šŸ—ļø Build Your Own
100
+
101
+ The API is REST + JSON. Build a dashboard in any language:
102
+
103
+ ```bash
104
+ # Login
105
+ curl -X POST https://your-server/auth/login \
106
+ -H "Content-Type: application/json" \
107
+ -d '{"email":"admin@acme.com","password":"..."}'
108
+ # Returns: { "token": "eyJ...", "user": {...} }
109
+
110
+ # Use the token
111
+ curl https://your-server/api/stats \
112
+ -H "Authorization: Bearer eyJ..."
113
+ # Returns: { "totalAgents": 5, "activeAgents": 3, ... }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## šŸ“„ License
119
+
120
+ MIT — Use these dashboards however you want.
@@ -0,0 +1,261 @@
1
+ // AgenticMail Enterprise Dashboard — .NET / C# Edition
2
+ //
3
+ // Uses .NET 8+ Minimal API. No MVC, no Razor, no extra NuGet packages.
4
+ //
5
+ // Setup:
6
+ // dotnet new web -n AgenticMailDashboard
7
+ // cp Program.cs AgenticMailDashboard/Program.cs
8
+ // cd AgenticMailDashboard && dotnet run
9
+ //
10
+ // Or create project in one shot:
11
+ // mkdir AgenticMailDashboard && cd AgenticMailDashboard
12
+ // dotnet new web
13
+ // # Replace Program.cs with this file
14
+ // dotnet run
15
+
16
+ using System.Net.Http.Json;
17
+ using System.Text.Json;
18
+ using System.Web;
19
+
20
+ var builder = WebApplication.CreateBuilder(args);
21
+ builder.Services.AddDistributedMemoryCache();
22
+ builder.Services.AddSession(o => { o.IdleTimeout = TimeSpan.FromHours(24); o.Cookie.HttpOnly = true; });
23
+ builder.Services.AddHttpClient();
24
+
25
+ var app = builder.Build();
26
+ app.UseSession();
27
+
28
+ var API_URL = Environment.GetEnvironmentVariable("AGENTICMAIL_URL") ?? "http://localhost:3000";
29
+ var httpFactory = app.Services.GetRequiredService<IHttpClientFactory>();
30
+
31
+ // ─── API Client ─────────────────────────────────────────
32
+
33
+ async Task<JsonElement?> Api(HttpContext ctx, string path, string method = "GET", object? body = null)
34
+ {
35
+ var client = httpFactory.CreateClient();
36
+ client.Timeout = TimeSpan.FromSeconds(10);
37
+ var token = ctx.Session.GetString("token");
38
+ if (token != null) client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
39
+
40
+ HttpResponseMessage resp;
41
+ var uri = $"{API_URL}{path}";
42
+ switch (method)
43
+ {
44
+ case "POST": resp = await client.PostAsJsonAsync(uri, body ?? new { }); break;
45
+ case "PATCH": resp = await client.PatchAsJsonAsync(uri, body ?? new { }); break;
46
+ case "DELETE": resp = await client.DeleteAsync(uri); break;
47
+ default: resp = await client.GetAsync(uri); break;
48
+ }
49
+ var json = await resp.Content.ReadAsStringAsync();
50
+ try { return JsonDocument.Parse(json).RootElement; }
51
+ catch { return null; }
52
+ }
53
+
54
+ string Esc(string? s) => HttpUtility.HtmlEncode(s ?? "");
55
+ string Str(JsonElement? el, string prop) => el?.TryGetProperty(prop, out var v) == true ? v.ToString() : "";
56
+ int Int(JsonElement? el, string prop) => el?.TryGetProperty(prop, out var v) == true && v.TryGetInt32(out var n) ? n : 0;
57
+
58
+ string Badge(string status)
59
+ {
60
+ var colors = new Dictionary<string, string>
61
+ {
62
+ ["active"] = "#22c55e", ["archived"] = "#888", ["suspended"] = "#ef4444",
63
+ ["owner"] = "#f59e0b", ["admin"] = "#6366f1", ["member"] = "#888", ["viewer"] = "#555"
64
+ };
65
+ var c = colors.GetValueOrDefault(status, "#888");
66
+ return $"<span style='display:inline-block;padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;background:{c}20;color:{c}'>{Esc(status)}</span>";
67
+ }
68
+
69
+ // ─── CSS & Layout ───────────────────────────────────────
70
+
71
+ const string CSS = @"*{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)}.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-p{background:var(--primary);border-color:var(--primary);color:#fff}.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)}select.input{appearance:auto}";
72
+
73
+ string NavItem(string href, string icon, string label, string page) =>
74
+ $"<a href='{href}' class='{(page == label.ToLower() ? "on" : "")}'>{icon} <span>{label}</span></a>";
75
+
76
+ string Layout(string page, string userName, string userEmail, string content) => $@"<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1.0'><title>AgenticMail Enterprise — .NET</title><style>{CSS}</style></head>
77
+ <body><div class='layout'>
78
+ <div class='sidebar'><div class='sh'><h2>šŸ¢ <em>Agentic</em>Mail</h2><small>Enterprise Ā· .NET</small></div>
79
+ <div class='nav'><div class='ns'>Overview</div>{NavItem("/", "šŸ“Š", "Dashboard", page)}<div class='ns'>Manage</div>{NavItem("/agents", "šŸ¤–", "Agents", page)}{NavItem("/users", "šŸ‘„", "Users", page)}{NavItem("/api-keys", "šŸ”‘", "API Keys", page)}<div class='ns'>System</div>{NavItem("/audit", "šŸ“‹", "Audit", page)}{NavItem("/settings", "āš™ļø", "Settings", page)}</div>
80
+ <div class='sf'><div style='color:var(--dim)'>{Esc(userName)}</div><div style='color:var(--muted);font-size:11px'>{Esc(userEmail)}</div><a href='/logout' style='color:var(--muted);font-size:11px;margin-top:6px;display:inline-block'>Sign out</a></div></div>
81
+ <div class='content'>{content}</div></div></body></html>";
82
+
83
+ string Html(HttpContext ctx, string page, string content)
84
+ {
85
+ var name = ctx.Session.GetString("userName") ?? "";
86
+ var email = ctx.Session.GetString("userEmail") ?? "";
87
+ return Layout(page, name, email, content);
88
+ }
89
+
90
+ // ─── Auth ───────────────────────────────────────────────
91
+
92
+ app.MapGet("/login", () => Results.Content($@"<!DOCTYPE html><html><head><meta charset='UTF-8'><title>AgenticMail</title><style>{CSS}</style></head>
93
+ <body style='display:flex;align-items:center;justify-content:center;min-height:100vh'>
94
+ <div style='width:380px'><h1 style='text-align:center;font-size:22px'>šŸ¢ <em style='color:var(--primary)'>AgenticMail</em> Enterprise</h1>
95
+ <p style='text-align:center;color:var(--dim);font-size:13px;margin-bottom:32px'>Sign in Ā· .NET Dashboard</p>
96
+ <form method='POST' action='/login'><div class='fg'><label class='fl'>Email</label><input class='input' type='email' name='email' required></div>
97
+ <div class='fg'><label class='fl'>Password</label><input class='input' type='password' name='password' required></div>
98
+ <button class='btn btn-p' style='width:100%' type='submit'>Sign In</button></form></div></body></html>", "text/html"));
99
+
100
+ app.MapPost("/login", async (HttpContext ctx) =>
101
+ {
102
+ var form = await ctx.Request.ReadFormAsync();
103
+ var data = await Api(ctx, "/auth/login", "POST", new { email = form["email"].ToString(), password = form["password"].ToString() });
104
+ if (data?.TryGetProperty("token", out var tok) == true)
105
+ {
106
+ ctx.Session.SetString("token", tok.ToString());
107
+ if (data?.TryGetProperty("user", out var u) == true)
108
+ {
109
+ ctx.Session.SetString("userName", Str(u, "name"));
110
+ ctx.Session.SetString("userEmail", Str(u, "email"));
111
+ }
112
+ return Results.Redirect("/");
113
+ }
114
+ return Results.Content($"Login failed: {Str(data, "error")} <a href='/login'>Try again</a>", "text/html");
115
+ });
116
+
117
+ app.MapGet("/logout", (HttpContext ctx) => { ctx.Session.Clear(); return Results.Redirect("/login"); });
118
+
119
+ // ─── Auth Guard ─────────────────────────────────────────
120
+
121
+ app.Use(async (ctx, next) =>
122
+ {
123
+ var path = ctx.Request.Path.Value ?? "";
124
+ if (path != "/login" && !path.StartsWith("/login") && ctx.Session.GetString("token") == null)
125
+ {
126
+ ctx.Response.Redirect("/login");
127
+ return;
128
+ }
129
+ await next();
130
+ });
131
+
132
+ // ─── Dashboard ──────────────────────────────────────────
133
+
134
+ app.MapGet("/", async (HttpContext ctx) =>
135
+ {
136
+ var stats = await Api(ctx, "/api/stats");
137
+ var audit = await Api(ctx, "/api/audit?limit=8");
138
+ var events = "";
139
+ if (audit?.TryGetProperty("events", out var evArr) == true)
140
+ {
141
+ foreach (var e in evArr.EnumerateArray())
142
+ events += $"<div style='padding:10px 0;border-bottom:1px solid var(--border);font-size:13px'><span style='color:var(--primary);font-weight:500'>{Esc(Str(e, "action"))}</span> on {Esc(Str(e, "resource"))}<div style='font-size:11px;color:var(--muted)'>{Esc(Str(e, "timestamp"))}</div></div>";
143
+ }
144
+ if (string.IsNullOrEmpty(events)) events = "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>šŸ“‹</div>No activity yet</div>";
145
+
146
+ return Results.Content(Html(ctx, "dashboard",
147
+ $@"<h2 class='t'>Dashboard</h2><p class='desc'>Overview</p>
148
+ <div class='stats'>
149
+ <div class='stat'><div class='l'>Total Agents</div><div class='v' style='color:var(--primary)'>{Int(stats, "totalAgents")}</div></div>
150
+ <div class='stat'><div class='l'>Active Agents</div><div class='v' style='color:var(--success)'>{Int(stats, "activeAgents")}</div></div>
151
+ <div class='stat'><div class='l'>Users</div><div class='v'>{Int(stats, "totalUsers")}</div></div>
152
+ <div class='stat'><div class='l'>Audit Events</div><div class='v'>{Int(stats, "totalAuditEvents")}</div></div></div>
153
+ <div class='card'><div class='ct'>Recent Activity</div>{events}</div>"), "text/html");
154
+ });
155
+
156
+ // ─── Agents ─────────────────────────────────────────────
157
+
158
+ app.MapGet("/agents", async (HttpContext ctx) =>
159
+ {
160
+ var data = await Api(ctx, "/api/agents");
161
+ var rows = "";
162
+ if (data?.TryGetProperty("agents", out var arr) == true)
163
+ {
164
+ foreach (var a in arr.EnumerateArray())
165
+ rows += $"<tr><td style='font-weight:600'>{Esc(Str(a, "name"))}</td><td style='color:var(--dim)'>{Esc(Str(a, "email"))}</td><td>{Esc(Str(a, "role"))}</td><td>{Badge(Str(a, "status"))}</td></tr>";
166
+ }
167
+ var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>šŸ¤–</div>No agents yet</div>" :
168
+ $"<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>";
169
+
170
+ return Results.Content(Html(ctx, "agents",
171
+ $@"<h2 class='t'>Agents</h2><p class='desc'>Manage AI agent identities</p>
172
+ <div class='card' style='margin-bottom:16px'><div class='ct'>Create Agent</div>
173
+ <form method='POST' action='/agents' style='display:flex;gap:10px;align-items:end'>
174
+ <div class='fg' style='flex:1;margin:0'><label class='fl'>Name</label><input class='input' name='name' required placeholder='e.g. researcher'></div>
175
+ <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></select></div>
176
+ <button class='btn btn-p' type='submit'>Create</button></form></div>
177
+ <div class='card'>{table}</div>"), "text/html");
178
+ });
179
+
180
+ app.MapPost("/agents", async (HttpContext ctx) =>
181
+ {
182
+ var form = await ctx.Request.ReadFormAsync();
183
+ await Api(ctx, "/api/agents", "POST", new { name = form["name"].ToString(), role = form["role"].ToString() });
184
+ return Results.Redirect("/agents");
185
+ });
186
+
187
+ // ─── Users ──────────────────────────────────────────────
188
+
189
+ app.MapGet("/users", async (HttpContext ctx) =>
190
+ {
191
+ var data = await Api(ctx, "/api/users");
192
+ var rows = "";
193
+ if (data?.TryGetProperty("users", out var arr) == true)
194
+ {
195
+ foreach (var u in arr.EnumerateArray())
196
+ rows += $"<tr><td style='font-weight:600'>{Esc(Str(u, "name"))}</td><td style='color:var(--dim)'>{Esc(Str(u, "email"))}</td><td>{Badge(Str(u, "role"))}</td></tr>";
197
+ }
198
+ var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>šŸ‘„</div>No users yet</div>" :
199
+ $"<table><thead><tr><th>Name</th><th>Email</th><th>Role</th></tr></thead><tbody>{rows}</tbody></table>";
200
+ return Results.Content(Html(ctx, "users", $"<h2 class='t'>Users</h2><p class='desc'>Manage team members</p><div class='card'>{table}</div>"), "text/html");
201
+ });
202
+
203
+ // ─── API Keys ───────────────────────────────────────────
204
+
205
+ app.MapGet("/api-keys", async (HttpContext ctx) =>
206
+ {
207
+ var data = await Api(ctx, "/api/api-keys");
208
+ var rows = "";
209
+ if (data?.TryGetProperty("keys", out var arr) == true)
210
+ {
211
+ foreach (var k in arr.EnumerateArray())
212
+ {
213
+ var status = k.TryGetProperty("revoked", out var rev) && rev.GetBoolean() ? "revoked" : "active";
214
+ rows += $"<tr><td style='font-weight:600'>{Esc(Str(k, "name"))}</td><td><code style='font-size:12px'>{Esc(Str(k, "keyPrefix"))}...</code></td><td>{Badge(status)}</td></tr>";
215
+ }
216
+ }
217
+ var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>šŸ”‘</div>No API keys</div>" :
218
+ $"<table><thead><tr><th>Name</th><th>Key</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>";
219
+ return Results.Content(Html(ctx, "api keys", $"<h2 class='t'>API Keys</h2><p class='desc'>Manage programmatic access</p><div class='card'>{table}</div>"), "text/html");
220
+ });
221
+
222
+ // ─── Audit ──────────────────────────────────────────────
223
+
224
+ app.MapGet("/audit", async (HttpContext ctx) =>
225
+ {
226
+ var data = await Api(ctx, "/api/audit?limit=25");
227
+ var total = Int(data, "total");
228
+ var rows = "";
229
+ if (data?.TryGetProperty("events", out var arr) == true)
230
+ {
231
+ foreach (var e in arr.EnumerateArray())
232
+ rows += $"<tr><td style='font-size:12px;color:var(--muted)'>{Esc(Str(e, "timestamp"))}</td><td>{Esc(Str(e, "actor"))}</td><td style='color:var(--primary);font-weight:500'>{Esc(Str(e, "action"))}</td><td style='font-size:12px'>{Esc(Str(e, "resource"))}</td></tr>";
233
+ }
234
+ var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>šŸ“‹</div>No audit events</div>" :
235
+ $"<table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Resource</th></tr></thead><tbody>{rows}</tbody></table>";
236
+ return Results.Content(Html(ctx, "audit", $"<h2 class='t'>Audit Log</h2><p class='desc'>{total} events</p><div class='card'>{table}</div>"), "text/html");
237
+ });
238
+
239
+ // ─── Settings ───────────────────────────────────────────
240
+
241
+ app.MapGet("/settings", async (HttpContext ctx) =>
242
+ {
243
+ var s = await Api(ctx, "/api/settings");
244
+ return Results.Content(Html(ctx, "settings",
245
+ $@"<h2 class='t'>Settings</h2><p class='desc'>Configure your organization</p>
246
+ <div class='card'><div class='ct'>General</div><div style='font-size:13px'>
247
+ Name: {Esc(Str(s, "name"))}<br>Domain: {Esc(Str(s, "domain"))}<br>
248
+ Plan: {Badge(Str(s, "plan").ToUpper())}<br>
249
+ Subdomain: {Esc(Str(s, "subdomain"))}.agenticmail.cloud</div></div>"), "text/html");
250
+ });
251
+
252
+ // ─── Start ──────────────────────────────────────────────
253
+
254
+ var port = Environment.GetEnvironmentVariable("PORT") ?? "5002";
255
+ app.Urls.Add($"http://localhost:{port}");
256
+
257
+ Console.WriteLine($"\nšŸ¢ AgenticMail Enterprise Dashboard (.NET)");
258
+ Console.WriteLine($" API: {API_URL}");
259
+ Console.WriteLine($" Dashboard: http://localhost:{port}\n");
260
+
261
+ app.Run();