web-agent-bridge 2.3.0 → 2.3.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 (35) hide show
  1. package/package.json +12 -4
  2. package/public/commander-dashboard.html +243 -0
  3. package/public/css/premium.css +317 -317
  4. package/public/demo.html +259 -259
  5. package/public/index.html +644 -644
  6. package/public/mesh-dashboard.html +309 -382
  7. package/public/premium-dashboard.html +2487 -2487
  8. package/public/premium.html +791 -791
  9. package/public/script/wab.min.js +124 -87
  10. package/script/ai-agent-bridge.js +154 -84
  11. package/sdk/agent-mesh.js +287 -171
  12. package/sdk/commander.js +262 -0
  13. package/sdk/index.js +260 -260
  14. package/server/index.js +8 -1
  15. package/server/migrations/002_premium_features.sql +418 -418
  16. package/server/models/db.js +24 -5
  17. package/server/routes/admin-premium.js +671 -671
  18. package/server/routes/commander.js +316 -0
  19. package/server/routes/mesh.js +370 -201
  20. package/server/routes/premium-v2.js +686 -686
  21. package/server/routes/premium.js +724 -724
  22. package/server/services/agent-learning.js +230 -77
  23. package/server/services/agent-memory.js +625 -625
  24. package/server/services/agent-mesh.js +260 -67
  25. package/server/services/agent-symphony.js +548 -518
  26. package/server/services/commander.js +738 -0
  27. package/server/services/edge-compute.js +440 -0
  28. package/server/services/local-ai.js +389 -0
  29. package/server/services/plugins.js +747 -747
  30. package/server/services/self-healing.js +843 -843
  31. package/server/services/swarm.js +788 -788
  32. package/server/services/vision.js +871 -871
  33. package/public/admin/dashboard.html +0 -848
  34. package/public/admin/login.html +0 -84
  35. package/public/video/tutorial.mp4 +0 -0
@@ -1,848 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Admin Panel — Web Agent Bridge</title>
7
- <script>
8
- (function () {
9
- try {
10
- if (!localStorage.getItem('wab_admin_token')) {
11
- window.location.replace('/admin/login');
12
- }
13
- } catch (e) {
14
- window.location.replace('/admin/login');
15
- }
16
- })();
17
- </script>
18
- <link rel="preconnect" href="https://fonts.googleapis.com">
19
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
20
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
21
- <link rel="stylesheet" href="/css/styles.css">
22
- </head>
23
- <body>
24
- <div class="dashboard">
25
-
26
- <!-- ═══════════ SIDEBAR ═══════════ -->
27
- <aside class="sidebar">
28
- <div class="sidebar-brand">
29
- <a href="/" class="navbar-brand">
30
- <div class="brand-icon" style="background:linear-gradient(135deg,#ef4444,#f59e0b);">🛡️</div>
31
- <span>WAB Admin</span>
32
- </a>
33
- </div>
34
- <nav class="sidebar-nav">
35
- <a href="#" class="active" data-view="overview">📊 Overview</a>
36
- <a href="#" data-view="users">👥 Users</a>
37
- <a href="#" data-view="sites">🌐 Sites</a>
38
- <a href="#" data-view="analytics">📈 Platform analytics</a>
39
- <a href="#" data-view="grants">🎁 Free Grants</a>
40
- <a href="#" data-view="payments">💳 Payments</a>
41
- <a href="#" data-view="stripe">🔧 Stripe Config</a>
42
- <a href="#" data-view="smtp">📧 SMTP / Email</a>
43
- <a href="#" data-view="notifications">🔔 Notifications</a>
44
- </nav>
45
- <div class="sidebar-footer">
46
- <div style="font-size:0.85rem;color:var(--text-muted);margin-bottom:8px;" id="adminName"></div>
47
- <button class="btn btn-ghost btn-sm" onclick="logout()" style="width:100%;justify-content:flex-start;">🚪 Sign Out</button>
48
- </div>
49
- </aside>
50
-
51
- <!-- ═══════════ MAIN CONTENT ═══════════ -->
52
- <main class="main-content">
53
-
54
- <!-- ── Overview ── -->
55
- <div id="view-overview" class="view active">
56
- <div class="page-header"><h1>Admin Dashboard</h1></div>
57
- <div class="stats-grid">
58
- <div class="stat-card"><div class="label">Total Users</div><div class="value" id="statUsers">0</div></div>
59
- <div class="stat-card"><div class="label">Active Sites</div><div class="value" id="statSites">0</div></div>
60
- <div class="stat-card"><div class="label">Total Actions</div><div class="value" id="statAnalytics">0</div></div>
61
- <div class="stat-card"><div class="label">Today Actions</div><div class="value" id="statToday">0</div></div>
62
- <div class="stat-card"><div class="label">Revenue (USD)</div><div class="value" id="statRevenue">$0</div></div>
63
- <div class="stat-card"><div class="label">Active Grants</div><div class="value" id="statGrants">0</div></div>
64
- <div class="stat-card"><div class="label">Monthly Signups</div><div class="value" id="statSignups">0</div></div>
65
- <div class="stat-card"><div class="label">Stripe</div><div class="value" id="statStripe" style="font-size:1.2rem;">—</div></div>
66
- </div>
67
- <div class="grid-2" style="margin-top:24px;">
68
- <div class="card">
69
- <h3 style="margin-bottom:16px;">Tier Breakdown</h3>
70
- <div id="tierBreakdown"></div>
71
- </div>
72
- <div class="card">
73
- <h3 style="margin-bottom:16px;">Recent Users</h3>
74
- <div id="recentUsers" style="max-height:300px;overflow-y:auto;"></div>
75
- </div>
76
- </div>
77
- </div>
78
-
79
- <!-- ── Users ── -->
80
- <div id="view-users" class="view">
81
- <div class="page-header">
82
- <h1>Users Management</h1>
83
- <input type="text" class="form-input" placeholder="Search users..." id="userSearch" oninput="filterUsers()" style="width:260px;">
84
- </div>
85
- <div class="table-wrapper">
86
- <table>
87
- <thead><tr><th>Name</th><th>Email</th><th>Company</th><th>Registered</th><th>Actions</th></tr></thead>
88
- <tbody id="usersTableBody"></tbody>
89
- </table>
90
- </div>
91
- </div>
92
-
93
- <!-- ── User Detail Modal ── -->
94
- <div class="modal-overlay" id="userDetailModal">
95
- <div class="modal" style="max-width:750px;">
96
- <div class="modal-header">
97
- <h2 id="userDetailName">User Details</h2>
98
- <button class="modal-close" onclick="closeModal('userDetailModal')">&times;</button>
99
- </div>
100
- <div class="modal-body" id="userDetailContent"></div>
101
- </div>
102
- </div>
103
-
104
- <!-- ── Sites ── -->
105
- <div id="view-sites" class="view">
106
- <div class="page-header"><h1>All Sites</h1></div>
107
- <div class="table-wrapper">
108
- <table>
109
- <thead><tr><th>Name</th><th>Domain</th><th>Owner</th><th>Tier</th><th>License</th><th>Status</th><th>Admin</th></tr></thead>
110
- <tbody id="sitesTableBody"></tbody>
111
- </table>
112
- </div>
113
- </div>
114
-
115
- <!-- ── Analytics ── -->
116
- <div id="view-analytics" class="view">
117
- <div class="page-header">
118
- <h1>Platform analytics</h1>
119
- <select class="form-input" style="width:auto;" id="analyticsDays" onchange="loadAnalytics()">
120
- <option value="7">Last 7 days</option>
121
- <option value="30" selected>Last 30 days</option>
122
- <option value="90">Last 90 days</option>
123
- </select>
124
- </div>
125
- <p style="color:var(--text-secondary);max-width:800px;margin-bottom:20px;line-height:1.6;">
126
- Aggregated events from all sites whose bridge script reports usage (<code>POST /api/license/track</code>).
127
- Per-site charts are available under <strong>All Sites</strong> → <strong>Analytics</strong> on each row.
128
- Ensure embedded sites use a valid <strong>license key</strong> and (for cross-domain scripts) CORS allows your API origin.
129
- </p>
130
- <div id="analyticsContent"></div>
131
- </div>
132
-
133
- <!-- ── Free Grants ── -->
134
- <div id="view-grants" class="view">
135
- <div class="page-header">
136
- <h1>Free Grants</h1>
137
- <button class="btn btn-primary btn-sm" onclick="showGrantModal()">+ New Grant</button>
138
- </div>
139
- <p style="color:var(--text-secondary);margin-bottom:20px;">Grant free premium access to users for marketing, testing, or support purposes.</p>
140
- <div class="table-wrapper">
141
- <table>
142
- <thead><tr><th>User</th><th>Tier</th><th>Reason</th><th>Granted By</th><th>Date</th><th>Expires</th><th>Actions</th></tr></thead>
143
- <tbody id="grantsTableBody"></tbody>
144
- </table>
145
- </div>
146
- </div>
147
-
148
- <!-- ── Grant Modal ── -->
149
- <div class="modal-overlay" id="grantModal">
150
- <div class="modal">
151
- <div class="modal-header">
152
- <h2>Grant Free Tier</h2>
153
- <button class="modal-close" onclick="closeModal('grantModal')">&times;</button>
154
- </div>
155
- <div class="modal-body">
156
- <div class="alert alert-error" id="grantError"></div>
157
- <div class="form-group">
158
- <label>User (select)</label>
159
- <select class="form-input" id="grantUserId"></select>
160
- </div>
161
- <div class="form-group">
162
- <label>Tier</label>
163
- <select class="form-input" id="grantTier">
164
- <option value="starter">Starter</option>
165
- <option value="pro">Pro</option>
166
- <option value="enterprise" selected>Enterprise</option>
167
- </select>
168
- </div>
169
- <div class="form-group">
170
- <label>Reason</label>
171
- <input type="text" class="form-input" id="grantReason" placeholder="Marketing, Beta tester, etc.">
172
- </div>
173
- <div class="form-group">
174
- <label>Expires (optional)</label>
175
- <input type="date" class="form-input" id="grantExpires">
176
- </div>
177
- </div>
178
- <div class="modal-footer">
179
- <button class="btn btn-secondary" onclick="closeModal('grantModal')">Cancel</button>
180
- <button class="btn btn-primary" onclick="createGrant()">Grant Access</button>
181
- </div>
182
- </div>
183
- </div>
184
-
185
- <!-- ── Site analytics modal ── -->
186
- <div class="modal-overlay" id="siteAnalyticsModal">
187
- <div class="modal" style="max-width:720px;">
188
- <div class="modal-header">
189
- <h2 id="siteAnalyticsTitle">Site analytics</h2>
190
- <button class="modal-close" onclick="closeModal('siteAnalyticsModal')">&times;</button>
191
- </div>
192
- <div class="modal-body" id="siteAnalyticsBody"></div>
193
- </div>
194
- </div>
195
-
196
- <!-- ── Payments ── -->
197
- <div id="view-payments" class="view">
198
- <div class="page-header"><h1>Payment History</h1></div>
199
- <div class="table-wrapper">
200
- <table>
201
- <thead><tr><th>User</th><th>Amount</th><th>Currency</th><th>Status</th><th>Description</th><th>Date</th></tr></thead>
202
- <tbody id="paymentsTableBody"></tbody>
203
- </table>
204
- </div>
205
- </div>
206
-
207
- <!-- ── Stripe Config ── -->
208
- <div id="view-stripe" class="view">
209
- <div class="page-header"><h1>Stripe Configuration</h1></div>
210
- <div class="card" style="max-width:700px;">
211
- <div class="alert alert-info" style="display:block;margin-bottom:20px;background:var(--bg-card);border:1px solid var(--accent-blue);border-radius:var(--radius-md);padding:16px;">
212
- <strong>💡 Setup Guide:</strong> Create products and prices in your <a href="https://dashboard.stripe.com" target="_blank">Stripe Dashboard</a>, then enter the keys below.
213
- </div>
214
- <div class="form-group">
215
- <label>Publishable Key</label>
216
- <input type="text" class="form-input" id="stripePublishable" placeholder="pk_live_...">
217
- </div>
218
- <div class="form-group">
219
- <label>Secret Key</label>
220
- <input type="password" class="form-input" id="stripeSecret" placeholder="sk_live_...">
221
- </div>
222
- <div class="form-group">
223
- <label>Webhook Secret</label>
224
- <input type="password" class="form-input" id="stripeWebhook" placeholder="whsec_...">
225
- </div>
226
- <hr style="border-color:var(--border-color);margin:24px 0;">
227
- <h4 style="margin-bottom:16px;">Price IDs</h4>
228
- <div class="form-group">
229
- <label>Starter Price ID</label>
230
- <input type="text" class="form-input" id="priceStarter" placeholder="price_...">
231
- </div>
232
- <div class="form-group">
233
- <label>Pro Price ID</label>
234
- <input type="text" class="form-input" id="pricePro" placeholder="price_...">
235
- </div>
236
- <div class="form-group">
237
- <label>Enterprise Price ID</label>
238
- <input type="text" class="form-input" id="priceEnterprise" placeholder="price_...">
239
- </div>
240
- <button class="btn btn-primary" onclick="saveStripeConfig()">Save Stripe Configuration</button>
241
- </div>
242
- </div>
243
-
244
- <!-- ── SMTP Config ── -->
245
- <div id="view-smtp" class="view">
246
- <div class="page-header"><h1>SMTP / Email Settings</h1></div>
247
- <div class="card" style="max-width:700px;">
248
- <div class="form-group">
249
- <label>SMTP Host</label>
250
- <input type="text" class="form-input" id="smtpHost" placeholder="smtp.gmail.com">
251
- </div>
252
- <div class="form-group">
253
- <label>Port</label>
254
- <input type="number" class="form-input" id="smtpPort" placeholder="587" value="587">
255
- </div>
256
- <div class="form-group">
257
- <label style="display:flex;align-items:center;gap:8px;">
258
- <input type="checkbox" id="smtpSecure"> Use SSL/TLS
259
- </label>
260
- </div>
261
- <div class="form-group">
262
- <label>Username / Email</label>
263
- <input type="text" class="form-input" id="smtpUsername" placeholder="your@email.com">
264
- </div>
265
- <div class="form-group">
266
- <label>Password / App Password</label>
267
- <input type="password" class="form-input" id="smtpPassword" placeholder="App-specific password">
268
- </div>
269
- <hr style="border-color:var(--border-color);margin:24px 0;">
270
- <div class="form-group">
271
- <label>From Name</label>
272
- <input type="text" class="form-input" id="smtpFromName" placeholder="Web Agent Bridge" value="Web Agent Bridge">
273
- </div>
274
- <div class="form-group">
275
- <label>From Email</label>
276
- <input type="text" class="form-input" id="smtpFromEmail" placeholder="noreply@webagentbridge.com">
277
- </div>
278
- <div class="form-group">
279
- <label style="display:flex;align-items:center;gap:8px;">
280
- <input type="checkbox" id="smtpEnabled"> Enable Email Notifications
281
- </label>
282
- </div>
283
- <div style="display:flex;gap:12px;margin-top:20px;">
284
- <button class="btn btn-primary" onclick="saveSmtpConfig()">Save SMTP Settings</button>
285
- <button class="btn btn-secondary" onclick="testSmtp()">Send Test Email</button>
286
- </div>
287
- </div>
288
- </div>
289
-
290
- <!-- ── Notifications Log ── -->
291
- <div id="view-notifications" class="view">
292
- <div class="page-header"><h1>Notification Logs</h1></div>
293
- <div class="table-wrapper">
294
- <table>
295
- <thead><tr><th>To</th><th>Template</th><th>Subject</th><th>Status</th><th>Error</th><th>Date</th></tr></thead>
296
- <tbody id="notificationsTableBody"></tbody>
297
- </table>
298
- </div>
299
- </div>
300
-
301
- </main>
302
- </div>
303
-
304
- <script>
305
- // ─── State ──────────────────────────────────────────────────────────
306
- const API = '/api/admin';
307
- let token = localStorage.getItem('wab_admin_token');
308
- let admin = JSON.parse(localStorage.getItem('wab_admin') || 'null');
309
- let allUsers = [];
310
-
311
- if (!token) window.location.href = '/admin/login';
312
-
313
- function headers() {
314
- return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` };
315
- }
316
-
317
- function logout() {
318
- localStorage.removeItem('wab_admin_token');
319
- localStorage.removeItem('wab_admin');
320
- window.location.href = '/admin/login';
321
- }
322
-
323
- function esc(str) {
324
- if (!str) return '';
325
- const d = document.createElement('div');
326
- d.textContent = str;
327
- return d.innerHTML;
328
- }
329
-
330
- function openModal(id) { document.getElementById(id).classList.add('active'); }
331
- function closeModal(id) { document.getElementById(id).classList.remove('active'); }
332
-
333
- // ─── Navigation ─────────────────────────────────────────────────────
334
- document.querySelectorAll('.sidebar-nav a[data-view]').forEach(link => {
335
- link.addEventListener('click', (e) => {
336
- e.preventDefault();
337
- const view = link.dataset.view;
338
- document.querySelectorAll('.sidebar-nav a').forEach(a => a.classList.remove('active'));
339
- link.classList.add('active');
340
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
341
- document.getElementById(`view-${view}`).classList.add('active');
342
- loadView(view);
343
- });
344
- });
345
-
346
- function loadView(view) {
347
- switch(view) {
348
- case 'overview': loadStats(); break;
349
- case 'users': loadUsers(); break;
350
- case 'sites': loadSites(); break;
351
- case 'analytics': loadAnalytics(); break;
352
- case 'grants': loadGrants(); break;
353
- case 'payments': loadPayments(); break;
354
- case 'stripe': loadStripeConfig(); break;
355
- case 'smtp': loadSmtpConfig(); break;
356
- case 'notifications': loadNotifications(); break;
357
- }
358
- }
359
-
360
- // ─── Init ───────────────────────────────────────────────────────────
361
- async function init() {
362
- if (admin) document.getElementById('adminName').textContent = admin.name;
363
- await loadStats();
364
- }
365
-
366
- // ─── Stats ──────────────────────────────────────────────────────────
367
- async function loadStats() {
368
- try {
369
- const res = await fetch(`${API}/stats`, { headers: headers() });
370
- if (res.status === 401 || res.status === 403) return logout();
371
- const s = await res.json();
372
- document.getElementById('statUsers').textContent = s.totalUsers;
373
- document.getElementById('statSites').textContent = s.totalSites;
374
- document.getElementById('statAnalytics').textContent = s.totalAnalytics.toLocaleString();
375
- document.getElementById('statToday').textContent = s.todayAnalytics;
376
- document.getElementById('statRevenue').textContent = '$' + (s.totalRevenue / 100).toFixed(2);
377
- document.getElementById('statGrants').textContent = s.activeGrants;
378
- document.getElementById('statSignups').textContent = s.monthlySignups;
379
- document.getElementById('statStripe').textContent = s.stripeConfigured ? '✅ Active' : '❌ Not Set';
380
-
381
- // Tier breakdown
382
- const tb = document.getElementById('tierBreakdown');
383
- if (s.tierBreakdown && s.tierBreakdown.length) {
384
- tb.innerHTML = s.tierBreakdown.map(t => `
385
- <div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border-color);">
386
- <span class="badge badge-${t.tier}">${t.tier}</span>
387
- <strong>${t.count}</strong>
388
- </div>
389
- `).join('');
390
- } else {
391
- tb.innerHTML = '<p style="color:var(--text-muted);">No sites yet</p>';
392
- }
393
-
394
- // Recent users
395
- const ru = document.getElementById('recentUsers');
396
- if (s.recentUsers && s.recentUsers.length) {
397
- ru.innerHTML = s.recentUsers.map(u => `
398
- <div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border-color);">
399
- <div>
400
- <strong style="color:var(--text-primary);">${esc(u.name)}</strong>
401
- <div style="font-size:0.8rem;color:var(--text-muted);">${esc(u.email)}</div>
402
- </div>
403
- <span style="font-size:0.75rem;color:var(--text-muted);">${new Date(u.created_at).toLocaleDateString()}</span>
404
- </div>
405
- `).join('');
406
- } else {
407
- ru.innerHTML = '<p style="color:var(--text-muted);">No users yet</p>';
408
- }
409
- } catch (err) { console.error(err); }
410
- }
411
-
412
- // ─── Users ──────────────────────────────────────────────────────────
413
- async function loadUsers() {
414
- try {
415
- const res = await fetch(`${API}/users`, { headers: headers() });
416
- const data = await res.json();
417
- allUsers = data.users || [];
418
- renderUsers(allUsers);
419
- } catch (err) { console.error(err); }
420
- }
421
-
422
- function filterUsers() {
423
- const q = document.getElementById('userSearch').value.toLowerCase();
424
- renderUsers(allUsers.filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q) || (u.company || '').toLowerCase().includes(q)));
425
- }
426
-
427
- function renderUsers(users) {
428
- const tbody = document.getElementById('usersTableBody');
429
- if (!users.length) {
430
- tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-muted);">No users found</td></tr>';
431
- return;
432
- }
433
- tbody.innerHTML = users.map(u => `
434
- <tr>
435
- <td><strong style="color:var(--text-primary);">${esc(u.name)}</strong></td>
436
- <td style="font-size:0.85rem;">${esc(u.email)}</td>
437
- <td style="color:var(--text-muted);">${esc(u.company) || '—'}</td>
438
- <td style="font-size:0.8rem;color:var(--text-muted);">${new Date(u.created_at).toLocaleDateString()}</td>
439
- <td>
440
- <button class="btn btn-secondary btn-sm" onclick="viewUser('${u.id}')">View</button>
441
- <button class="btn btn-danger btn-sm" onclick="deleteUser('${u.id}','${esc(u.name)}')">Delete</button>
442
- </td>
443
- </tr>
444
- `).join('');
445
- }
446
-
447
- async function viewUser(id) {
448
- try {
449
- const res = await fetch(`${API}/users/${id}`, { headers: headers() });
450
- const { user } = await res.json();
451
- document.getElementById('userDetailName').textContent = user.name;
452
- document.getElementById('userDetailContent').innerHTML = `
453
- <div class="grid-2" style="gap:20px;">
454
- <div>
455
- <h4 style="margin-bottom:12px;">Profile</h4>
456
- <p><strong>Email:</strong> ${esc(user.email)}</p>
457
- <p><strong>Company:</strong> ${esc(user.company) || '—'}</p>
458
- <p><strong>Registered:</strong> ${new Date(user.created_at).toLocaleString()}</p>
459
- ${user.stripeCustomer ? `<p><strong>Stripe ID:</strong> <span class="mono" style="font-size:0.8rem;">${esc(user.stripeCustomer.stripe_customer_id)}</span></p>` : ''}
460
- </div>
461
- <div>
462
- <h4 style="margin-bottom:12px;">Quick Actions</h4>
463
- <div style="display:flex;flex-direction:column;gap:8px;">
464
- <button class="btn btn-primary btn-sm" onclick="quickGrant('${user.id}','${esc(user.name)}')">🎁 Grant Free Tier</button>
465
- <button class="btn btn-secondary btn-sm" onclick="quickChangeTier('${user.id}')">🔄 Change Tier</button>
466
- </div>
467
- </div>
468
- </div>
469
- <h4 style="margin:24px 0 12px;">Sites (${user.sites ? user.sites.length : 0})</h4>
470
- ${user.sites && user.sites.length ? `
471
- <div class="table-wrapper">
472
- <table>
473
- <thead><tr><th>Name</th><th>Domain</th><th>Tier</th><th>Active</th></tr></thead>
474
- <tbody>${user.sites.map(s => `
475
- <tr>
476
- <td>${esc(s.name)}</td>
477
- <td class="mono" style="font-size:0.85rem;">${esc(s.domain)}</td>
478
- <td><span class="badge badge-${s.tier}">${s.tier}</span></td>
479
- <td>${s.active ? '<span style="color:var(--accent-green);">✓</span>' : '<span style="color:var(--accent-red);">✗</span>'}</td>
480
- </tr>
481
- `).join('')}</tbody>
482
- </table>
483
- </div>
484
- ` : '<p style="color:var(--text-muted);">No sites</p>'}
485
- ${user.grants && user.grants.length ? `
486
- <h4 style="margin:24px 0 12px;">Active Grants</h4>
487
- ${user.grants.map(g => `<div style="padding:8px;background:var(--bg-surface);border-radius:var(--radius-sm);margin-bottom:6px;"><span class="badge badge-${g.granted_tier}">${g.granted_tier}</span> — ${esc(g.reason) || 'No reason'}</div>`).join('')}
488
- ` : ''}
489
- ${user.payments && user.payments.length ? `
490
- <h4 style="margin:24px 0 12px;">Payments</h4>
491
- ${user.payments.map(p => `<div style="padding:8px;background:var(--bg-surface);border-radius:var(--radius-sm);margin-bottom:6px;">$${(p.amount / 100).toFixed(2)} ${p.currency.toUpperCase()} — ${esc(p.description) || ''} — ${new Date(p.created_at).toLocaleDateString()}</div>`).join('')}
492
- ` : ''}
493
- `;
494
- openModal('userDetailModal');
495
- } catch (err) { console.error(err); }
496
- }
497
-
498
- function quickGrant(userId, userName) {
499
- closeModal('userDetailModal');
500
- document.getElementById('grantUserId').value = userId;
501
- showGrantModal();
502
- }
503
-
504
- async function quickChangeTier(userId) {
505
- const tier = prompt('Enter new tier (free, starter, pro, enterprise):');
506
- if (!tier || !['free', 'starter', 'pro', 'enterprise'].includes(tier)) return;
507
- try {
508
- await fetch(`${API}/users/${userId}/tier`, { method: 'PUT', headers: headers(), body: JSON.stringify({ tier }) });
509
- alert('Tier updated!');
510
- viewUser(userId);
511
- } catch (err) { alert('Failed'); }
512
- }
513
-
514
- async function deleteUser(id, name) {
515
- if (!confirm(`Delete user "${name}"? This will deactivate all their sites. This action cannot be undone.`)) return;
516
- try {
517
- await fetch(`${API}/users/${id}`, { method: 'DELETE', headers: headers() });
518
- loadUsers();
519
- } catch (err) { alert('Failed to delete user'); }
520
- }
521
-
522
- // ─── Sites ──────────────────────────────────────────────────────────
523
- async function loadSites() {
524
- try {
525
- const res = await fetch(`${API}/sites`, { headers: headers() });
526
- const { sites } = await res.json();
527
- const tbody = document.getElementById('sitesTableBody');
528
- if (!sites.length) {
529
- tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-muted);">No sites</td></tr>';
530
- return;
531
- }
532
- tbody.innerHTML = sites.map(s => `
533
- <tr>
534
- <td><strong style="color:var(--text-primary);">${esc(s.name)}</strong></td>
535
- <td class="mono" style="font-size:0.85rem;">${esc(s.domain)}</td>
536
- <td style="font-size:0.85rem;">${esc(s.user_name)} <span style="color:var(--text-muted);">(${esc(s.user_email)})</span></td>
537
- <td><span class="badge badge-${s.tier}">${s.tier}</span></td>
538
- <td class="mono" style="font-size:0.75rem;color:var(--text-muted);">${esc(s.license_key)}</td>
539
- <td>${s.active ? '<span style="color:var(--accent-green);">Active</span>' : '<span style="color:var(--accent-red);">Inactive</span>'}</td>
540
- <td style="white-space:nowrap;">
541
- <button type="button" class="btn btn-secondary btn-sm" onclick="adminSiteTier('${s.id}')">Tier</button>
542
- <button type="button" class="btn btn-secondary btn-sm" onclick="adminSiteAnalytics('${s.id}')">Analytics</button>
543
- <button type="button" class="btn btn-danger btn-sm" onclick="adminSiteToggle('${s.id}', ${s.active ? 'true' : 'false'})">${s.active ? 'Deactivate' : 'Activate'}</button>
544
- </td>
545
- </tr>
546
- `).join('');
547
- } catch (err) { console.error(err); }
548
- }
549
-
550
- async function adminSiteTier(siteId) {
551
- const tier = prompt('New tier (free, starter, pro, enterprise):');
552
- if (!tier || !['free', 'starter', 'pro', 'enterprise'].includes(tier)) return;
553
- try {
554
- const res = await fetch(`${API}/sites/${siteId}`, { method: 'PUT', headers: headers(), body: JSON.stringify({ tier }) });
555
- if (!res.ok) { alert('Failed to update tier'); return; }
556
- loadSites();
557
- } catch (e) { alert('Error'); }
558
- }
559
-
560
- async function adminSiteToggle(siteId, currentlyActive) {
561
- const cur = currentlyActive === true || currentlyActive === 'true';
562
- const nextActive = !cur;
563
- if (!confirm(nextActive ? 'Activate this site?' : 'Deactivate this site? Embedded keys will stop working until reactivated.')) return;
564
- try {
565
- const res = await fetch(`${API}/sites/${siteId}`, { method: 'PUT', headers: headers(), body: JSON.stringify({ active: nextActive }) });
566
- if (!res.ok) { alert('Failed'); return; }
567
- loadSites();
568
- } catch (e) { alert('Error'); }
569
- }
570
-
571
- async function adminSiteAnalytics(siteId) {
572
- try {
573
- const res = await fetch(`${API}/sites/${siteId}/analytics?days=30`, { headers: headers() });
574
- if (!res.ok) { alert('Failed to load analytics'); return; }
575
- const data = await res.json();
576
- const site = data.site || {};
577
- document.getElementById('siteAnalyticsTitle').textContent = (site.name || 'Site') + ' — analytics';
578
- const sum = data.summary || [];
579
- const total = sum.reduce((a, r) => a + (r.count || 0), 0);
580
- let html = `<p style="color:var(--text-secondary);margin-bottom:12px;"><strong>Domain:</strong> ${esc(site.domain || '')} · <strong>Total events:</strong> ${total}</p>`;
581
- if (sum.length) {
582
- html += '<div class="table-wrapper"><table><thead><tr><th>Action</th><th>Type</th><th>Calls</th><th>OK</th></tr></thead><tbody>';
583
- html += sum.map(r => `<tr><td>${esc(r.action_name)}</td><td>${esc(r.trigger_type || '—')}</td><td>${r.count}</td><td>${r.successes}</td></tr>`).join('');
584
- html += '</tbody></table></div>';
585
- } else {
586
- html += '<p style="color:var(--text-muted);">No events yet. Bridge must call <code>/api/license/track</code> after actions (enabled by default in the script).</p>';
587
- }
588
- document.getElementById('siteAnalyticsBody').innerHTML = html;
589
- openModal('siteAnalyticsModal');
590
- } catch (e) {
591
- alert('Error loading analytics');
592
- }
593
- }
594
-
595
- // ─── Analytics ──────────────────────────────────────────────────────
596
- async function loadAnalytics() {
597
- const days = document.getElementById('analyticsDays').value;
598
- try {
599
- const res = await fetch(`${API}/analytics?days=${days}`, { headers: headers() });
600
- const data = await res.json();
601
- const c = document.getElementById('analyticsContent');
602
-
603
- let html = '<div class="grid-2" style="gap:20px;">';
604
-
605
- // Timeline
606
- html += '<div class="card"><h4 style="margin-bottom:16px;">Actions Timeline</h4>';
607
- if (data.timeline && data.timeline.length) {
608
- const maxCount = Math.max(...data.timeline.map(t => t.count));
609
- html += data.timeline.map(t => `
610
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
611
- <span style="font-size:0.75rem;color:var(--text-muted);width:70px;">${t.day.slice(5)}</span>
612
- <div style="flex:1;background:var(--bg-surface);border-radius:4px;height:20px;overflow:hidden;">
613
- <div style="width:${(t.count / maxCount * 100)}%;height:100%;background:var(--gradient-primary);border-radius:4px;"></div>
614
- </div>
615
- <span style="font-size:0.75rem;width:35px;text-align:right;">${t.count}</span>
616
- </div>
617
- `).join('');
618
- } else { html += '<p style="color:var(--text-muted);">No data</p>'; }
619
- html += '</div>';
620
-
621
- // Top Actions
622
- html += '<div class="card"><h4 style="margin-bottom:16px;">Top Actions</h4>';
623
- if (data.topActions && data.topActions.length) {
624
- html += data.topActions.map((a, i) => `
625
- <div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border-color);">
626
- <span>${i + 1}. ${esc(a.action_name)}</span>
627
- <strong>${a.count}</strong>
628
- </div>
629
- `).join('');
630
- } else { html += '<p style="color:var(--text-muted);">No data</p>'; }
631
- html += '</div>';
632
-
633
- // Signups
634
- html += '<div class="card" style="grid-column:1/-1;"><h4 style="margin-bottom:16px;">User Signups</h4>';
635
- if (data.signups && data.signups.length) {
636
- const maxS = Math.max(...data.signups.map(s => s.count));
637
- html += '<div style="display:flex;gap:4px;align-items:flex-end;height:120px;">';
638
- html += data.signups.map(s => `
639
- <div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;">
640
- <span style="font-size:0.7rem;">${s.count}</span>
641
- <div style="width:100%;height:${(s.count / maxS * 80)}px;background:var(--gradient-secondary);border-radius:4px 4px 0 0;min-height:4px;"></div>
642
- <span style="font-size:0.65rem;color:var(--text-muted);">${s.day.slice(5)}</span>
643
- </div>
644
- `).join('');
645
- html += '</div>';
646
- } else { html += '<p style="color:var(--text-muted);">No signups in this period</p>'; }
647
- html += '</div></div>';
648
-
649
- c.innerHTML = html;
650
- } catch (err) { console.error(err); }
651
- }
652
-
653
- // ─── Free Grants ────────────────────────────────────────────────────
654
- async function loadGrants() {
655
- try {
656
- const res = await fetch(`${API}/grants`, { headers: headers() });
657
- const { grants } = await res.json();
658
- const tbody = document.getElementById('grantsTableBody');
659
- if (!grants.length) {
660
- tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-muted);">No active grants</td></tr>';
661
- return;
662
- }
663
- tbody.innerHTML = grants.map(g => `
664
- <tr>
665
- <td>${esc(g.user_name)} <span style="color:var(--text-muted);font-size:0.8rem;">(${esc(g.user_email)})</span></td>
666
- <td><span class="badge badge-${g.granted_tier}">${g.granted_tier}</span></td>
667
- <td style="color:var(--text-secondary);">${esc(g.reason) || '—'}</td>
668
- <td style="font-size:0.85rem;">${esc(g.admin_name) || '—'}</td>
669
- <td style="font-size:0.8rem;color:var(--text-muted);">${new Date(g.granted_at).toLocaleDateString()}</td>
670
- <td style="font-size:0.8rem;color:var(--text-muted);">${g.expires_at ? new Date(g.expires_at).toLocaleDateString() : 'Never'}</td>
671
- <td><button class="btn btn-danger btn-sm" onclick="revokeGrant('${g.id}')">Revoke</button></td>
672
- </tr>
673
- `).join('');
674
- } catch (err) { console.error(err); }
675
- }
676
-
677
- function showGrantModal() {
678
- // Populate user select
679
- const sel = document.getElementById('grantUserId');
680
- sel.innerHTML = allUsers.map(u => `<option value="${u.id}">${esc(u.name)} (${esc(u.email)})</option>`).join('');
681
- if (!allUsers.length) {
682
- fetch(`${API}/users`, { headers: headers() }).then(r => r.json()).then(data => {
683
- allUsers = data.users || [];
684
- sel.innerHTML = allUsers.map(u => `<option value="${u.id}">${esc(u.name)} (${esc(u.email)})</option>`).join('');
685
- });
686
- }
687
- openModal('grantModal');
688
- }
689
-
690
- async function createGrant() {
691
- const userId = document.getElementById('grantUserId').value;
692
- const tier = document.getElementById('grantTier').value;
693
- const reason = document.getElementById('grantReason').value;
694
- const expires = document.getElementById('grantExpires').value;
695
- const errEl = document.getElementById('grantError');
696
- errEl.style.display = 'none';
697
-
698
- if (!userId) { errEl.textContent = 'Select a user'; errEl.style.display = 'block'; return; }
699
-
700
- try {
701
- const res = await fetch(`${API}/grants`, {
702
- method: 'POST', headers: headers(),
703
- body: JSON.stringify({ userId, tier, reason, expiresAt: expires || null })
704
- });
705
- if (!res.ok) { const d = await res.json(); errEl.textContent = d.error; errEl.style.display = 'block'; return; }
706
- closeModal('grantModal');
707
- loadGrants();
708
- alert('Free grant created successfully! User has been notified via email.');
709
- } catch (err) { errEl.textContent = 'Failed'; errEl.style.display = 'block'; }
710
- }
711
-
712
- async function revokeGrant(id) {
713
- if (!confirm('Revoke this grant? The user will be downgraded to Free tier.')) return;
714
- try {
715
- await fetch(`${API}/grants/${id}`, { method: 'DELETE', headers: headers() });
716
- loadGrants();
717
- } catch (err) { alert('Failed'); }
718
- }
719
-
720
- // ─── Payments ───────────────────────────────────────────────────────
721
- async function loadPayments() {
722
- try {
723
- const res = await fetch(`${API}/payments`, { headers: headers() });
724
- const { payments } = await res.json();
725
- const tbody = document.getElementById('paymentsTableBody');
726
- if (!payments.length) {
727
- tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:40px;color:var(--text-muted);">No payments recorded</td></tr>';
728
- return;
729
- }
730
- tbody.innerHTML = payments.map(p => `
731
- <tr>
732
- <td>${esc(p.user_name)} <span style="color:var(--text-muted);font-size:0.8rem;">(${esc(p.user_email)})</span></td>
733
- <td><strong style="color:var(--accent-green);">$${(p.amount / 100).toFixed(2)}</strong></td>
734
- <td style="text-transform:uppercase;">${p.currency}</td>
735
- <td><span class="badge badge-${p.status === 'succeeded' ? 'active' : 'inactive'}">${p.status}</span></td>
736
- <td style="color:var(--text-secondary);">${esc(p.description) || '—'}</td>
737
- <td style="font-size:0.8rem;color:var(--text-muted);">${new Date(p.created_at).toLocaleDateString()}</td>
738
- </tr>
739
- `).join('');
740
- } catch (err) { console.error(err); }
741
- }
742
-
743
- // ─── Stripe Config ──────────────────────────────────────────────────
744
- async function loadStripeConfig() {
745
- try {
746
- const res = await fetch(`${API}/stripe/config`, { headers: headers() });
747
- const cfg = await res.json();
748
- document.getElementById('stripePublishable').value = cfg.publishableKey || '';
749
- document.getElementById('priceStarter').value = cfg.prices.starter || '';
750
- document.getElementById('pricePro').value = cfg.prices.pro || '';
751
- document.getElementById('priceEnterprise').value = cfg.prices.enterprise || '';
752
- } catch (err) { console.error(err); }
753
- }
754
-
755
- async function saveStripeConfig() {
756
- try {
757
- const body = {
758
- publishableKey: document.getElementById('stripePublishable').value,
759
- secretKey: document.getElementById('stripeSecret').value || undefined,
760
- webhookSecret: document.getElementById('stripeWebhook').value || undefined,
761
- priceStarter: document.getElementById('priceStarter').value,
762
- pricePro: document.getElementById('pricePro').value,
763
- priceEnterprise: document.getElementById('priceEnterprise').value
764
- };
765
- const res = await fetch(`${API}/stripe/config`, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
766
- if (res.ok) alert('Stripe configuration saved!');
767
- else alert('Failed to save');
768
- } catch (err) { alert('Error saving Stripe config'); }
769
- }
770
-
771
- // ─── SMTP Config ────────────────────────────────────────────────────
772
- async function loadSmtpConfig() {
773
- try {
774
- const res = await fetch(`${API}/smtp`, { headers: headers() });
775
- const { settings } = await res.json();
776
- if (settings) {
777
- document.getElementById('smtpHost').value = settings.host || '';
778
- document.getElementById('smtpPort').value = settings.port || 587;
779
- document.getElementById('smtpSecure').checked = !!settings.secure;
780
- document.getElementById('smtpUsername').value = settings.username || '';
781
- document.getElementById('smtpFromName').value = settings.from_name || 'Web Agent Bridge';
782
- document.getElementById('smtpFromEmail').value = settings.from_email || '';
783
- document.getElementById('smtpEnabled').checked = !!settings.enabled;
784
- }
785
- } catch (err) { console.error(err); }
786
- }
787
-
788
- async function saveSmtpConfig() {
789
- try {
790
- const body = {
791
- host: document.getElementById('smtpHost').value,
792
- port: parseInt(document.getElementById('smtpPort').value) || 587,
793
- secure: document.getElementById('smtpSecure').checked,
794
- username: document.getElementById('smtpUsername').value,
795
- password: document.getElementById('smtpPassword').value,
796
- fromName: document.getElementById('smtpFromName').value,
797
- fromEmail: document.getElementById('smtpFromEmail').value,
798
- enabled: document.getElementById('smtpEnabled').checked
799
- };
800
- const res = await fetch(`${API}/smtp`, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
801
- if (res.ok) alert('SMTP settings saved!');
802
- else { const d = await res.json(); alert(d.error || 'Failed'); }
803
- } catch (err) { alert('Error saving SMTP settings'); }
804
- }
805
-
806
- async function testSmtp() {
807
- const email = prompt('Enter email address to send test email:');
808
- if (!email) return;
809
- try {
810
- const res = await fetch(`${API}/smtp/test`, { method: 'POST', headers: headers(), body: JSON.stringify({ testEmail: email }) });
811
- const data = await res.json();
812
- alert(data.success ? 'Test email sent successfully!' : 'Failed: ' + (data.error || 'Unknown error'));
813
- } catch (err) { alert('Connection error'); }
814
- }
815
-
816
- // ─── Notifications ──────────────────────────────────────────────────
817
- async function loadNotifications() {
818
- try {
819
- const res = await fetch(`${API}/notifications`, { headers: headers() });
820
- const { logs } = await res.json();
821
- const tbody = document.getElementById('notificationsTableBody');
822
- if (!logs.length) {
823
- tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:40px;color:var(--text-muted);">No notifications sent yet</td></tr>';
824
- return;
825
- }
826
- tbody.innerHTML = logs.map(n => `
827
- <tr>
828
- <td style="font-size:0.85rem;">${esc(n.email_to)}</td>
829
- <td><span class="badge">${esc(n.template)}</span></td>
830
- <td style="font-size:0.85rem;color:var(--text-secondary);">${esc(n.subject)}</td>
831
- <td><span style="color:${n.status === 'sent' ? 'var(--accent-green)' : 'var(--accent-red)'};">${n.status}</span></td>
832
- <td style="font-size:0.8rem;color:var(--accent-red);">${esc(n.error_message) || '—'}</td>
833
- <td style="font-size:0.8rem;color:var(--text-muted);">${new Date(n.created_at).toLocaleString()}</td>
834
- </tr>
835
- `).join('');
836
- } catch (err) { console.error(err); }
837
- }
838
-
839
- // ─── Styles ─────────────────────────────────────────────────────────
840
- const style = document.createElement('style');
841
- style.textContent = '.view { display: none; } .view.active { display: block; }';
842
- document.head.appendChild(style);
843
-
844
- init();
845
- </script>
846
- <script src="/js/cookie-consent.js"></script>
847
- </body>
848
- </html>