web-agent-bridge 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +21 -21
  2. package/README.ar.md +446 -446
  3. package/README.md +780 -933
  4. package/bin/cli.js +80 -80
  5. package/bin/wab.js +80 -80
  6. package/examples/bidi-agent.js +119 -119
  7. package/examples/mcp-agent.js +94 -94
  8. package/examples/next-app-router/README.md +44 -0
  9. package/examples/puppeteer-agent.js +108 -108
  10. package/examples/saas-dashboard/README.md +55 -0
  11. package/examples/shopify-hydrogen/README.md +74 -0
  12. package/examples/vision-agent.js +171 -171
  13. package/examples/wordpress-elementor/README.md +77 -0
  14. package/package.json +69 -78
  15. package/public/.well-known/ai-assets.json +59 -0
  16. package/public/admin/login.html +84 -84
  17. package/public/ai.html +196 -0
  18. package/public/cookies.html +208 -208
  19. package/public/css/premium.css +317 -0
  20. package/public/css/styles.css +1235 -1235
  21. package/public/dashboard.html +704 -704
  22. package/public/demo.html +259 -0
  23. package/public/docs.html +585 -585
  24. package/public/feed.xml +89 -0
  25. package/public/index.html +495 -332
  26. package/public/js/auth-nav.js +31 -31
  27. package/public/js/auth-redirect.js +12 -12
  28. package/public/js/cookie-consent.js +56 -56
  29. package/public/js/wab-demo-page.js +721 -0
  30. package/public/js/ws-client.js +74 -74
  31. package/public/llms-full.txt +309 -0
  32. package/public/llms.txt +85 -0
  33. package/public/login.html +83 -83
  34. package/public/openapi.json +580 -0
  35. package/public/premium-dashboard.html +2487 -0
  36. package/public/premium.html +791 -0
  37. package/public/privacy.html +295 -295
  38. package/public/register.html +103 -103
  39. package/public/robots.txt +87 -0
  40. package/public/script/wab-consent.d.ts +36 -0
  41. package/public/script/wab-consent.js +104 -0
  42. package/public/script/wab-schema.js +131 -0
  43. package/public/script/wab.d.ts +108 -0
  44. package/public/script/wab.min.js +234 -0
  45. package/public/sitemap.xml +93 -0
  46. package/public/terms.html +254 -254
  47. package/public/video/tutorial.mp4 +0 -0
  48. package/script/ai-agent-bridge.js +1558 -1513
  49. package/sdk/README.md +55 -55
  50. package/sdk/index.d.ts +118 -0
  51. package/sdk/index.js +257 -203
  52. package/sdk/package.json +14 -14
  53. package/sdk/schema-discovery.js +83 -0
  54. package/server/config/secrets.js +94 -92
  55. package/server/index.js +0 -9
  56. package/server/middleware/adminAuth.js +30 -30
  57. package/server/middleware/auth.js +41 -41
  58. package/server/middleware/rateLimits.js +24 -24
  59. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  60. package/server/migrations/002_premium_features.sql +418 -0
  61. package/server/models/adapters/index.js +33 -33
  62. package/server/models/adapters/mysql.js +183 -183
  63. package/server/models/adapters/postgresql.js +172 -172
  64. package/server/models/adapters/sqlite.js +7 -7
  65. package/server/models/db.js +561 -561
  66. package/server/routes/admin-premium.js +671 -0
  67. package/server/routes/admin.js +247 -247
  68. package/server/routes/api.js +131 -138
  69. package/server/routes/auth.js +51 -51
  70. package/server/routes/billing.js +45 -45
  71. package/server/routes/discovery.js +406 -329
  72. package/server/routes/license.js +240 -240
  73. package/server/routes/noscript.js +543 -543
  74. package/server/routes/premium-v2.js +686 -0
  75. package/server/routes/premium.js +724 -0
  76. package/server/routes/wab-api.js +476 -476
  77. package/server/services/agent-memory.js +625 -0
  78. package/server/services/email.js +204 -204
  79. package/server/services/fairness.js +420 -420
  80. package/server/services/plugins.js +747 -0
  81. package/server/services/premium.js +1883 -0
  82. package/server/services/self-healing.js +843 -0
  83. package/server/services/stripe.js +192 -192
  84. package/server/services/swarm.js +788 -0
  85. package/server/services/vision.js +871 -0
  86. package/server/utils/cache.js +125 -125
  87. package/server/utils/migrate.js +81 -81
  88. package/server/utils/secureFields.js +50 -50
  89. package/server/ws.js +101 -101
  90. package/docs/DEPLOY.md +0 -118
  91. package/docs/SPEC.md +0 -1540
  92. package/wab-mcp-adapter/README.md +0 -136
  93. package/wab-mcp-adapter/index.js +0 -555
  94. package/wab-mcp-adapter/package.json +0 -17
@@ -0,0 +1,2487 @@
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>Premium Dashboard — Web Agent Bridge</title>
7
+ <script>
8
+ (function() {
9
+ try {
10
+ if (!localStorage.getItem('wab_token')) {
11
+ window.location.replace('/login?next=/premium-dashboard');
12
+ }
13
+ } catch(e) { window.location.replace('/login'); }
14
+ })();
15
+ </script>
16
+ <link rel="preconnect" href="https://fonts.googleapis.com">
17
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
18
+ <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">
19
+ <link rel="stylesheet" href="/css/styles.css">
20
+ <style>
21
+ .view { display: none; }
22
+ .view.active { display: block; }
23
+ .prem-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 24px; }
24
+ .prem-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 24px; }
25
+ .prem-grid-6 { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; margin-bottom: 24px; }
26
+ @media (max-width: 1200px) {
27
+ .prem-grid-3 { grid-template-columns: repeat(2, 1fr); }
28
+ .prem-grid-6 { grid-template-columns: repeat(3, 1fr); }
29
+ }
30
+ @media (max-width: 768px) {
31
+ .prem-grid-2, .prem-grid-3 { grid-template-columns: 1fr; }
32
+ .prem-grid-6 { grid-template-columns: repeat(2, 1fr); }
33
+ }
34
+ .section-title { font-size: 1.1rem; font-weight: 700; margin: 28px 0 16px; color: var(--text-primary); }
35
+ .section-title:first-child { margin-top: 0; }
36
+ .site-select-bar { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
37
+ .site-select-bar label { font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); white-space: nowrap; }
38
+ .site-select-bar select { width: auto; min-width: 240px; }
39
+ .empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
40
+ .empty-state .icon { font-size: 2.5rem; margin-bottom: 12px; }
41
+ .actions-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; }
42
+ .sev-critical { background: rgba(239,68,68,0.15); color: var(--accent-red); }
43
+ .sev-high { background: rgba(245,158,11,0.15); color: var(--accent-orange); }
44
+ .sev-medium { background: rgba(234,179,8,0.15); color: #eab308; }
45
+ .sev-low { background: rgba(59,130,246,0.15); color: var(--accent-blue); }
46
+ .type-friendly { background: rgba(16,185,129,0.15); color: var(--accent-green); }
47
+ .type-suspicious { background: rgba(245,158,11,0.15); color: var(--accent-orange); }
48
+ .type-aggressive { background: rgba(239,68,68,0.15); color: var(--accent-red); }
49
+ .type-unknown { background: rgba(100,116,139,0.15); color: var(--text-muted); }
50
+ .status-open { background: rgba(59,130,246,0.15); color: var(--accent-blue); }
51
+ .status-in_progress { background: rgba(245,158,11,0.15); color: var(--accent-orange); }
52
+ .status-waiting { background: rgba(139,92,246,0.15); color: var(--accent-purple); }
53
+ .status-resolved { background: rgba(16,185,129,0.15); color: var(--accent-green); }
54
+ .status-closed { background: rgba(100,116,139,0.15); color: var(--text-muted); }
55
+ .status-active { background: rgba(16,185,129,0.15); color: var(--accent-green); }
56
+ .status-paused { background: rgba(245,158,11,0.15); color: var(--accent-orange); }
57
+ .status-running { background: rgba(59,130,246,0.15); color: var(--accent-blue); }
58
+ .status-success { background: rgba(16,185,129,0.15); color: var(--accent-green); }
59
+ .status-failed { background: rgba(239,68,68,0.15); color: var(--accent-red); }
60
+ .priority-low { background: rgba(100,116,139,0.15); color: var(--text-muted); }
61
+ .priority-normal { background: rgba(59,130,246,0.15); color: var(--accent-blue); }
62
+ .priority-high { background: rgba(245,158,11,0.15); color: var(--accent-orange); }
63
+ .priority-urgent { background: rgba(239,68,68,0.15); color: var(--accent-red); }
64
+ .role-admin { background: rgba(239,68,68,0.15); color: var(--accent-red); }
65
+ .role-editor { background: rgba(139,92,246,0.15); color: var(--accent-purple); }
66
+ .role-viewer { background: rgba(100,116,139,0.15); color: var(--text-muted); }
67
+ .msg-list { max-height: 400px; overflow-y: auto; margin-bottom: 16px; }
68
+ .msg-item { padding: 12px 16px; border-radius: var(--radius-md); margin-bottom: 8px; }
69
+ .msg-user { background: rgba(59,130,246,0.08); border: 1px solid rgba(59,130,246,0.15); }
70
+ .msg-bot { background: rgba(139,92,246,0.08); border: 1px solid rgba(139,92,246,0.15); }
71
+ .msg-item .msg-meta { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 4px; display: flex; gap: 8px; align-items: center; }
72
+ .msg-item .msg-text { font-size: 0.9rem; color: var(--text-secondary); line-height: 1.6; }
73
+ .slider-group { display: flex; align-items: center; gap: 12px; }
74
+ .slider-group input[type="range"] { flex: 1; accent-color: var(--accent-blue); }
75
+ .slider-group .slider-val { min-width: 50px; text-align: right; font-family: var(--font-mono); font-size: 0.85rem; color: var(--text-secondary); }
76
+ .checkbox-group { display: flex; flex-wrap: wrap; gap: 12px; }
77
+ .checkbox-group label { display: flex; align-items: center; gap: 6px; font-size: 0.9rem; color: var(--text-secondary); cursor: pointer; }
78
+ .checkbox-group input[type="checkbox"] { accent-color: var(--accent-blue); width: 16px; height: 16px; }
79
+ .pack-card { cursor: pointer; }
80
+ .pack-card:hover { border-color: var(--accent-blue); }
81
+ .sidebar-nav a { font-size: 0.84rem; }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <div class="dashboard">
86
+
87
+ <aside class="sidebar">
88
+ <div class="sidebar-brand">
89
+ <a href="/" class="navbar-brand">
90
+ <div class="brand-icon">⚡</div>
91
+ <span>WAB</span>
92
+ </a>
93
+ </div>
94
+ <nav class="sidebar-nav">
95
+ <a href="#" class="active" data-view="overview">📊 Overview</a>
96
+ <a href="#" data-view="traffic">🔍 Traffic Intelligence</a>
97
+ <a href="#" data-view="security">🛡️ Exploit Shield</a>
98
+ <a href="#" data-view="actions">📦 Actions Library</a>
99
+ <a href="#" data-view="agents">🤖 Custom Agents</a>
100
+ <a href="#" data-view="webhooks">🔗 Webhooks &amp; CRM</a>
101
+ <a href="#" data-view="team">👥 Team Management</a>
102
+ <a href="#" data-view="support">🎫 Support</a>
103
+ <a href="#" data-view="script">✏️ Script Builder</a>
104
+ <a href="#" data-view="stealth">👻 Stealth Mode</a>
105
+ <a href="#" data-view="cdn">🌐 CDN</a>
106
+ <a href="#" data-view="audit">📋 Audit &amp; Compliance</a>
107
+ <a href="#" data-view="sandbox">🧪 Sandbox</a>
108
+ <div style="border-top:1px solid var(--border);margin:12px 0;opacity:0.3;"></div>
109
+ <div style="font-size:0.65rem;text-transform:uppercase;letter-spacing:1px;color:var(--text-muted);padding:4px 16px;margin-bottom:4px;">Advanced AI</div>
110
+ <a href="#" data-view="memory">🧠 Agent Memory</a>
111
+ <a href="#" data-view="healing">🩹 Self-Healing</a>
112
+ <a href="#" data-view="vision">👁️ Vision AI</a>
113
+ <a href="#" data-view="swarm">🐝 Agent Swarm</a>
114
+ <a href="#" data-view="marketplace">🔌 Plugins</a>
115
+ <a href="/dashboard" style="margin-top:20px;">← Back to Dashboard</a>
116
+ </nav>
117
+ <div class="sidebar-footer">
118
+ <div style="font-size:0.85rem; color:var(--text-muted); margin-bottom:8px;" id="userName"></div>
119
+ <button class="btn btn-ghost btn-sm" onclick="logout()" style="width:100%; justify-content:flex-start;">🚪 Sign Out</button>
120
+ </div>
121
+ </aside>
122
+
123
+ <main class="main-content">
124
+
125
+ <!-- ══════ OVERVIEW ══════ -->
126
+ <div id="view-overview" class="view active">
127
+ <div class="page-header"><h1>Premium Overview</h1></div>
128
+ <div class="prem-grid-6" id="overviewStats">
129
+ <div class="stat-card"><div class="label">Active Alerts</div><div class="value" id="ovAlerts">0</div></div>
130
+ <div class="stat-card"><div class="label">Security Events (24h)</div><div class="value" id="ovSecurity">0</div></div>
131
+ <div class="stat-card"><div class="label">Installed Packs</div><div class="value" id="ovPacks">0</div></div>
132
+ <div class="stat-card"><div class="label">Active Agents</div><div class="value" id="ovAgents">0</div></div>
133
+ <div class="stat-card"><div class="label">Team Members</div><div class="value" id="ovTeam">0</div></div>
134
+ <div class="stat-card"><div class="label">Open Tickets</div><div class="value" id="ovTickets">0</div></div>
135
+ </div>
136
+ <div class="actions-bar">
137
+ <button class="btn btn-primary btn-sm" onclick="quickCheckAnomalies()">🔍 Check Anomalies</button>
138
+ <button class="btn btn-secondary btn-sm" onclick="quickSecurityReport()">🛡️ View Security Report</button>
139
+ <button class="btn btn-secondary btn-sm" onclick="switchView('agents')">🤖 Create Agent</button>
140
+ <button class="btn btn-secondary btn-sm" onclick="switchView('support')">🎫 Create Ticket</button>
141
+ </div>
142
+ <h3 class="section-title">Recent Alerts</h3>
143
+ <div class="table-wrapper">
144
+ <table>
145
+ <thead><tr><th>Type</th><th>Severity</th><th>Message</th><th>Time</th><th>Actions</th></tr></thead>
146
+ <tbody id="ovAlertsTable"><tr><td colspan="5" class="empty-state">No recent alerts</td></tr></tbody>
147
+ </table>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- ══════ TRAFFIC INTELLIGENCE ══════ -->
152
+ <div id="view-traffic" class="view">
153
+ <div class="page-header"><h1>Traffic Intelligence</h1></div>
154
+ <div class="site-select-bar">
155
+ <label>Site:</label>
156
+ <select class="form-input site-selector" onchange="loadTraffic()"></select>
157
+ <button class="btn btn-primary btn-sm" onclick="checkAnomalies()">Check Anomalies</button>
158
+ </div>
159
+ <div class="stats-grid" id="trafficStats">
160
+ <div class="stat-card"><div class="label">Total Agents</div><div class="value" id="tfTotal">0</div></div>
161
+ <div class="stat-card"><div class="label">Friendly</div><div class="value" id="tfFriendly" style="color:var(--accent-green)">0</div></div>
162
+ <div class="stat-card"><div class="label">Suspicious</div><div class="value" id="tfSuspicious" style="color:var(--accent-orange)">0</div></div>
163
+ <div class="stat-card"><div class="label">Aggressive</div><div class="value" id="tfAggressive" style="color:var(--accent-red)">0</div></div>
164
+ </div>
165
+ <h3 class="section-title">Agent Profiles</h3>
166
+ <div class="table-wrapper">
167
+ <table>
168
+ <thead><tr><th>Signature</th><th>Type</th><th>Platform</th><th>Country</th><th>Requests</th><th>Last Seen</th><th>Actions</th></tr></thead>
169
+ <tbody id="trafficTable"><tr><td colspan="7" class="empty-state">Select a site</td></tr></tbody>
170
+ </table>
171
+ </div>
172
+ <h3 class="section-title">Alerts</h3>
173
+ <div class="table-wrapper">
174
+ <table>
175
+ <thead><tr><th>Type</th><th>Severity</th><th>Message</th><th>Time</th><th>Actions</th></tr></thead>
176
+ <tbody id="trafficAlerts"><tr><td colspan="5" class="empty-state">No alerts</td></tr></tbody>
177
+ </table>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- ══════ EXPLOIT SHIELD ══════ -->
182
+ <div id="view-security" class="view">
183
+ <div class="page-header"><h1>Exploit Shield</h1></div>
184
+ <div class="site-select-bar">
185
+ <label>Site:</label>
186
+ <select class="form-input site-selector" onchange="loadSecurity()"></select>
187
+ </div>
188
+ <div class="stats-grid" id="securityStats">
189
+ <div class="stat-card"><div class="label">Total Events</div><div class="value" id="secTotal">0</div></div>
190
+ <div class="stat-card"><div class="label">Critical</div><div class="value" id="secCritical" style="color:var(--accent-red)">0</div></div>
191
+ <div class="stat-card"><div class="label">High</div><div class="value" id="secHigh" style="color:var(--accent-orange)">0</div></div>
192
+ <div class="stat-card"><div class="label">Blocked Agents</div><div class="value" id="secBlocked">0</div></div>
193
+ </div>
194
+ <h3 class="section-title">Security Events</h3>
195
+ <div class="table-wrapper">
196
+ <table>
197
+ <thead><tr><th>Type</th><th>Severity</th><th>Agent</th><th>Details</th><th>Time</th></tr></thead>
198
+ <tbody id="secEventsTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
199
+ </table>
200
+ </div>
201
+ <h3 class="section-title">Blocked Agents</h3>
202
+ <div class="table-wrapper">
203
+ <table>
204
+ <thead><tr><th>Agent Signature</th><th>Reason</th><th>Blocked At</th><th>Expires</th><th>Actions</th></tr></thead>
205
+ <tbody id="secBlockedTable"><tr><td colspan="5" class="empty-state">No blocked agents</td></tr></tbody>
206
+ </table>
207
+ </div>
208
+ <h3 class="section-title">Block an Agent</h3>
209
+ <div class="card" style="max-width:500px;">
210
+ <div class="form-group">
211
+ <label>Agent Signature</label>
212
+ <input type="text" class="form-input" id="blockSig" placeholder="e.g. Scrapy">
213
+ </div>
214
+ <div class="form-group">
215
+ <label>Reason</label>
216
+ <input type="text" class="form-input" id="blockReason" placeholder="Reason for blocking">
217
+ </div>
218
+ <button class="btn btn-danger btn-sm" onclick="blockAgent()">Block Agent</button>
219
+ </div>
220
+ <h3 class="section-title">Security Report</h3>
221
+ <div class="site-select-bar">
222
+ <label>Period:</label>
223
+ <select class="form-input" id="reportDays" style="width:auto;min-width:120px;">
224
+ <option value="7">7 days</option>
225
+ <option value="30" selected>30 days</option>
226
+ <option value="90">90 days</option>
227
+ </select>
228
+ <button class="btn btn-secondary btn-sm" onclick="loadSecurityReport()">Generate Report</button>
229
+ </div>
230
+ <div id="secReportContent"></div>
231
+ </div>
232
+
233
+ <!-- ══════ ACTIONS LIBRARY ══════ -->
234
+ <div id="view-actions" class="view">
235
+ <div class="page-header"><h1>Actions Library</h1></div>
236
+ <div class="site-select-bar">
237
+ <label>Filter by platform:</label>
238
+ <select class="form-input" id="packPlatformFilter" onchange="loadPacks()" style="width:auto;min-width:160px;">
239
+ <option value="">All Platforms</option>
240
+ </select>
241
+ <label style="margin-left:16px;">Site for installs:</label>
242
+ <select class="form-input site-selector" onchange="loadInstalledPacks()" style="min-width:200px;"></select>
243
+ </div>
244
+ <h3 class="section-title">Available Packs</h3>
245
+ <div class="prem-grid-3" id="packsGrid"><div class="empty-state" style="grid-column:1/-1;">Loading packs...</div></div>
246
+ <h3 class="section-title">Installed Packs</h3>
247
+ <div class="table-wrapper">
248
+ <table>
249
+ <thead><tr><th>Pack</th><th>Platform</th><th>Version</th><th>Installed</th><th>Actions</th></tr></thead>
250
+ <tbody id="installedPacksTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
251
+ </table>
252
+ </div>
253
+ </div>
254
+
255
+ <!-- ══════ CUSTOM AGENTS ══════ -->
256
+ <div id="view-agents" class="view">
257
+ <div class="page-header">
258
+ <h1>Custom Agents</h1>
259
+ <button class="btn btn-primary btn-sm" onclick="openModal('createAgentModal')">+ Create Agent</button>
260
+ </div>
261
+ <div class="site-select-bar">
262
+ <label>Site:</label>
263
+ <select class="form-input site-selector" onchange="loadAgents()"></select>
264
+ </div>
265
+ <div class="table-wrapper">
266
+ <table>
267
+ <thead><tr><th>Name</th><th>Status</th><th>Schedule</th><th>Runs</th><th>Last Run</th><th>Actions</th></tr></thead>
268
+ <tbody id="agentsTable"><tr><td colspan="6" class="empty-state">Select a site</td></tr></tbody>
269
+ </table>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- ══════ WEBHOOKS & CRM ══════ -->
274
+ <div id="view-webhooks" class="view">
275
+ <div class="page-header">
276
+ <h1>Webhooks &amp; CRM</h1>
277
+ </div>
278
+ <div class="site-select-bar">
279
+ <label>Site:</label>
280
+ <select class="form-input site-selector" onchange="loadWebhooksView()"></select>
281
+ </div>
282
+ <h3 class="section-title" style="display:flex;align-items:center;justify-content:space-between;">Webhooks <button class="btn btn-primary btn-sm" onclick="openModal('createWebhookModal')">+ Add Webhook</button></h3>
283
+ <div class="table-wrapper">
284
+ <table>
285
+ <thead><tr><th>Name</th><th>URL</th><th>Events</th><th>Status</th><th>Failures</th><th>Actions</th></tr></thead>
286
+ <tbody id="webhooksTable"><tr><td colspan="6" class="empty-state">Select a site</td></tr></tbody>
287
+ </table>
288
+ </div>
289
+ <div id="webhookLogsSection" style="display:none;">
290
+ <h3 class="section-title">Webhook Logs</h3>
291
+ <div class="table-wrapper">
292
+ <table>
293
+ <thead><tr><th>Event</th><th>Status</th><th>Response</th><th>Time</th></tr></thead>
294
+ <tbody id="webhookLogsTable"></tbody>
295
+ </table>
296
+ </div>
297
+ </div>
298
+ <div class="actions-bar" style="margin-top:16px;">
299
+ <button class="btn btn-secondary btn-sm" onclick="testWebhook()">🧪 Test Webhooks</button>
300
+ </div>
301
+ <h3 class="section-title" style="display:flex;align-items:center;justify-content:space-between;">CRM Integrations <button class="btn btn-primary btn-sm" onclick="openModal('createCrmModal')">+ Add Integration</button></h3>
302
+ <div class="table-wrapper">
303
+ <table>
304
+ <thead><tr><th>Provider</th><th>Status</th><th>Last Sync</th><th>Actions</th></tr></thead>
305
+ <tbody id="crmTable"><tr><td colspan="4" class="empty-state">No integrations</td></tr></tbody>
306
+ </table>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- ══════ TEAM MANAGEMENT ══════ -->
311
+ <div id="view-team" class="view">
312
+ <div class="page-header">
313
+ <h1>Team Management</h1>
314
+ <button class="btn btn-primary btn-sm" onclick="openModal('inviteTeamModal')">+ Invite Member</button>
315
+ </div>
316
+ <div class="table-wrapper">
317
+ <table>
318
+ <thead><tr><th>Email</th><th>Name</th><th>Role</th><th>Site Access</th><th>Quota</th><th>Usage</th><th>Actions</th></tr></thead>
319
+ <tbody id="teamTable"><tr><td colspan="7" class="empty-state">No team members</td></tr></tbody>
320
+ </table>
321
+ </div>
322
+ </div>
323
+
324
+ <!-- ══════ SUPPORT ══════ -->
325
+ <div id="view-support" class="view">
326
+ <div class="page-header">
327
+ <h1>Support</h1>
328
+ <button class="btn btn-primary btn-sm" onclick="openModal('createTicketModal')">+ Create Ticket</button>
329
+ </div>
330
+ <div class="stats-grid" id="supportStats">
331
+ <div class="stat-card"><div class="label">Open</div><div class="value" id="supOpen" style="color:var(--accent-blue)">0</div></div>
332
+ <div class="stat-card"><div class="label">In Progress</div><div class="value" id="supProgress" style="color:var(--accent-orange)">0</div></div>
333
+ <div class="stat-card"><div class="label">Waiting</div><div class="value" id="supWaiting" style="color:var(--accent-purple)">0</div></div>
334
+ <div class="stat-card"><div class="label">Resolved</div><div class="value" id="supResolved" style="color:var(--accent-green)">0</div></div>
335
+ </div>
336
+ <div class="table-wrapper">
337
+ <table>
338
+ <thead><tr><th>Subject</th><th>Priority</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead>
339
+ <tbody id="ticketsTable"><tr><td colspan="5" class="empty-state">No tickets</td></tr></tbody>
340
+ </table>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- ══════ SCRIPT BUILDER ══════ -->
345
+ <div id="view-script" class="view">
346
+ <div class="page-header"><h1>Script Builder</h1></div>
347
+ <div class="site-select-bar">
348
+ <label>Site:</label>
349
+ <select class="form-input site-selector" onchange="loadScriptConfig()"></select>
350
+ </div>
351
+ <div class="prem-grid-2">
352
+ <div>
353
+ <h3 class="section-title">Plugins</h3>
354
+ <div class="card" id="pluginsList" style="max-height:300px;overflow-y:auto;">Loading plugins...</div>
355
+ <h3 class="section-title">Options</h3>
356
+ <div class="card">
357
+ <div class="toggle-wrap">
358
+ <div class="toggle-label">Minified<small>Remove whitespace and comments</small></div>
359
+ <label class="toggle"><input type="checkbox" id="scriptMinified" checked><span class="toggle-slider"></span></label>
360
+ </div>
361
+ <div class="toggle-wrap">
362
+ <div class="toggle-label">AMP Compatible<small>Remove document.write calls</small></div>
363
+ <label class="toggle"><input type="checkbox" id="scriptAmp"><span class="toggle-slider"></span></label>
364
+ </div>
365
+ <div class="toggle-wrap">
366
+ <div class="toggle-label">Auto Patch<small>Auto-apply updates</small></div>
367
+ <label class="toggle"><input type="checkbox" id="scriptAutoPatch" checked><span class="toggle-slider"></span></label>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ <div>
372
+ <h3 class="section-title">Custom CSS</h3>
373
+ <textarea class="form-input" id="scriptCustomCss" rows="5" placeholder="/* Custom CSS for bridge overlay */"></textarea>
374
+ <h3 class="section-title">Custom JS</h3>
375
+ <textarea class="form-input" id="scriptCustomJs" rows="5" placeholder="// Custom JavaScript"></textarea>
376
+ </div>
377
+ </div>
378
+ <div class="actions-bar">
379
+ <button class="btn btn-primary btn-sm" onclick="buildScript()">🔨 Build Script</button>
380
+ <button class="btn btn-secondary btn-sm" onclick="saveScriptConfig()">💾 Save Config</button>
381
+ </div>
382
+ <div id="scriptPreviewSection" style="display:none;">
383
+ <h3 class="section-title">Built Script</h3>
384
+ <div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;">
385
+ <span class="badge badge-active" id="scriptHash"></span>
386
+ <span style="font-size:0.85rem;color:var(--text-muted);" id="scriptSize"></span>
387
+ </div>
388
+ <div class="snippet-box" style="max-height:300px;overflow:auto;">
389
+ <button class="copy-btn" onclick="copyBuiltScript()">Copy</button>
390
+ <pre id="scriptPreview"></pre>
391
+ </div>
392
+ </div>
393
+ </div>
394
+
395
+ <!-- ══════ STEALTH MODE ══════ -->
396
+ <div id="view-stealth" class="view">
397
+ <div class="page-header"><h1>Stealth Mode</h1></div>
398
+ <div class="site-select-bar">
399
+ <label>Site:</label>
400
+ <select class="form-input site-selector" onchange="loadStealth()"></select>
401
+ </div>
402
+ <div class="prem-grid-2">
403
+ <div class="card">
404
+ <h3 style="margin-bottom:20px;">Profile Settings</h3>
405
+ <div class="form-group">
406
+ <label>Typing Speed Min (ms)</label>
407
+ <div class="slider-group">
408
+ <input type="range" min="0" max="500" value="30" id="stTypingMin" oninput="document.getElementById('stTypingMinVal').textContent=this.value+'ms'">
409
+ <span class="slider-val" id="stTypingMinVal">30ms</span>
410
+ </div>
411
+ </div>
412
+ <div class="form-group">
413
+ <label>Typing Speed Max (ms)</label>
414
+ <div class="slider-group">
415
+ <input type="range" min="0" max="500" value="120" id="stTypingMax" oninput="document.getElementById('stTypingMaxVal').textContent=this.value+'ms'">
416
+ <span class="slider-val" id="stTypingMaxVal">120ms</span>
417
+ </div>
418
+ </div>
419
+ <div class="form-group">
420
+ <label>Mouse Speed</label>
421
+ <select class="form-input" id="stMouseSpeed">
422
+ <option value="slow">Slow</option>
423
+ <option value="natural" selected>Natural</option>
424
+ <option value="fast">Fast</option>
425
+ </select>
426
+ </div>
427
+ <div class="form-group">
428
+ <label>Scroll Behavior</label>
429
+ <select class="form-input" id="stScrollBehavior">
430
+ <option value="linear">Linear</option>
431
+ <option value="eased" selected>Eased</option>
432
+ <option value="natural">Natural</option>
433
+ </select>
434
+ </div>
435
+ <div class="form-group">
436
+ <label>Click Delay Min (ms)</label>
437
+ <div class="slider-group">
438
+ <input type="range" min="0" max="500" value="50" id="stClickMin" oninput="document.getElementById('stClickMinVal').textContent=this.value+'ms'">
439
+ <span class="slider-val" id="stClickMinVal">50ms</span>
440
+ </div>
441
+ </div>
442
+ <div class="form-group">
443
+ <label>Click Delay Max (ms)</label>
444
+ <div class="slider-group">
445
+ <input type="range" min="0" max="500" value="400" id="stClickMax" oninput="document.getElementById('stClickMaxVal').textContent=this.value+'ms'">
446
+ <span class="slider-val" id="stClickMaxVal">400ms</span>
447
+ </div>
448
+ </div>
449
+ </div>
450
+ <div>
451
+ <div class="card" style="margin-bottom:20px;">
452
+ <h3 style="margin-bottom:20px;">Anti-Detection Options</h3>
453
+ <div class="checkbox-group" style="flex-direction:column;">
454
+ <label><input type="checkbox" id="stHideWebdriver"> Hide WebDriver flag</label>
455
+ <label><input type="checkbox" id="stSpoofPlugins"> Plugin spoofing</label>
456
+ <label><input type="checkbox" id="stSpoofLang"> Language spoofing</label>
457
+ </div>
458
+ </div>
459
+ <div class="actions-bar">
460
+ <button class="btn btn-primary btn-sm" onclick="saveStealth()">💾 Save Profile</button>
461
+ <button class="btn btn-secondary btn-sm" onclick="generateStealthScript()">⚡ Generate Script</button>
462
+ </div>
463
+ <div id="stealthScriptSection" style="display:none;">
464
+ <h3 class="section-title">Generated Stealth Script</h3>
465
+ <div class="snippet-box" style="max-height:300px;overflow:auto;">
466
+ <button class="copy-btn" onclick="copyStealthScript()">Copy</button>
467
+ <pre id="stealthScriptPreview"></pre>
468
+ </div>
469
+ </div>
470
+ </div>
471
+ </div>
472
+ </div>
473
+
474
+ <!-- ══════ CDN ══════ -->
475
+ <div id="view-cdn" class="view">
476
+ <div class="page-header"><h1>CDN</h1></div>
477
+ <div class="site-select-bar">
478
+ <label>Site:</label>
479
+ <select class="form-input site-selector" onchange="loadCdn()"></select>
480
+ </div>
481
+ <div class="prem-grid-2">
482
+ <div class="card">
483
+ <h3 style="margin-bottom:20px;">CDN Configuration</h3>
484
+ <div class="form-group">
485
+ <label>Custom Domain</label>
486
+ <input type="text" class="form-input" id="cdnDomain" placeholder="cdn.yourdomain.com">
487
+ </div>
488
+ <div class="form-group">
489
+ <label>Cache TTL (seconds)</label>
490
+ <input type="number" class="form-input" id="cdnTtl" value="86400" min="60">
491
+ </div>
492
+ <div class="form-group">
493
+ <label>Edge Locations</label>
494
+ <div class="checkbox-group" id="cdnEdges">
495
+ <label><input type="checkbox" value="us-east" checked> US East</label>
496
+ <label><input type="checkbox" value="us-west"> US West</label>
497
+ <label><input type="checkbox" value="eu-west" checked> EU West</label>
498
+ <label><input type="checkbox" value="eu-central"> EU Central</label>
499
+ <label><input type="checkbox" value="ap-southeast"> AP Southeast</label>
500
+ <label><input type="checkbox" value="ap-northeast"> AP Northeast</label>
501
+ </div>
502
+ </div>
503
+ <div class="form-group">
504
+ <label>SSL Status</label>
505
+ <span class="badge badge-active" id="cdnSslBadge">Pending</span>
506
+ </div>
507
+ <button class="btn btn-primary btn-sm" onclick="saveCdn()">Save CDN Config</button>
508
+ </div>
509
+ <div>
510
+ <div class="card" style="margin-bottom:20px;">
511
+ <h3 style="margin-bottom:20px;">CDN Stats</h3>
512
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;">
513
+ <div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Requests</div><div style="font-size:1.5rem;font-weight:700;" id="cdnStatReqs">0</div></div>
514
+ <div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Bandwidth</div><div style="font-size:1.5rem;font-weight:700;" id="cdnStatBw">0 B</div></div>
515
+ <div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Cache Hits</div><div style="font-size:1.5rem;font-weight:700;" id="cdnStatHits">0</div></div>
516
+ </div>
517
+ </div>
518
+ <div class="card">
519
+ <h3 style="margin-bottom:12px;">CDN URL</h3>
520
+ <div class="snippet-box">
521
+ <button class="copy-btn" onclick="copyCdnUrl()">Copy</button>
522
+ <pre id="cdnUrlDisplay">Configure a site to see the CDN URL</pre>
523
+ </div>
524
+ </div>
525
+ </div>
526
+ </div>
527
+ </div>
528
+
529
+ <!-- ══════ AUDIT & COMPLIANCE ══════ -->
530
+ <div id="view-audit" class="view">
531
+ <div class="page-header"><h1>Audit &amp; Compliance</h1></div>
532
+ <div class="site-select-bar">
533
+ <label>Site:</label>
534
+ <select class="form-input site-selector" onchange="loadAudit()"></select>
535
+ </div>
536
+ <div class="prem-grid-2">
537
+ <div class="card">
538
+ <h3 style="margin-bottom:20px;">Compliance Settings</h3>
539
+ <div class="form-group">
540
+ <label>Retention Days</label>
541
+ <input type="number" class="form-input" id="auditRetention" value="90" min="1">
542
+ </div>
543
+ <div class="toggle-wrap">
544
+ <div class="toggle-label">HIPAA Mode<small>Enable HIPAA compliance</small></div>
545
+ <label class="toggle"><input type="checkbox" id="auditHipaa"><span class="toggle-slider"></span></label>
546
+ </div>
547
+ <div class="toggle-wrap">
548
+ <div class="toggle-label">GDPR Mode<small>Enable GDPR compliance</small></div>
549
+ <label class="toggle"><input type="checkbox" id="auditGdpr"><span class="toggle-slider"></span></label>
550
+ </div>
551
+ <div class="toggle-wrap">
552
+ <div class="toggle-label">SOC2 Mode<small>Enable SOC2 compliance</small></div>
553
+ <label class="toggle"><input type="checkbox" id="auditSoc2"><span class="toggle-slider"></span></label>
554
+ </div>
555
+ <div class="toggle-wrap">
556
+ <div class="toggle-label">Auto-Purge<small>Automatically purge old logs</small></div>
557
+ <label class="toggle"><input type="checkbox" id="auditAutoPurge" checked><span class="toggle-slider"></span></label>
558
+ </div>
559
+ <div style="margin-top:16px;display:flex;gap:10px;">
560
+ <button class="btn btn-primary btn-sm" onclick="saveCompliance()">Save Compliance</button>
561
+ <button class="btn btn-danger btn-sm" onclick="purgeAuditLogs()">Purge Old Logs</button>
562
+ </div>
563
+ </div>
564
+ <div>
565
+ <div class="card">
566
+ <h3 style="margin-bottom:16px;">Export Logs</h3>
567
+ <div class="form-group"><label>From</label><input type="date" class="form-input" id="auditSince"></div>
568
+ <div class="form-group"><label>Until</label><input type="date" class="form-input" id="auditUntil"></div>
569
+ <div style="display:flex;gap:10px;">
570
+ <button class="btn btn-secondary btn-sm" onclick="exportAuditLogs('csv')">📥 Export CSV</button>
571
+ <button class="btn btn-secondary btn-sm" onclick="exportAuditLogs('json')">📥 Export JSON</button>
572
+ </div>
573
+ </div>
574
+ </div>
575
+ </div>
576
+ <h3 class="section-title">Audit Logs</h3>
577
+ <div class="site-select-bar">
578
+ <label>Filter:</label>
579
+ <select class="form-input" id="auditActionFilter" onchange="loadAuditLogs()" style="width:auto;min-width:150px;">
580
+ <option value="">All Actions</option>
581
+ </select>
582
+ </div>
583
+ <div class="table-wrapper">
584
+ <table>
585
+ <thead><tr><th>Timestamp</th><th>Action</th><th>Resource</th><th>User</th><th>IP</th></tr></thead>
586
+ <tbody id="auditLogsTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
587
+ </table>
588
+ </div>
589
+ <div style="display:flex;gap:10px;margin-top:12px;align-items:center;" id="auditPagination"></div>
590
+ </div>
591
+
592
+ <!-- ══════ SANDBOX ══════ -->
593
+ <div id="view-sandbox" class="view">
594
+ <div class="page-header">
595
+ <h1>Sandbox</h1>
596
+ <button class="btn btn-primary btn-sm" onclick="createSandbox()">+ Create Sandbox</button>
597
+ </div>
598
+ <div class="site-select-bar">
599
+ <label>Site:</label>
600
+ <select class="form-input site-selector" onchange="loadSandboxes()"></select>
601
+ </div>
602
+ <h3 class="section-title">Environments</h3>
603
+ <div class="table-wrapper">
604
+ <table>
605
+ <thead><tr><th>Name</th><th>Status</th><th>Traffic Generated</th><th>Created</th><th>Actions</th></tr></thead>
606
+ <tbody id="sandboxTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
607
+ </table>
608
+ </div>
609
+ <div id="sandboxDetail" style="display:none;">
610
+ <h3 class="section-title">Simulate Traffic</h3>
611
+ <div class="card" style="max-width:500px;">
612
+ <div class="form-group">
613
+ <label>Agent Count</label>
614
+ <input type="number" class="form-input" id="simAgentCount" value="10" min="1" max="1000">
615
+ </div>
616
+ <div class="form-group">
617
+ <label>Duration (seconds)</label>
618
+ <input type="number" class="form-input" id="simDuration" value="60" min="1" max="3600">
619
+ </div>
620
+ <div class="form-group">
621
+ <label>Actions per Agent</label>
622
+ <input type="number" class="form-input" id="simActions" value="5" min="1" max="100">
623
+ </div>
624
+ <button class="btn btn-primary btn-sm" onclick="simulateTraffic()">▶ Run Simulation</button>
625
+ </div>
626
+ <div id="simResults" style="display:none;">
627
+ <h3 class="section-title">Simulation Results</h3>
628
+ <div class="card" id="simResultsContent"></div>
629
+ </div>
630
+ <h3 class="section-title">Benchmarks</h3>
631
+ <div class="site-select-bar">
632
+ <label>Type:</label>
633
+ <select class="form-input" id="benchType" style="width:auto;min-width:180px;">
634
+ <option value="rate_limit">Rate Limit</option>
635
+ <option value="response_time">Response Time</option>
636
+ <option value="throughput">Throughput</option>
637
+ </select>
638
+ <button class="btn btn-primary btn-sm" onclick="runBenchmark()">▶ Run Benchmark</button>
639
+ <button class="btn btn-secondary btn-sm" onclick="compareBenchmarks()">📊 Compare</button>
640
+ </div>
641
+ <div class="table-wrapper">
642
+ <table>
643
+ <thead><tr><th>Type</th><th>Before</th><th>After</th><th>Improvement</th><th>Date</th></tr></thead>
644
+ <tbody id="benchmarksTable"><tr><td colspan="5" class="empty-state">No benchmarks</td></tr></tbody>
645
+ </table>
646
+ </div>
647
+ <div id="benchCompare" style="display:none;">
648
+ <h3 class="section-title">Benchmark Comparison</h3>
649
+ <div class="card" id="benchCompareContent"></div>
650
+ </div>
651
+ </div>
652
+ </div>
653
+
654
+ <!-- ═══════════ ADVANCED AI VIEWS ═══════════ -->
655
+
656
+ <!-- ══════ AGENT MEMORY ══════ -->
657
+ <div id="view-memory" class="view">
658
+ <div class="page-header"><h1>🧠 Agent Memory</h1></div>
659
+ <div class="site-select-bar"><label>Site:</label><select class="form-input site-selector" onchange="loadMemoryView()"></select></div>
660
+ <div class="prem-grid-6" id="memoryStats"></div>
661
+ <div class="prem-grid-2">
662
+ <div class="card">
663
+ <h3 class="section-title">Store Memory</h3>
664
+ <div class="form-group"><label>Key</label><input type="text" class="form-input" id="memKey" placeholder="user_preference"></div>
665
+ <div class="form-group"><label>Value</label><textarea class="form-input" id="memValue" rows="3" placeholder='{"favorite_store":"Local Shop"}'></textarea></div>
666
+ <div class="form-group"><label>Type</label>
667
+ <select class="form-input" id="memType"><option>preference</option><option>interaction</option><option>correction</option><option>pattern</option></select>
668
+ </div>
669
+ <div class="form-group"><label>Category</label>
670
+ <select class="form-input" id="memCat"><option>navigation</option><option>purchase</option><option>search</option><option>form</option><option>custom</option></select>
671
+ </div>
672
+ <button class="btn btn-primary btn-sm" onclick="storeMemory()">💾 Store</button>
673
+ </div>
674
+ <div class="card">
675
+ <h3 class="section-title">Recall Memories</h3>
676
+ <div class="form-group"><label>Query (natural language)</label><input type="text" class="form-input" id="memQuery" placeholder="What does the user prefer?"></div>
677
+ <div class="form-group"><label>Filter by Type</label>
678
+ <select class="form-input" id="memFilterType"><option value="">All</option><option>preference</option><option>interaction</option><option>correction</option><option>pattern</option></select>
679
+ </div>
680
+ <button class="btn btn-primary btn-sm" onclick="recallMemories()">🔍 Search</button>
681
+ <div id="memResults" style="margin-top:16px;max-height:300px;overflow-y:auto;"></div>
682
+ </div>
683
+ </div>
684
+ <div class="prem-grid-2" style="margin-top:16px;">
685
+ <div class="card">
686
+ <h3 class="section-title">Preferences</h3>
687
+ <div id="memPreferences" style="max-height:250px;overflow-y:auto;"></div>
688
+ </div>
689
+ <div class="card">
690
+ <h3 class="section-title">Sessions</h3>
691
+ <div id="memSessions" style="max-height:250px;overflow-y:auto;"></div>
692
+ <button class="btn btn-secondary btn-sm" onclick="consolidateMemories()" style="margin-top:12px;">🔄 Consolidate</button>
693
+ </div>
694
+ </div>
695
+ </div>
696
+
697
+ <!-- ══════ SELF-HEALING ══════ -->
698
+ <div id="view-healing" class="view">
699
+ <div class="page-header"><h1>🩹 Self-Healing Selectors</h1></div>
700
+ <div class="site-select-bar"><label>Site:</label><select class="form-input site-selector" onchange="loadHealingView()"></select></div>
701
+ <div class="prem-grid-6" id="healingStats"></div>
702
+ <div class="prem-grid-2">
703
+ <div class="card">
704
+ <h3 class="section-title">Register Selector</h3>
705
+ <div class="form-group"><label>Action Name</label><input type="text" class="form-input" id="healAction" placeholder="login_button"></div>
706
+ <div class="form-group"><label>CSS Selector</label><input type="text" class="form-input" id="healSelector" placeholder="#submit-btn"></div>
707
+ <div class="form-group"><label>Type</label>
708
+ <select class="form-input" id="healType"><option>css</option><option>xpath</option><option>aria</option><option>text</option><option>data-attr</option></select>
709
+ </div>
710
+ <button class="btn btn-primary btn-sm" onclick="registerSelector()">📝 Register</button>
711
+ </div>
712
+ <div class="card">
713
+ <h3 class="section-title">Heal Selector</h3>
714
+ <div class="form-group"><label>Action Name</label><input type="text" class="form-input" id="healTargetAction" placeholder="login_button"></div>
715
+ <div class="form-group"><label>Failed Selector</label><input type="text" class="form-input" id="healFailed" placeholder="#old-submit-btn"></div>
716
+ <button class="btn btn-primary btn-sm" onclick="healSelector()">🩹 Heal</button>
717
+ <div id="healResult" style="margin-top:16px;"></div>
718
+ </div>
719
+ </div>
720
+ <div class="card" style="margin-top:16px;">
721
+ <h3 class="section-title">Healing History</h3>
722
+ <div class="table-wrapper"><table>
723
+ <thead><tr><th>Action</th><th>Old</th><th>New</th><th>Strategy</th><th>Confidence</th><th>Success</th><th>Date</th></tr></thead>
724
+ <tbody id="healingHistoryBody"></tbody>
725
+ </table></div>
726
+ </div>
727
+ </div>
728
+
729
+ <!-- ══════ VISION AI ══════ -->
730
+ <div id="view-vision" class="view">
731
+ <div class="page-header"><h1>👁️ Vision AI</h1></div>
732
+ <div class="site-select-bar"><label>Site:</label><select class="form-input site-selector" onchange="loadVisionView()"></select></div>
733
+ <div class="prem-grid-2">
734
+ <div class="card">
735
+ <h3 class="section-title">Configure Vision</h3>
736
+ <div class="form-group"><label>Provider</label>
737
+ <select class="form-input" id="visionProvider"><option value="local">Local (Ollama)</option><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option></select>
738
+ </div>
739
+ <div class="form-group"><label>Model</label><input type="text" class="form-input" id="visionModel" placeholder="moondream"></div>
740
+ <div class="form-group"><label>Endpoint (for local)</label><input type="text" class="form-input" id="visionEndpoint" placeholder="http://localhost:11434"></div>
741
+ <div class="form-group"><label>API Key</label><input type="password" class="form-input" id="visionApiKey" placeholder="sk-..."></div>
742
+ <button class="btn btn-primary btn-sm" onclick="configureVision()">💾 Save Config</button>
743
+ </div>
744
+ <div class="card">
745
+ <h3 class="section-title">Analyze Screenshot</h3>
746
+ <div class="form-group"><label>URL</label><input type="text" class="form-input" id="visionUrl" placeholder="https://example.com/page"></div>
747
+ <div class="form-group"><label>Custom Prompt (optional)</label><textarea class="form-input" id="visionPrompt" rows="2" placeholder="Find all purchase buttons"></textarea></div>
748
+ <div class="form-group"><label>Screenshot (Base64)</label><textarea class="form-input" id="visionScreenshot" rows="3" placeholder="Paste base64 encoded screenshot..."></textarea></div>
749
+ <button class="btn btn-primary btn-sm" onclick="analyzeScreenshot()">👁️ Analyze</button>
750
+ <div id="visionResult" style="margin-top:16px;max-height:400px;overflow-y:auto;"></div>
751
+ </div>
752
+ </div>
753
+ <div class="card" style="margin-top:16px;">
754
+ <h3 class="section-title">Analysis History</h3>
755
+ <div class="table-wrapper"><table>
756
+ <thead><tr><th>URL</th><th>Provider</th><th>Elements</th><th>Tokens</th><th>Latency</th><th>Date</th></tr></thead>
757
+ <tbody id="visionHistoryBody"></tbody>
758
+ </table></div>
759
+ </div>
760
+ </div>
761
+
762
+ <!-- ══════ AGENT SWARM ══════ -->
763
+ <div id="view-swarm" class="view">
764
+ <div class="page-header"><h1>🐝 Agent Swarm</h1></div>
765
+ <div class="site-select-bar"><label>Site:</label><select class="form-input site-selector" onchange="loadSwarmView()"></select></div>
766
+ <div class="prem-grid-2">
767
+ <div class="card">
768
+ <h3 class="section-title">Swarm Configuration</h3>
769
+ <div class="form-group"><label>Strategy</label>
770
+ <select class="form-input" id="swarmStrategy"><option value="parallel">Parallel (all at once)</option><option value="sequential">Sequential (one by one)</option><option value="competitive">Competitive (best wins)</option><option value="collaborative">Collaborative (merge all)</option></select>
771
+ </div>
772
+ <div class="form-group"><label>Max Agents</label><input type="number" class="form-input" id="swarmMaxAgents" value="3" min="1" max="10"></div>
773
+ <div class="form-group"><label>Timeout (ms)</label><input type="number" class="form-input" id="swarmTimeout" value="30000" min="5000" max="120000"></div>
774
+ <button class="btn btn-primary btn-sm" onclick="configureSwarm()">💾 Save Config</button>
775
+ </div>
776
+ <div class="card">
777
+ <h3 class="section-title">Create Task</h3>
778
+ <div class="form-group"><label>Task Type</label>
779
+ <select class="form-input" id="swarmTaskType"><option>search</option><option>compare</option><option>monitor</option><option>scrape</option></select>
780
+ </div>
781
+ <div class="form-group"><label>Objective</label><input type="text" class="form-input" id="swarmObjective" placeholder="Find the cheapest hotel in Riyadh"></div>
782
+ <div class="form-group"><label>Targets (one URL per line)</label><textarea class="form-input" id="swarmTargets" rows="4" placeholder="https://booking.com&#10;https://localhotel.sa&#10;https://agoda.com"></textarea></div>
783
+ <button class="btn btn-primary btn-sm" onclick="createAndRunSwarm()">🐝 Launch Swarm</button>
784
+ <div id="swarmResult" style="margin-top:16px;max-height:400px;overflow-y:auto;"></div>
785
+ </div>
786
+ </div>
787
+ <div class="prem-grid-6" id="swarmStats"></div>
788
+ <div class="card" style="margin-top:16px;">
789
+ <h3 class="section-title">Task History</h3>
790
+ <div class="table-wrapper"><table>
791
+ <thead><tr><th>Task</th><th>Type</th><th>Objective</th><th>Agents</th><th>Status</th><th>Date</th></tr></thead>
792
+ <tbody id="swarmHistoryBody"></tbody>
793
+ </table></div>
794
+ </div>
795
+ </div>
796
+
797
+ <!-- ══════ PLUGINS ══════ -->
798
+ <div id="view-marketplace" class="view">
799
+ <div class="page-header"><h1>🔌 Plugin Marketplace</h1></div>
800
+ <div class="site-select-bar"><label>Site:</label><select class="form-input site-selector" onchange="loadMarketplaceView()"></select></div>
801
+ <h3 class="section-title">Available Plugins</h3>
802
+ <div class="prem-grid-3" id="pluginCards"></div>
803
+ <h3 class="section-title">Installed Plugins</h3>
804
+ <div class="table-wrapper"><table>
805
+ <thead><tr><th>Plugin</th><th>Category</th><th>Version</th><th>Status</th><th>Actions</th></tr></thead>
806
+ <tbody id="installedPluginsBody"></tbody>
807
+ </table></div>
808
+ </div>
809
+
810
+ </main>
811
+ </div>
812
+
813
+ <!-- ══════ MODALS ══════ -->
814
+
815
+ <div class="modal-overlay" id="createAgentModal">
816
+ <div class="modal" style="max-width:600px;">
817
+ <div class="modal-header"><h2>Create Agent</h2><button class="modal-close" onclick="closeModal('createAgentModal')">&times;</button></div>
818
+ <div class="modal-body">
819
+ <div class="alert alert-error" id="createAgentError"></div>
820
+ <div class="form-group"><label>Name</label><input type="text" class="form-input" id="agentName" placeholder="My Agent"></div>
821
+ <div class="form-group"><label>Description</label><input type="text" class="form-input" id="agentDesc" placeholder="What does this agent do?"></div>
822
+ <div class="form-group">
823
+ <label>Steps (JSON array)</label>
824
+ <textarea class="form-input" id="agentSteps" rows="6" placeholder='[{"action":"click","selector":"#login-btn","waitMs":1000},{"action":"fill","selector":"#email","value":"test@example.com"}]'></textarea>
825
+ <small style="color:var(--text-muted);display:block;margin-top:4px;">Each step: { action, selector, value, waitMs }</small>
826
+ </div>
827
+ <div class="form-group"><label>Schedule (optional)</label><input type="text" class="form-input" id="agentSchedule" placeholder="e.g. every 1h, daily 09:00, weekly mon 08:00"></div>
828
+ </div>
829
+ <div class="modal-footer">
830
+ <button class="btn btn-secondary" onclick="closeModal('createAgentModal')">Cancel</button>
831
+ <button class="btn btn-primary" onclick="createAgent()">Create</button>
832
+ </div>
833
+ </div>
834
+ </div>
835
+
836
+ <div class="modal-overlay" id="agentDetailModal">
837
+ <div class="modal" style="max-width:700px;">
838
+ <div class="modal-header"><h2 id="agentDetailTitle">Agent Details</h2><button class="modal-close" onclick="closeModal('agentDetailModal')">&times;</button></div>
839
+ <div class="modal-body" id="agentDetailBody"></div>
840
+ <div class="modal-footer">
841
+ <button class="btn btn-danger btn-sm" id="agentDeleteBtn">Delete</button>
842
+ <button class="btn btn-secondary btn-sm" id="agentRunBtn">▶ Run Now</button>
843
+ <button class="btn btn-secondary" onclick="closeModal('agentDetailModal')">Close</button>
844
+ </div>
845
+ </div>
846
+ </div>
847
+
848
+ <div class="modal-overlay" id="createWebhookModal">
849
+ <div class="modal">
850
+ <div class="modal-header"><h2>Add Webhook</h2><button class="modal-close" onclick="closeModal('createWebhookModal')">&times;</button></div>
851
+ <div class="modal-body">
852
+ <div class="alert alert-error" id="createWebhookError"></div>
853
+ <div class="form-group"><label>Name</label><input type="text" class="form-input" id="whName" placeholder="My Webhook"></div>
854
+ <div class="form-group"><label>URL</label><input type="url" class="form-input" id="whUrl" placeholder="https://example.com/webhook"></div>
855
+ <div class="form-group">
856
+ <label>Events</label>
857
+ <div class="checkbox-group" id="whEvents">
858
+ <label><input type="checkbox" value="action.executed" checked> action.executed</label>
859
+ <label><input type="checkbox" value="agent.detected"> agent.detected</label>
860
+ <label><input type="checkbox" value="security.alert"> security.alert</label>
861
+ <label><input type="checkbox" value="agent.blocked"> agent.blocked</label>
862
+ </div>
863
+ </div>
864
+ <div class="form-group"><label>Secret (optional)</label><input type="text" class="form-input" id="whSecret" placeholder="Auto-generated if empty"></div>
865
+ </div>
866
+ <div class="modal-footer">
867
+ <button class="btn btn-secondary" onclick="closeModal('createWebhookModal')">Cancel</button>
868
+ <button class="btn btn-primary" onclick="createWebhook()">Create</button>
869
+ </div>
870
+ </div>
871
+ </div>
872
+
873
+ <div class="modal-overlay" id="createCrmModal">
874
+ <div class="modal">
875
+ <div class="modal-header"><h2>Add CRM Integration</h2><button class="modal-close" onclick="closeModal('createCrmModal')">&times;</button></div>
876
+ <div class="modal-body">
877
+ <div class="alert alert-error" id="createCrmError"></div>
878
+ <div class="form-group">
879
+ <label>Provider</label>
880
+ <select class="form-input" id="crmProvider">
881
+ <option value="salesforce">Salesforce</option>
882
+ <option value="hubspot">HubSpot</option>
883
+ <option value="zoho">Zoho</option>
884
+ </select>
885
+ </div>
886
+ <div class="form-group"><label>Config (JSON)</label><textarea class="form-input" id="crmConfig" rows="4" placeholder='{"apiKey":"...","instanceUrl":"..."}'></textarea></div>
887
+ </div>
888
+ <div class="modal-footer">
889
+ <button class="btn btn-secondary" onclick="closeModal('createCrmModal')">Cancel</button>
890
+ <button class="btn btn-primary" onclick="createCrmIntegration()">Add Integration</button>
891
+ </div>
892
+ </div>
893
+ </div>
894
+
895
+ <div class="modal-overlay" id="inviteTeamModal">
896
+ <div class="modal">
897
+ <div class="modal-header"><h2>Invite Team Member</h2><button class="modal-close" onclick="closeModal('inviteTeamModal')">&times;</button></div>
898
+ <div class="modal-body">
899
+ <div class="alert alert-error" id="inviteTeamError"></div>
900
+ <div class="form-group"><label>Email</label><input type="email" class="form-input" id="teamEmail" placeholder="user@example.com"></div>
901
+ <div class="form-group"><label>Name</label><input type="text" class="form-input" id="teamName" placeholder="Full Name"></div>
902
+ <div class="form-group"><label>Password</label><input type="password" class="form-input" id="teamPassword" placeholder="Initial password"></div>
903
+ <div class="form-group">
904
+ <label>Role</label>
905
+ <select class="form-input" id="teamRole">
906
+ <option value="viewer">Viewer</option>
907
+ <option value="editor">Editor</option>
908
+ <option value="admin">Admin</option>
909
+ </select>
910
+ </div>
911
+ <div class="form-group">
912
+ <label>Site Access</label>
913
+ <div class="checkbox-group" id="teamSiteAccess"></div>
914
+ </div>
915
+ <div class="form-group"><label>Monthly Quota (actions)</label><input type="number" class="form-input" id="teamQuota" placeholder="Leave empty for unlimited"></div>
916
+ </div>
917
+ <div class="modal-footer">
918
+ <button class="btn btn-secondary" onclick="closeModal('inviteTeamModal')">Cancel</button>
919
+ <button class="btn btn-primary" onclick="inviteTeamMember()">Invite</button>
920
+ </div>
921
+ </div>
922
+ </div>
923
+
924
+ <div class="modal-overlay" id="createTicketModal">
925
+ <div class="modal">
926
+ <div class="modal-header"><h2>Create Ticket</h2><button class="modal-close" onclick="closeModal('createTicketModal')">&times;</button></div>
927
+ <div class="modal-body">
928
+ <div class="alert alert-error" id="createTicketError"></div>
929
+ <div class="form-group"><label>Subject</label><input type="text" class="form-input" id="ticketSubject" placeholder="Describe your issue"></div>
930
+ <div class="form-group">
931
+ <label>Priority</label>
932
+ <select class="form-input" id="ticketPriority">
933
+ <option value="low">Low</option>
934
+ <option value="normal" selected>Normal</option>
935
+ <option value="high">High</option>
936
+ <option value="urgent">Urgent</option>
937
+ </select>
938
+ </div>
939
+ <div class="form-group">
940
+ <label>Category</label>
941
+ <select class="form-input" id="ticketCategory">
942
+ <option value="general">General</option>
943
+ <option value="billing">Billing</option>
944
+ <option value="technical">Technical</option>
945
+ <option value="feature">Feature Request</option>
946
+ <option value="bug">Bug Report</option>
947
+ </select>
948
+ </div>
949
+ </div>
950
+ <div class="modal-footer">
951
+ <button class="btn btn-secondary" onclick="closeModal('createTicketModal')">Cancel</button>
952
+ <button class="btn btn-primary" onclick="createTicket()">Create</button>
953
+ </div>
954
+ </div>
955
+ </div>
956
+
957
+ <div class="modal-overlay" id="ticketDetailModal">
958
+ <div class="modal" style="max-width:700px;">
959
+ <div class="modal-header"><h2 id="ticketDetailTitle">Ticket</h2><button class="modal-close" onclick="closeModal('ticketDetailModal')">&times;</button></div>
960
+ <div class="modal-body">
961
+ <div style="display:flex;gap:10px;align-items:center;margin-bottom:16px;">
962
+ <label style="font-size:0.85rem;font-weight:600;color:var(--text-secondary);">Status:</label>
963
+ <select class="form-input" id="ticketStatusSelect" style="width:auto;min-width:140px;" onchange="updateTicketStatus()">
964
+ <option value="open">Open</option>
965
+ <option value="in_progress">In Progress</option>
966
+ <option value="waiting">Waiting</option>
967
+ <option value="resolved">Resolved</option>
968
+ <option value="closed">Closed</option>
969
+ </select>
970
+ </div>
971
+ <div class="msg-list" id="ticketMessages"></div>
972
+ <div class="form-group" style="margin-bottom:0;">
973
+ <textarea class="form-input" id="ticketReply" rows="3" placeholder="Type your reply..."></textarea>
974
+ </div>
975
+ </div>
976
+ <div class="modal-footer">
977
+ <button class="btn btn-secondary" onclick="closeModal('ticketDetailModal')">Close</button>
978
+ <button class="btn btn-primary" onclick="sendTicketReply()">Send Reply</button>
979
+ </div>
980
+ </div>
981
+ </div>
982
+
983
+ <div class="modal-overlay" id="packDetailModal">
984
+ <div class="modal" style="max-width:600px;">
985
+ <div class="modal-header"><h2 id="packDetailTitle">Pack Details</h2><button class="modal-close" onclick="closeModal('packDetailModal')">&times;</button></div>
986
+ <div class="modal-body" id="packDetailBody"></div>
987
+ <div class="modal-footer">
988
+ <button class="btn btn-secondary" onclick="closeModal('packDetailModal')">Close</button>
989
+ </div>
990
+ </div>
991
+ </div>
992
+
993
+ <script>
994
+ const PAPI = '/api/premium';
995
+ const API = '/api';
996
+ let token = localStorage.getItem('wab_token');
997
+ let user = JSON.parse(localStorage.getItem('wab_user') || 'null');
998
+ let sites = [];
999
+ let currentSandboxId = null;
1000
+ let currentTicketId = null;
1001
+ let auditPage = 0;
1002
+ const AUDIT_LIMIT = 25;
1003
+
1004
+ if (!token) window.location.href = '/login';
1005
+
1006
+ function headers() {
1007
+ return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.getItem('wab_token') };
1008
+ }
1009
+
1010
+ function logout() {
1011
+ localStorage.removeItem('wab_token');
1012
+ localStorage.removeItem('wab_user');
1013
+ window.location.href = '/login';
1014
+ }
1015
+
1016
+ function esc(s) {
1017
+ if (!s) return '';
1018
+ const d = document.createElement('div');
1019
+ d.textContent = s;
1020
+ return d.innerHTML;
1021
+ }
1022
+
1023
+ function fmtDate(d) {
1024
+ if (!d) return '—';
1025
+ try { return new Date(d).toLocaleString(); } catch { return d; }
1026
+ }
1027
+
1028
+ function fmtBytes(b) {
1029
+ if (!b || b === 0) return '0 B';
1030
+ const u = ['B', 'KB', 'MB', 'GB'];
1031
+ let i = 0;
1032
+ while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
1033
+ return b.toFixed(i ? 1 : 0) + ' ' + u[i];
1034
+ }
1035
+
1036
+ function openModal(id) { document.getElementById(id).classList.add('active'); }
1037
+ function closeModal(id) { document.getElementById(id).classList.remove('active'); }
1038
+
1039
+ function getSiteId() {
1040
+ const view = document.querySelector('.view.active');
1041
+ if (!view) return null;
1042
+ const sel = view.querySelector('.site-selector');
1043
+ return sel ? sel.value : null;
1044
+ }
1045
+
1046
+ // ─── Navigation ──────────────────────────────────────────────────────
1047
+ document.querySelectorAll('.sidebar-nav a[data-view]').forEach(link => {
1048
+ link.addEventListener('click', (e) => {
1049
+ e.preventDefault();
1050
+ switchView(link.dataset.view);
1051
+ });
1052
+ });
1053
+
1054
+ function switchView(view) {
1055
+ document.querySelectorAll('.sidebar-nav a').forEach(a => a.classList.remove('active'));
1056
+ const link = document.querySelector(`.sidebar-nav a[data-view="${view}"]`);
1057
+ if (link) link.classList.add('active');
1058
+ document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
1059
+ const el = document.getElementById('view-' + view);
1060
+ if (el) el.classList.add('active');
1061
+ onViewActivated(view);
1062
+ }
1063
+
1064
+ function onViewActivated(view) {
1065
+ switch (view) {
1066
+ case 'overview': loadOverview(); break;
1067
+ case 'traffic': loadTraffic(); break;
1068
+ case 'security': loadSecurity(); break;
1069
+ case 'actions': loadPacks(); loadInstalledPacks(); break;
1070
+ case 'agents': loadAgents(); break;
1071
+ case 'webhooks': loadWebhooksView(); break;
1072
+ case 'team': loadTeam(); break;
1073
+ case 'support': loadSupport(); break;
1074
+ case 'script': loadScriptPlugins(); loadScriptConfig(); break;
1075
+ case 'stealth': loadStealth(); break;
1076
+ case 'cdn': loadCdn(); break;
1077
+ case 'audit': loadAudit(); break;
1078
+ case 'sandbox': loadSandboxes(); break;
1079
+ case 'memory': loadMemoryView(); break;
1080
+ case 'healing': loadHealingView(); break;
1081
+ case 'vision': loadVisionView(); break;
1082
+ case 'swarm': loadSwarmView(); break;
1083
+ case 'marketplace': loadMarketplaceView(); break;
1084
+ }
1085
+ }
1086
+
1087
+ // ─── Site Loading ────────────────────────────────────────────────────
1088
+ async function loadSites() {
1089
+ try {
1090
+ const res = await fetch(API + '/sites', { headers: headers() });
1091
+ if (res.status === 401 || res.status === 403) return logout();
1092
+ const data = await res.json();
1093
+ sites = data.sites || [];
1094
+ populateSiteSelectors();
1095
+ } catch (err) { console.error('loadSites:', err); }
1096
+ }
1097
+
1098
+ function populateSiteSelectors() {
1099
+ const opts = '<option value="">Select a site</option>' + sites.map(s => '<option value="' + s.id + '">' + esc(s.name) + ' (' + esc(s.domain) + ')</option>').join('');
1100
+ document.querySelectorAll('.site-selector').forEach(sel => { sel.innerHTML = opts; });
1101
+ const teamSA = document.getElementById('teamSiteAccess');
1102
+ if (teamSA) {
1103
+ teamSA.innerHTML = '<label><input type="checkbox" value="*" checked> All Sites</label>' + sites.map(s => '<label><input type="checkbox" value="' + s.id + '"> ' + esc(s.name) + '</label>').join('');
1104
+ }
1105
+ }
1106
+
1107
+ // ─── Overview ────────────────────────────────────────────────────────
1108
+ async function loadOverview() {
1109
+ const promises = [];
1110
+ if (sites.length > 0) {
1111
+ const sid = sites[0].id;
1112
+ promises.push(
1113
+ fetch(PAPI + '/traffic/' + sid + '/alerts?limit=10&acknowledged=false', { headers: headers() }).then(r => r.json()).catch(() => ({ alerts: [] })),
1114
+ fetch(PAPI + '/security/' + sid + '/events?limit=50&since=' + new Date(Date.now() - 86400000).toISOString(), { headers: headers() }).then(r => r.json()).catch(() => ({ events: [] })),
1115
+ fetch(PAPI + '/actions/' + sid + '/installed', { headers: headers() }).then(r => r.json()).catch(() => ({ installed: [] })),
1116
+ fetch(PAPI + '/agents/' + sid, { headers: headers() }).then(r => r.json()).catch(() => ({ agents: [] }))
1117
+ );
1118
+ } else {
1119
+ promises.push(Promise.resolve({ alerts: [] }), Promise.resolve({ events: [] }), Promise.resolve({ installed: [] }), Promise.resolve({ agents: [] }));
1120
+ }
1121
+ promises.push(
1122
+ fetch(PAPI + '/team', { headers: headers() }).then(r => r.json()).catch(() => ({ subUsers: [] })),
1123
+ fetch(PAPI + '/support/stats', { headers: headers() }).then(r => r.json()).catch(() => ({ stats: { open: 0 } }))
1124
+ );
1125
+
1126
+ const [alertsData, eventsData, packsData, agentsData, teamData, ticketData] = await Promise.all(promises);
1127
+
1128
+ document.getElementById('ovAlerts').textContent = (alertsData.alerts || []).length;
1129
+ document.getElementById('ovSecurity').textContent = (eventsData.events || []).length;
1130
+ document.getElementById('ovPacks').textContent = (packsData.installed || []).length;
1131
+ document.getElementById('ovAgents').textContent = (agentsData.agents || []).filter(a => a.status === 'active').length;
1132
+ document.getElementById('ovTeam').textContent = (teamData.subUsers || []).length;
1133
+ document.getElementById('ovTickets').textContent = (ticketData.stats || {}).open || 0;
1134
+
1135
+ const alerts = alertsData.alerts || [];
1136
+ const tbody = document.getElementById('ovAlertsTable');
1137
+ if (alerts.length === 0) {
1138
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No recent alerts</td></tr>';
1139
+ } else {
1140
+ tbody.innerHTML = alerts.slice(0, 10).map(a => '<tr>' +
1141
+ '<td>' + esc(a.alert_type) + '</td>' +
1142
+ '<td><span class="badge sev-' + (a.severity || 'low') + '">' + esc(a.severity) + '</span></td>' +
1143
+ '<td>' + esc(a.message) + '</td>' +
1144
+ '<td>' + fmtDate(a.created_at) + '</td>' +
1145
+ '<td><button class="btn btn-sm btn-secondary" onclick="ackAlertOverview(\'' + a.id + '\')">Acknowledge</button></td>' +
1146
+ '</tr>').join('');
1147
+ }
1148
+ }
1149
+
1150
+ async function ackAlertOverview(alertId) {
1151
+ if (!sites.length) return;
1152
+ await fetch(PAPI + '/traffic/' + sites[0].id + '/alerts/' + alertId + '/acknowledge', { method: 'POST', headers: headers() });
1153
+ loadOverview();
1154
+ }
1155
+
1156
+ async function quickCheckAnomalies() {
1157
+ if (!sites.length) { alert('No sites available'); return; }
1158
+ const res = await fetch(PAPI + '/traffic/' + sites[0].id + '/check-anomalies', { method: 'POST', headers: headers() });
1159
+ const data = await res.json();
1160
+ alert('Anomaly check complete. ' + (data.alerts || []).length + ' new alerts found.');
1161
+ loadOverview();
1162
+ }
1163
+
1164
+ async function quickSecurityReport() {
1165
+ if (!sites.length) { alert('No sites available'); return; }
1166
+ switchView('security');
1167
+ setTimeout(() => {
1168
+ const sel = document.querySelector('#view-security .site-selector');
1169
+ if (sel && sites.length) { sel.value = sites[0].id; loadSecurity(); loadSecurityReport(); }
1170
+ }, 100);
1171
+ }
1172
+
1173
+ // ─── Traffic Intelligence ────────────────────────────────────────────
1174
+ async function loadTraffic() {
1175
+ const sid = getSiteId();
1176
+ if (!sid) return;
1177
+ try {
1178
+ const [profilesRes, statsRes, alertsRes] = await Promise.all([
1179
+ fetch(PAPI + '/traffic/' + sid + '/profiles?limit=50', { headers: headers() }),
1180
+ fetch(PAPI + '/traffic/' + sid + '/stats?days=30', { headers: headers() }),
1181
+ fetch(PAPI + '/traffic/' + sid + '/alerts?limit=20', { headers: headers() })
1182
+ ]);
1183
+ const profiles = await profilesRes.json();
1184
+ const stats = await statsRes.json();
1185
+ const alerts = await alertsRes.json();
1186
+
1187
+ const s = stats.stats || {};
1188
+ document.getElementById('tfTotal').textContent = s.totalAgents || 0;
1189
+ const byType = s.byType || [];
1190
+ document.getElementById('tfFriendly').textContent = (byType.find(t => t.agent_type === 'friendly') || {}).count || 0;
1191
+ document.getElementById('tfSuspicious').textContent = (byType.find(t => t.agent_type === 'suspicious') || {}).count || 0;
1192
+ document.getElementById('tfAggressive').textContent = (byType.find(t => t.agent_type === 'aggressive') || {}).count || 0;
1193
+
1194
+ const profs = profiles.profiles || [];
1195
+ const tbody = document.getElementById('trafficTable');
1196
+ if (profs.length === 0) {
1197
+ tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No agent profiles</td></tr>';
1198
+ } else {
1199
+ tbody.innerHTML = profs.map(p => '<tr>' +
1200
+ '<td><strong style="color:var(--text-primary)">' + esc(p.agent_signature) + '</strong></td>' +
1201
+ '<td><span class="badge type-' + (p.agent_type || 'unknown') + '">' + esc(p.agent_type) + '</span></td>' +
1202
+ '<td>' + esc(p.platform) + '</td>' +
1203
+ '<td>' + esc(p.country || '—') + '</td>' +
1204
+ '<td>' + (p.total_requests || 0) + '</td>' +
1205
+ '<td>' + fmtDate(p.last_seen) + '</td>' +
1206
+ '<td><button class="btn btn-danger btn-sm" onclick="blockFromTraffic(\'' + esc(p.agent_signature) + '\')">Block</button></td>' +
1207
+ '</tr>').join('');
1208
+ }
1209
+
1210
+ const al = alerts.alerts || [];
1211
+ const atbody = document.getElementById('trafficAlerts');
1212
+ if (al.length === 0) {
1213
+ atbody.innerHTML = '<tr><td colspan="5" class="empty-state">No alerts</td></tr>';
1214
+ } else {
1215
+ atbody.innerHTML = al.map(a => '<tr>' +
1216
+ '<td>' + esc(a.alert_type) + '</td>' +
1217
+ '<td><span class="badge sev-' + (a.severity || 'low') + '">' + esc(a.severity) + '</span></td>' +
1218
+ '<td>' + esc(a.message) + '</td>' +
1219
+ '<td>' + fmtDate(a.created_at) + '</td>' +
1220
+ '<td>' + (a.acknowledged ? '<span class="badge badge-active">Ack</span>' : '<button class="btn btn-sm btn-secondary" onclick="ackAlert(\'' + a.id + '\')">Acknowledge</button>') + '</td>' +
1221
+ '</tr>').join('');
1222
+ }
1223
+ } catch (err) { console.error('loadTraffic:', err); }
1224
+ }
1225
+
1226
+ async function blockFromTraffic(sig) {
1227
+ const sid = getSiteId();
1228
+ if (!sid || !confirm('Block agent "' + sig + '"?')) return;
1229
+ await fetch(PAPI + '/security/' + sid + '/block', { method: 'POST', headers: headers(), body: JSON.stringify({ agentSignature: sig, reason: 'Blocked from traffic view' }) });
1230
+ alert('Agent blocked');
1231
+ }
1232
+
1233
+ async function ackAlert(alertId) {
1234
+ const sid = getSiteId();
1235
+ if (!sid) return;
1236
+ await fetch(PAPI + '/traffic/' + sid + '/alerts/' + alertId + '/acknowledge', { method: 'POST', headers: headers() });
1237
+ loadTraffic();
1238
+ }
1239
+
1240
+ async function checkAnomalies() {
1241
+ const sid = getSiteId();
1242
+ if (!sid) { alert('Select a site first'); return; }
1243
+ const res = await fetch(PAPI + '/traffic/' + sid + '/check-anomalies', { method: 'POST', headers: headers() });
1244
+ const data = await res.json();
1245
+ alert('Check complete. ' + (data.alerts || []).length + ' new alerts.');
1246
+ loadTraffic();
1247
+ }
1248
+
1249
+ // ─── Exploit Shield ──────────────────────────────────────────────────
1250
+ async function loadSecurity() {
1251
+ const sid = getSiteId();
1252
+ if (!sid) return;
1253
+ try {
1254
+ const [eventsRes, blockedRes, reportRes] = await Promise.all([
1255
+ fetch(PAPI + '/security/' + sid + '/events?limit=50', { headers: headers() }),
1256
+ fetch(PAPI + '/security/' + sid + '/blocked', { headers: headers() }),
1257
+ fetch(PAPI + '/security/' + sid + '/report?days=1', { headers: headers() })
1258
+ ]);
1259
+ const events = await eventsRes.json();
1260
+ const blocked = await blockedRes.json();
1261
+ const report = await reportRes.json();
1262
+
1263
+ const r = report.report || {};
1264
+ document.getElementById('secTotal').textContent = r.totalEvents || 0;
1265
+ const sevDist = r.severityDist || [];
1266
+ document.getElementById('secCritical').textContent = (sevDist.find(s => s.severity === 'critical') || {}).count || 0;
1267
+ document.getElementById('secHigh').textContent = (sevDist.find(s => s.severity === 'high') || {}).count || 0;
1268
+ document.getElementById('secBlocked').textContent = r.activeBlocks || 0;
1269
+
1270
+ const ev = events.events || [];
1271
+ const etbody = document.getElementById('secEventsTable');
1272
+ if (ev.length === 0) {
1273
+ etbody.innerHTML = '<tr><td colspan="5" class="empty-state">No security events</td></tr>';
1274
+ } else {
1275
+ etbody.innerHTML = ev.map(e => {
1276
+ let details = '';
1277
+ try { details = JSON.stringify(JSON.parse(e.details || '{}'), null, 0).substring(0, 80); } catch { details = e.details || ''; }
1278
+ return '<tr>' +
1279
+ '<td>' + esc(e.event_type) + '</td>' +
1280
+ '<td><span class="badge sev-' + (e.severity || 'low') + '">' + esc(e.severity) + '</span></td>' +
1281
+ '<td>' + esc(e.agent_signature || '—') + '</td>' +
1282
+ '<td style="font-size:0.82rem;color:var(--text-muted)">' + esc(details) + '</td>' +
1283
+ '<td>' + fmtDate(e.created_at) + '</td></tr>';
1284
+ }).join('');
1285
+ }
1286
+
1287
+ const bl = blocked.blocked || [];
1288
+ const btbody = document.getElementById('secBlockedTable');
1289
+ if (bl.length === 0) {
1290
+ btbody.innerHTML = '<tr><td colspan="5" class="empty-state">No blocked agents</td></tr>';
1291
+ } else {
1292
+ btbody.innerHTML = bl.map(b => '<tr>' +
1293
+ '<td><strong style="color:var(--text-primary)">' + esc(b.agent_signature) + '</strong></td>' +
1294
+ '<td>' + esc(b.reason || '—') + '</td>' +
1295
+ '<td>' + fmtDate(b.blocked_at) + '</td>' +
1296
+ '<td>' + (b.expires_at ? fmtDate(b.expires_at) : 'Never') + '</td>' +
1297
+ '<td><button class="btn btn-sm btn-secondary" onclick="unblockAgent(\'' + b.id + '\')">Unblock</button></td>' +
1298
+ '</tr>').join('');
1299
+ }
1300
+ } catch (err) { console.error('loadSecurity:', err); }
1301
+ }
1302
+
1303
+ async function blockAgent() {
1304
+ const sid = getSiteId();
1305
+ if (!sid) { alert('Select a site first'); return; }
1306
+ const sig = document.getElementById('blockSig').value.trim();
1307
+ const reason = document.getElementById('blockReason').value.trim();
1308
+ if (!sig) { alert('Agent signature is required'); return; }
1309
+ await fetch(PAPI + '/security/' + sid + '/block', { method: 'POST', headers: headers(), body: JSON.stringify({ agentSignature: sig, reason: reason }) });
1310
+ document.getElementById('blockSig').value = '';
1311
+ document.getElementById('blockReason').value = '';
1312
+ loadSecurity();
1313
+ }
1314
+
1315
+ async function unblockAgent(blockId) {
1316
+ const sid = getSiteId();
1317
+ if (!sid) return;
1318
+ await fetch(PAPI + '/security/' + sid + '/block/' + blockId, { method: 'DELETE', headers: headers() });
1319
+ loadSecurity();
1320
+ }
1321
+
1322
+ async function loadSecurityReport() {
1323
+ const sid = getSiteId();
1324
+ if (!sid) { alert('Select a site first'); return; }
1325
+ const days = document.getElementById('reportDays').value;
1326
+ const res = await fetch(PAPI + '/security/' + sid + '/report?days=' + days, { headers: headers() });
1327
+ const data = await res.json();
1328
+ const r = data.report || {};
1329
+ document.getElementById('secReportContent').innerHTML =
1330
+ '<div class="card" style="margin-top:16px;">' +
1331
+ '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:20px;">' +
1332
+ '<div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Total Events</div><div style="font-size:1.5rem;font-weight:700;">' + (r.totalEvents || 0) + '</div></div>' +
1333
+ '<div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Active Blocks</div><div style="font-size:1.5rem;font-weight:700;">' + (r.activeBlocks || 0) + '</div></div>' +
1334
+ '<div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Event Types</div><div style="font-size:1.5rem;font-weight:700;">' + (r.eventsByType || []).length + '</div></div>' +
1335
+ '</div>' +
1336
+ '<h4 style="margin-bottom:8px;">Severity Distribution</h4>' +
1337
+ '<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px;">' +
1338
+ (r.severityDist || []).map(s => '<span class="badge sev-' + s.severity + '">' + s.severity + ': ' + s.count + '</span>').join('') +
1339
+ '</div>' +
1340
+ '<h4 style="margin-bottom:8px;">Top Blocked Agents</h4>' +
1341
+ ((r.topBlocked || []).length ? '<ul style="list-style:none;padding:0;">' + (r.topBlocked || []).map(b => '<li style="padding:4px 0;color:var(--text-secondary);">' + esc(b.agent_signature) + ' <span class="badge badge-free">' + b.count + 'x</span></li>').join('') + '</ul>' : '<p style="color:var(--text-muted);">None</p>') +
1342
+ '</div>';
1343
+ }
1344
+
1345
+ // ─── Actions Library ─────────────────────────────────────────────────
1346
+ async function loadPacks() {
1347
+ const platform = document.getElementById('packPlatformFilter').value;
1348
+ const url = PAPI + '/actions/packs' + (platform ? '?platform=' + encodeURIComponent(platform) : '');
1349
+ try {
1350
+ const res = await fetch(url, { headers: headers() });
1351
+ const data = await res.json();
1352
+ const packs = data.packs || [];
1353
+ const grid = document.getElementById('packsGrid');
1354
+ if (packs.length === 0) {
1355
+ grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;">No packs available</div>';
1356
+ return;
1357
+ }
1358
+ grid.innerHTML = packs.map(p => '<div class="card pack-card" onclick="showPackDetail(\'' + p.id + '\')">' +
1359
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px;">' +
1360
+ '<h3 style="font-size:1rem;">' + esc(p.icon || '📦') + ' ' + esc(p.name) + '</h3>' +
1361
+ '<span class="badge badge-' + (p.tier_required || 'free') + '">' + esc(p.tier_required || 'free') + '</span>' +
1362
+ '</div>' +
1363
+ '<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:12px;">' + esc(p.description) + '</p>' +
1364
+ '<div style="display:flex;justify-content:space-between;align-items:center;">' +
1365
+ '<span style="font-size:0.8rem;color:var(--text-muted);">' + esc(p.platform || '—') + ' · v' + esc(p.version || '1.0') + '</span>' +
1366
+ '<button class="btn btn-primary btn-sm" onclick="event.stopPropagation();installPack(\'' + p.id + '\')">Install</button>' +
1367
+ '</div></div>').join('');
1368
+
1369
+ const platforms = [...new Set(packs.map(p => p.platform).filter(Boolean))];
1370
+ const filterSel = document.getElementById('packPlatformFilter');
1371
+ const current = filterSel.value;
1372
+ filterSel.innerHTML = '<option value="">All Platforms</option>' + platforms.map(p => '<option value="' + esc(p) + '"' + (p === current ? ' selected' : '') + '>' + esc(p) + '</option>').join('');
1373
+ } catch (err) { console.error('loadPacks:', err); }
1374
+ }
1375
+
1376
+ async function showPackDetail(packId) {
1377
+ try {
1378
+ const [packRes, actionsRes] = await Promise.all([
1379
+ fetch(PAPI + '/actions/packs/' + packId, { headers: headers() }),
1380
+ fetch(PAPI + '/actions/packs/' + packId + '/actions', { headers: headers() })
1381
+ ]);
1382
+ const packData = await packRes.json();
1383
+ const actionsData = await actionsRes.json();
1384
+ const p = packData.pack;
1385
+ const actions = actionsData.actions || [];
1386
+ document.getElementById('packDetailTitle').textContent = (p.icon || '📦') + ' ' + (p.name || 'Pack');
1387
+ document.getElementById('packDetailBody').innerHTML =
1388
+ '<p style="color:var(--text-secondary);margin-bottom:16px;">' + esc(p.description) + '</p>' +
1389
+ '<div style="display:flex;gap:12px;margin-bottom:16px;">' +
1390
+ '<span class="badge badge-' + (p.tier_required || 'free') + '">' + esc(p.tier_required) + '</span>' +
1391
+ '<span style="font-size:0.85rem;color:var(--text-muted);">' + esc(p.platform) + ' · v' + esc(p.version) + '</span></div>' +
1392
+ '<h4 style="margin-bottom:8px;">Actions (' + actions.length + ')</h4>' +
1393
+ (actions.length ? '<div class="table-wrapper"><table><thead><tr><th>Name</th><th>Type</th></tr></thead><tbody>' +
1394
+ actions.map(a => '<tr><td>' + esc(a.name || a.action || '—') + '</td><td>' + esc(a.type || a.trigger_type || '—') + '</td></tr>').join('') +
1395
+ '</tbody></table></div>' : '<p style="color:var(--text-muted);">No action details available</p>');
1396
+ openModal('packDetailModal');
1397
+ } catch (err) { console.error('showPackDetail:', err); }
1398
+ }
1399
+
1400
+ async function installPack(packId) {
1401
+ const sid = getSiteId();
1402
+ if (!sid) { alert('Select a site first'); return; }
1403
+ try {
1404
+ const res = await fetch(PAPI + '/actions/' + sid + '/install', { method: 'POST', headers: headers(), body: JSON.stringify({ packId: packId }) });
1405
+ if (res.ok) { alert('Pack installed'); loadInstalledPacks(); } else { const d = await res.json(); alert(d.error || 'Install failed'); }
1406
+ } catch (err) { alert('Connection error'); }
1407
+ }
1408
+
1409
+ async function loadInstalledPacks() {
1410
+ const sid = getSiteId();
1411
+ if (!sid) return;
1412
+ try {
1413
+ const res = await fetch(PAPI + '/actions/' + sid + '/installed', { headers: headers() });
1414
+ const data = await res.json();
1415
+ const installed = data.installed || [];
1416
+ const tbody = document.getElementById('installedPacksTable');
1417
+ if (installed.length === 0) {
1418
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No packs installed</td></tr>';
1419
+ } else {
1420
+ tbody.innerHTML = installed.map(i => '<tr>' +
1421
+ '<td><strong style="color:var(--text-primary)">' + esc(i.name) + '</strong></td>' +
1422
+ '<td>' + esc(i.platform || '—') + '</td>' +
1423
+ '<td>' + esc(i.version || '—') + '</td>' +
1424
+ '<td>' + fmtDate(i.installed_at) + '</td>' +
1425
+ '<td><button class="btn btn-danger btn-sm" onclick="uninstallPack(\'' + i.id + '\')">Uninstall</button></td>' +
1426
+ '</tr>').join('');
1427
+ }
1428
+ } catch (err) { console.error('loadInstalledPacks:', err); }
1429
+ }
1430
+
1431
+ async function uninstallPack(installId) {
1432
+ const sid = getSiteId();
1433
+ if (!sid || !confirm('Uninstall this pack?')) return;
1434
+ await fetch(PAPI + '/actions/' + sid + '/install/' + installId, { method: 'DELETE', headers: headers() });
1435
+ loadInstalledPacks();
1436
+ }
1437
+
1438
+ // ─── Custom Agents ───────────────────────────────────────────────────
1439
+ async function loadAgents() {
1440
+ const sid = getSiteId();
1441
+ if (!sid) return;
1442
+ try {
1443
+ const res = await fetch(PAPI + '/agents/' + sid, { headers: headers() });
1444
+ const data = await res.json();
1445
+ const agents = data.agents || [];
1446
+ const tbody = document.getElementById('agentsTable');
1447
+ if (agents.length === 0) {
1448
+ tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No agents. Create one to get started.</td></tr>';
1449
+ } else {
1450
+ tbody.innerHTML = agents.map(a => '<tr>' +
1451
+ '<td><strong style="color:var(--text-primary)">' + esc(a.name) + '</strong></td>' +
1452
+ '<td><span class="badge status-' + (a.status || 'active') + '">' + esc(a.status || 'active') + '</span></td>' +
1453
+ '<td style="font-family:var(--font-mono);font-size:0.82rem;">' + esc(a.schedule || '—') + '</td>' +
1454
+ '<td>' + (a.run_count || 0) + '</td>' +
1455
+ '<td>' + fmtDate(a.last_run) + '</td>' +
1456
+ '<td><button class="btn btn-sm btn-secondary" onclick="showAgentDetail(\'' + a.id + '\')">Details</button></td>' +
1457
+ '</tr>').join('');
1458
+ }
1459
+ } catch (err) { console.error('loadAgents:', err); }
1460
+ }
1461
+
1462
+ async function createAgent() {
1463
+ const sid = getSiteId();
1464
+ if (!sid) { alert('Select a site first'); return; }
1465
+ const errEl = document.getElementById('createAgentError');
1466
+ errEl.style.display = 'none';
1467
+ const name = document.getElementById('agentName').value.trim();
1468
+ const desc = document.getElementById('agentDesc').value.trim();
1469
+ const schedule = document.getElementById('agentSchedule').value.trim();
1470
+ let steps;
1471
+ try { steps = JSON.parse(document.getElementById('agentSteps').value); } catch { errEl.textContent = 'Steps must be valid JSON'; errEl.style.display = 'block'; return; }
1472
+ if (!name) { errEl.textContent = 'Name is required'; errEl.style.display = 'block'; return; }
1473
+ try {
1474
+ const res = await fetch(PAPI + '/agents/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ name, description: desc, steps, schedule: schedule || undefined }) });
1475
+ if (res.ok) {
1476
+ closeModal('createAgentModal');
1477
+ document.getElementById('agentName').value = '';
1478
+ document.getElementById('agentDesc').value = '';
1479
+ document.getElementById('agentSteps').value = '';
1480
+ document.getElementById('agentSchedule').value = '';
1481
+ loadAgents();
1482
+ } else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
1483
+ } catch { errEl.textContent = 'Connection error'; errEl.style.display = 'block'; }
1484
+ }
1485
+
1486
+ async function showAgentDetail(agentId) {
1487
+ const sid = getSiteId();
1488
+ if (!sid) return;
1489
+ try {
1490
+ const [agentRes, runsRes] = await Promise.all([
1491
+ fetch(PAPI + '/agents/' + sid + '/' + agentId, { headers: headers() }),
1492
+ fetch(PAPI + '/agents/' + sid + '/' + agentId + '/runs?limit=10', { headers: headers() })
1493
+ ]);
1494
+ const agentData = await agentRes.json();
1495
+ const runsData = await runsRes.json();
1496
+ const a = agentData.agent;
1497
+ const runs = runsData.runs || [];
1498
+ document.getElementById('agentDetailTitle').textContent = a.name;
1499
+ let steps = [];
1500
+ try { steps = JSON.parse(a.steps_json || '[]'); } catch {}
1501
+ document.getElementById('agentDetailBody').innerHTML =
1502
+ '<p style="color:var(--text-secondary);margin-bottom:16px;">' + esc(a.description || 'No description') + '</p>' +
1503
+ '<div style="display:flex;gap:12px;margin-bottom:16px;">' +
1504
+ '<span class="badge status-' + (a.status || 'active') + '">' + esc(a.status) + '</span>' +
1505
+ (a.schedule ? '<span style="font-size:0.85rem;color:var(--text-muted);">Schedule: ' + esc(a.schedule) + '</span>' : '') +
1506
+ '<span style="font-size:0.85rem;color:var(--text-muted);">Runs: ' + (a.run_count || 0) + '</span></div>' +
1507
+ '<h4 style="margin-bottom:8px;">Steps</h4>' +
1508
+ '<div class="snippet-box" style="margin-bottom:20px;"><pre>' + esc(JSON.stringify(steps, null, 2)) + '</pre></div>' +
1509
+ '<h4 style="margin-bottom:8px;">Run History</h4>' +
1510
+ (runs.length ? '<div class="table-wrapper"><table><thead><tr><th>Status</th><th>Started</th><th>Finished</th></tr></thead><tbody>' +
1511
+ runs.map(r => '<tr><td><span class="badge status-' + (r.status || 'running') + '">' + esc(r.status) + '</span></td><td>' + fmtDate(r.started_at) + '</td><td>' + fmtDate(r.finished_at) + '</td></tr>').join('') +
1512
+ '</tbody></table></div>' : '<p style="color:var(--text-muted);">No runs yet</p>');
1513
+
1514
+ document.getElementById('agentRunBtn').onclick = async () => {
1515
+ await fetch(PAPI + '/agents/' + sid + '/' + agentId + '/run', { method: 'POST', headers: headers() });
1516
+ alert('Agent run started');
1517
+ showAgentDetail(agentId);
1518
+ loadAgents();
1519
+ };
1520
+ document.getElementById('agentDeleteBtn').onclick = async () => {
1521
+ if (!confirm('Delete agent "' + a.name + '"?')) return;
1522
+ await fetch(PAPI + '/agents/' + sid + '/' + agentId, { method: 'DELETE', headers: headers() });
1523
+ closeModal('agentDetailModal');
1524
+ loadAgents();
1525
+ };
1526
+ openModal('agentDetailModal');
1527
+ } catch (err) { console.error('showAgentDetail:', err); }
1528
+ }
1529
+
1530
+ // ─── Webhooks & CRM ─────────────────────────────────────────────────
1531
+ async function loadWebhooksView() {
1532
+ const sid = getSiteId();
1533
+ if (!sid) return;
1534
+ try {
1535
+ const [whRes, crmRes] = await Promise.all([
1536
+ fetch(PAPI + '/webhooks/' + sid, { headers: headers() }),
1537
+ fetch(PAPI + '/crm/' + sid, { headers: headers() })
1538
+ ]);
1539
+ const whData = await whRes.json();
1540
+ const crmData = await crmRes.json();
1541
+
1542
+ const wh = whData.webhooks || [];
1543
+ const wtb = document.getElementById('webhooksTable');
1544
+ if (wh.length === 0) {
1545
+ wtb.innerHTML = '<tr><td colspan="6" class="empty-state">No webhooks</td></tr>';
1546
+ } else {
1547
+ wtb.innerHTML = wh.map(w => {
1548
+ let events = [];
1549
+ try { events = JSON.parse(w.events || '[]'); } catch {}
1550
+ return '<tr>' +
1551
+ '<td><strong style="color:var(--text-primary)">' + esc(w.name) + '</strong></td>' +
1552
+ '<td style="font-family:var(--font-mono);font-size:0.8rem;">' + esc(w.url) + '</td>' +
1553
+ '<td>' + events.map(e => '<span class="badge badge-free" style="margin:1px;">' + esc(e) + '</span>').join(' ') + '</td>' +
1554
+ '<td><span class="badge ' + (w.active ? 'badge-active' : 'badge-inactive') + '">' + (w.active ? 'Active' : 'Inactive') + '</span></td>' +
1555
+ '<td>' + (w.failure_count || 0) + '</td>' +
1556
+ '<td style="display:flex;gap:4px;"><button class="btn btn-sm btn-secondary" onclick="viewWebhookLogs(\'' + w.id + '\')">Logs</button><button class="btn btn-sm btn-danger" onclick="deleteWebhook(\'' + w.id + '\')">Delete</button></td>' +
1557
+ '</tr>';
1558
+ }).join('');
1559
+ }
1560
+
1561
+ const crm = crmData.integrations || [];
1562
+ const ctb = document.getElementById('crmTable');
1563
+ if (crm.length === 0) {
1564
+ ctb.innerHTML = '<tr><td colspan="4" class="empty-state">No CRM integrations</td></tr>';
1565
+ } else {
1566
+ ctb.innerHTML = crm.map(c => '<tr>' +
1567
+ '<td><strong style="color:var(--text-primary);text-transform:capitalize;">' + esc(c.provider) + '</strong></td>' +
1568
+ '<td><span class="badge ' + (c.active ? 'badge-active' : 'badge-inactive') + '">' + (c.active ? 'Active' : 'Inactive') + '</span></td>' +
1569
+ '<td>' + fmtDate(c.last_sync) + '</td>' +
1570
+ '<td><button class="btn btn-sm btn-danger" onclick="deleteCrm(\'' + c.id + '\')">Delete</button></td>' +
1571
+ '</tr>').join('');
1572
+ }
1573
+ } catch (err) { console.error('loadWebhooksView:', err); }
1574
+ }
1575
+
1576
+ async function createWebhook() {
1577
+ const sid = getSiteId();
1578
+ if (!sid) { alert('Select a site first'); return; }
1579
+ const errEl = document.getElementById('createWebhookError');
1580
+ errEl.style.display = 'none';
1581
+ const name = document.getElementById('whName').value.trim();
1582
+ const url = document.getElementById('whUrl').value.trim();
1583
+ const secret = document.getElementById('whSecret').value.trim();
1584
+ const events = [...document.querySelectorAll('#whEvents input:checked')].map(c => c.value);
1585
+ if (!name || !url) { errEl.textContent = 'Name and URL are required'; errEl.style.display = 'block'; return; }
1586
+ if (events.length === 0) { errEl.textContent = 'Select at least one event'; errEl.style.display = 'block'; return; }
1587
+ const res = await fetch(PAPI + '/webhooks/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ name, url, events, secret: secret || undefined }) });
1588
+ if (res.ok) {
1589
+ closeModal('createWebhookModal');
1590
+ document.getElementById('whName').value = '';
1591
+ document.getElementById('whUrl').value = '';
1592
+ document.getElementById('whSecret').value = '';
1593
+ loadWebhooksView();
1594
+ } else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
1595
+ }
1596
+
1597
+ async function deleteWebhook(whId) {
1598
+ const sid = getSiteId();
1599
+ if (!sid || !confirm('Delete this webhook?')) return;
1600
+ await fetch(PAPI + '/webhooks/' + sid + '/' + whId, { method: 'DELETE', headers: headers() });
1601
+ loadWebhooksView();
1602
+ }
1603
+
1604
+ async function viewWebhookLogs(whId) {
1605
+ const sid = getSiteId();
1606
+ if (!sid) return;
1607
+ const res = await fetch(PAPI + '/webhooks/' + sid + '/' + whId + '/logs?limit=20', { headers: headers() });
1608
+ const data = await res.json();
1609
+ const logs = data.logs || [];
1610
+ document.getElementById('webhookLogsSection').style.display = 'block';
1611
+ const tbody = document.getElementById('webhookLogsTable');
1612
+ if (logs.length === 0) {
1613
+ tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No logs</td></tr>';
1614
+ } else {
1615
+ tbody.innerHTML = logs.map(l => '<tr>' +
1616
+ '<td>' + esc(l.event_type) + '</td>' +
1617
+ '<td><span class="badge ' + (l.response_code < 400 ? 'badge-active' : 'badge-inactive') + '">' + l.response_code + '</span></td>' +
1618
+ '<td style="font-size:0.8rem;color:var(--text-muted);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + esc((l.response_body || '').substring(0, 100)) + '</td>' +
1619
+ '<td>' + fmtDate(l.created_at) + '</td></tr>').join('');
1620
+ }
1621
+ }
1622
+
1623
+ async function testWebhook() {
1624
+ const sid = getSiteId();
1625
+ if (!sid) { alert('Select a site first'); return; }
1626
+ const res = await fetch(PAPI + '/webhooks/' + sid + '/test', { method: 'POST', headers: headers(), body: JSON.stringify({ eventType: 'test.ping', payload: { test: true, timestamp: new Date().toISOString() } }) });
1627
+ const data = await res.json();
1628
+ const results = data.results || [];
1629
+ alert('Test sent to ' + results.length + ' webhook(s). Success: ' + results.filter(r => r.success).length);
1630
+ loadWebhooksView();
1631
+ }
1632
+
1633
+ async function createCrmIntegration() {
1634
+ const sid = getSiteId();
1635
+ if (!sid) { alert('Select a site first'); return; }
1636
+ const errEl = document.getElementById('createCrmError');
1637
+ errEl.style.display = 'none';
1638
+ const provider = document.getElementById('crmProvider').value;
1639
+ let config;
1640
+ try { config = JSON.parse(document.getElementById('crmConfig').value || '{}'); } catch { errEl.textContent = 'Config must be valid JSON'; errEl.style.display = 'block'; return; }
1641
+ const res = await fetch(PAPI + '/crm/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ provider, config }) });
1642
+ if (res.ok) { closeModal('createCrmModal'); document.getElementById('crmConfig').value = ''; loadWebhooksView(); }
1643
+ else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
1644
+ }
1645
+
1646
+ async function deleteCrm(intId) {
1647
+ const sid = getSiteId();
1648
+ if (!sid || !confirm('Delete this integration?')) return;
1649
+ await fetch(PAPI + '/crm/' + sid + '/' + intId, { method: 'DELETE', headers: headers() });
1650
+ loadWebhooksView();
1651
+ }
1652
+
1653
+ // ─── Team Management ─────────────────────────────────────────────────
1654
+ async function loadTeam() {
1655
+ try {
1656
+ const res = await fetch(PAPI + '/team', { headers: headers() });
1657
+ const data = await res.json();
1658
+ const members = data.subUsers || [];
1659
+ const tbody = document.getElementById('teamTable');
1660
+ if (members.length === 0) {
1661
+ tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No team members. Invite someone to get started.</td></tr>';
1662
+ } else {
1663
+ tbody.innerHTML = members.map(m => {
1664
+ let access = [];
1665
+ try { access = JSON.parse(m.site_access || '[]'); } catch {}
1666
+ return '<tr>' +
1667
+ '<td>' + esc(m.email) + '</td>' +
1668
+ '<td><strong style="color:var(--text-primary)">' + esc(m.name) + '</strong></td>' +
1669
+ '<td><span class="badge role-' + (m.role || 'viewer') + '">' + esc(m.role) + '</span></td>' +
1670
+ '<td style="font-size:0.82rem;">' + (access.includes('*') ? 'All' : access.length + ' site(s)') + '</td>' +
1671
+ '<td>' + (m.quota_actions_month !== null ? m.quota_actions_month : '∞') + '</td>' +
1672
+ '<td>' + (m.actions_used_month || 0) + '</td>' +
1673
+ '<td><button class="btn btn-sm btn-danger" onclick="deleteTeamMember(\'' + m.id + '\')">Remove</button></td>' +
1674
+ '</tr>';
1675
+ }).join('');
1676
+ }
1677
+ } catch (err) { console.error('loadTeam:', err); }
1678
+ }
1679
+
1680
+ async function inviteTeamMember() {
1681
+ const errEl = document.getElementById('inviteTeamError');
1682
+ errEl.style.display = 'none';
1683
+ const email = document.getElementById('teamEmail').value.trim();
1684
+ const name = document.getElementById('teamName').value.trim();
1685
+ const password = document.getElementById('teamPassword').value;
1686
+ const role = document.getElementById('teamRole').value;
1687
+ const quota = document.getElementById('teamQuota').value;
1688
+ const siteAccess = [...document.querySelectorAll('#teamSiteAccess input:checked')].map(c => c.value);
1689
+ if (!email || !name || !password) { errEl.textContent = 'Email, name, and password are required'; errEl.style.display = 'block'; return; }
1690
+ const res = await fetch(PAPI + '/team', { method: 'POST', headers: headers(), body: JSON.stringify({ email, name, password, role, siteAccess, quotaActionsMonth: quota ? parseInt(quota) : undefined }) });
1691
+ if (res.ok) {
1692
+ closeModal('inviteTeamModal');
1693
+ document.getElementById('teamEmail').value = '';
1694
+ document.getElementById('teamName').value = '';
1695
+ document.getElementById('teamPassword').value = '';
1696
+ document.getElementById('teamQuota').value = '';
1697
+ loadTeam();
1698
+ } else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
1699
+ }
1700
+
1701
+ async function deleteTeamMember(subId) {
1702
+ if (!confirm('Remove this team member?')) return;
1703
+ await fetch(PAPI + '/team/' + subId, { method: 'DELETE', headers: headers() });
1704
+ loadTeam();
1705
+ }
1706
+
1707
+ // ─── Support ─────────────────────────────────────────────────────────
1708
+ async function loadSupport() {
1709
+ try {
1710
+ const [statsRes, ticketsRes] = await Promise.all([
1711
+ fetch(PAPI + '/support/stats', { headers: headers() }),
1712
+ fetch(PAPI + '/support/tickets?limit=50', { headers: headers() })
1713
+ ]);
1714
+ const statsData = await statsRes.json();
1715
+ const ticketsData = await ticketsRes.json();
1716
+
1717
+ const st = statsData.stats || {};
1718
+ document.getElementById('supOpen').textContent = st.open || 0;
1719
+ document.getElementById('supProgress').textContent = st.in_progress || 0;
1720
+ document.getElementById('supWaiting').textContent = st.waiting || 0;
1721
+ document.getElementById('supResolved').textContent = (st.resolved || 0) + (st.closed || 0);
1722
+
1723
+ const tickets = ticketsData.tickets || [];
1724
+ const tbody = document.getElementById('ticketsTable');
1725
+ if (tickets.length === 0) {
1726
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No tickets</td></tr>';
1727
+ } else {
1728
+ tbody.innerHTML = tickets.map(t => '<tr>' +
1729
+ '<td><strong style="color:var(--text-primary)">' + esc(t.subject) + '</strong></td>' +
1730
+ '<td><span class="badge priority-' + (t.priority || 'normal') + '">' + esc(t.priority) + '</span></td>' +
1731
+ '<td><span class="badge status-' + (t.status || 'open') + '">' + esc(t.status) + '</span></td>' +
1732
+ '<td>' + fmtDate(t.created_at) + '</td>' +
1733
+ '<td><button class="btn btn-sm btn-secondary" onclick="showTicketDetail(\'' + t.id + '\')">View</button></td>' +
1734
+ '</tr>').join('');
1735
+ }
1736
+ } catch (err) { console.error('loadSupport:', err); }
1737
+ }
1738
+
1739
+ async function createTicket() {
1740
+ const errEl = document.getElementById('createTicketError');
1741
+ errEl.style.display = 'none';
1742
+ const subject = document.getElementById('ticketSubject').value.trim();
1743
+ const priority = document.getElementById('ticketPriority').value;
1744
+ const category = document.getElementById('ticketCategory').value;
1745
+ if (!subject) { errEl.textContent = 'Subject is required'; errEl.style.display = 'block'; return; }
1746
+ const res = await fetch(PAPI + '/support/tickets', { method: 'POST', headers: headers(), body: JSON.stringify({ subject, priority, category }) });
1747
+ if (res.ok) {
1748
+ closeModal('createTicketModal');
1749
+ document.getElementById('ticketSubject').value = '';
1750
+ loadSupport();
1751
+ } else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
1752
+ }
1753
+
1754
+ async function showTicketDetail(ticketId) {
1755
+ currentTicketId = ticketId;
1756
+ try {
1757
+ const res = await fetch(PAPI + '/support/tickets/' + ticketId, { headers: headers() });
1758
+ const data = await res.json();
1759
+ const t = data.ticket;
1760
+ const msgs = data.messages || [];
1761
+ document.getElementById('ticketDetailTitle').textContent = t.subject;
1762
+ document.getElementById('ticketStatusSelect').value = t.status || 'open';
1763
+ document.getElementById('ticketMessages').innerHTML = msgs.map(m => '<div class="msg-item msg-' + (m.sender_type || 'user') + '">' +
1764
+ '<div class="msg-meta"><strong>' + (m.sender_type === 'bot' ? '🤖 Bot' : '👤 You') + '</strong><span>' + fmtDate(m.created_at) + '</span></div>' +
1765
+ '<div class="msg-text">' + esc(m.message) + '</div></div>').join('');
1766
+ openModal('ticketDetailModal');
1767
+ const msgList = document.getElementById('ticketMessages');
1768
+ msgList.scrollTop = msgList.scrollHeight;
1769
+ } catch (err) { console.error('showTicketDetail:', err); }
1770
+ }
1771
+
1772
+ async function updateTicketStatus() {
1773
+ if (!currentTicketId) return;
1774
+ const status = document.getElementById('ticketStatusSelect').value;
1775
+ await fetch(PAPI + '/support/tickets/' + currentTicketId + '/status', { method: 'PUT', headers: headers(), body: JSON.stringify({ status }) });
1776
+ loadSupport();
1777
+ }
1778
+
1779
+ async function sendTicketReply() {
1780
+ if (!currentTicketId) return;
1781
+ const msg = document.getElementById('ticketReply').value.trim();
1782
+ if (!msg) return;
1783
+ await fetch(PAPI + '/support/tickets/' + currentTicketId + '/messages', { method: 'POST', headers: headers(), body: JSON.stringify({ message: msg }) });
1784
+ document.getElementById('ticketReply').value = '';
1785
+ showTicketDetail(currentTicketId);
1786
+ }
1787
+
1788
+ // ─── Script Builder ──────────────────────────────────────────────────
1789
+ let availablePlugins = [];
1790
+
1791
+ async function loadScriptPlugins() {
1792
+ try {
1793
+ const res = await fetch(PAPI + '/script/plugins', { headers: headers() });
1794
+ const data = await res.json();
1795
+ availablePlugins = data.plugins || [];
1796
+ const container = document.getElementById('pluginsList');
1797
+ container.innerHTML = availablePlugins.map(p => '<div class="toggle-wrap">' +
1798
+ '<div class="toggle-label">' + esc(p.name) + '<small>' + esc(p.description) + '</small></div>' +
1799
+ '<label class="toggle"><input type="checkbox" data-plugin="' + esc(p.id) + '"><span class="toggle-slider"></span></label>' +
1800
+ '</div>').join('');
1801
+ } catch (err) { console.error('loadScriptPlugins:', err); }
1802
+ }
1803
+
1804
+ async function loadScriptConfig() {
1805
+ const sid = getSiteId();
1806
+ if (!sid) return;
1807
+ try {
1808
+ const res = await fetch(PAPI + '/script/' + sid + '/config', { headers: headers() });
1809
+ const data = await res.json();
1810
+ const c = data.config;
1811
+ if (c) {
1812
+ document.getElementById('scriptMinified').checked = !!c.minified;
1813
+ document.getElementById('scriptAmp').checked = !!c.amp_compatible;
1814
+ document.getElementById('scriptAutoPatch').checked = !!c.auto_patch;
1815
+ document.getElementById('scriptCustomCss').value = c.custom_css || '';
1816
+ document.getElementById('scriptCustomJs').value = c.custom_js || '';
1817
+ let plugins = [];
1818
+ try { plugins = JSON.parse(c.plugins_json || '[]'); } catch {}
1819
+ document.querySelectorAll('#pluginsList input[data-plugin]').forEach(cb => { cb.checked = plugins.includes(cb.dataset.plugin); });
1820
+ }
1821
+ } catch (err) { console.error('loadScriptConfig:', err); }
1822
+ }
1823
+
1824
+ async function saveScriptConfig() {
1825
+ const sid = getSiteId();
1826
+ if (!sid) { alert('Select a site first'); return; }
1827
+ const plugins = [...document.querySelectorAll('#pluginsList input[data-plugin]:checked')].map(c => c.dataset.plugin);
1828
+ const body = {
1829
+ plugins,
1830
+ minified: document.getElementById('scriptMinified').checked,
1831
+ ampCompatible: document.getElementById('scriptAmp').checked,
1832
+ autoPatch: document.getElementById('scriptAutoPatch').checked,
1833
+ customCss: document.getElementById('scriptCustomCss').value,
1834
+ customJs: document.getElementById('scriptCustomJs').value
1835
+ };
1836
+ await fetch(PAPI + '/script/' + sid + '/config', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
1837
+ alert('Script config saved');
1838
+ }
1839
+
1840
+ async function buildScript() {
1841
+ const sid = getSiteId();
1842
+ if (!sid) { alert('Select a site first'); return; }
1843
+ await saveScriptConfig();
1844
+ const res = await fetch(PAPI + '/script/' + sid + '/build', { method: 'POST', headers: headers() });
1845
+ const data = await res.json();
1846
+ if (data.error) { alert(data.error); return; }
1847
+ document.getElementById('scriptPreviewSection').style.display = 'block';
1848
+ document.getElementById('scriptHash').textContent = 'Hash: ' + (data.hash || '—');
1849
+ document.getElementById('scriptSize').textContent = data.size ? fmtBytes(data.size) : '';
1850
+ document.getElementById('scriptPreview').textContent = data.script || '';
1851
+ }
1852
+
1853
+ function copyBuiltScript() {
1854
+ const text = document.getElementById('scriptPreview').textContent;
1855
+ navigator.clipboard.writeText(text).then(() => {
1856
+ const btn = document.querySelector('#scriptPreviewSection .copy-btn');
1857
+ btn.textContent = 'Copied!';
1858
+ setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
1859
+ });
1860
+ }
1861
+
1862
+ // ─── Stealth Mode ────────────────────────────────────────────────────
1863
+ async function loadStealth() {
1864
+ const sid = getSiteId();
1865
+ if (!sid) return;
1866
+ try {
1867
+ const res = await fetch(PAPI + '/stealth/' + sid, { headers: headers() });
1868
+ const data = await res.json();
1869
+ const p = data.profile;
1870
+ if (p) {
1871
+ document.getElementById('stTypingMin').value = p.typing_speed_min || 30;
1872
+ document.getElementById('stTypingMinVal').textContent = (p.typing_speed_min || 30) + 'ms';
1873
+ document.getElementById('stTypingMax').value = p.typing_speed_max || 120;
1874
+ document.getElementById('stTypingMaxVal').textContent = (p.typing_speed_max || 120) + 'ms';
1875
+ document.getElementById('stMouseSpeed').value = p.mouse_speed || 'natural';
1876
+ document.getElementById('stScrollBehavior').value = p.scroll_behavior || 'eased';
1877
+ document.getElementById('stClickMin').value = p.click_delay_min || 50;
1878
+ document.getElementById('stClickMinVal').textContent = (p.click_delay_min || 50) + 'ms';
1879
+ document.getElementById('stClickMax').value = p.click_delay_max || 400;
1880
+ document.getElementById('stClickMaxVal').textContent = (p.click_delay_max || 400) + 'ms';
1881
+ let ad = {};
1882
+ try { ad = JSON.parse(p.anti_detection_json || '{}'); } catch {}
1883
+ document.getElementById('stHideWebdriver').checked = !!ad.hideWebdriver;
1884
+ document.getElementById('stSpoofPlugins').checked = !!ad.spoofPlugins;
1885
+ document.getElementById('stSpoofLang').checked = !!ad.spoofLanguages;
1886
+ }
1887
+ } catch (err) { console.error('loadStealth:', err); }
1888
+ }
1889
+
1890
+ async function saveStealth() {
1891
+ const sid = getSiteId();
1892
+ if (!sid) { alert('Select a site first'); return; }
1893
+ const body = {
1894
+ typingSpeedMin: parseInt(document.getElementById('stTypingMin').value),
1895
+ typingSpeedMax: parseInt(document.getElementById('stTypingMax').value),
1896
+ mouseSpeed: document.getElementById('stMouseSpeed').value,
1897
+ scrollBehavior: document.getElementById('stScrollBehavior').value,
1898
+ clickDelayMin: parseInt(document.getElementById('stClickMin').value),
1899
+ clickDelayMax: parseInt(document.getElementById('stClickMax').value),
1900
+ antiDetection: {
1901
+ hideWebdriver: document.getElementById('stHideWebdriver').checked,
1902
+ spoofPlugins: document.getElementById('stSpoofPlugins').checked,
1903
+ spoofLanguages: document.getElementById('stSpoofLang').checked
1904
+ }
1905
+ };
1906
+ await fetch(PAPI + '/stealth/' + sid, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
1907
+ alert('Stealth profile saved');
1908
+ }
1909
+
1910
+ async function generateStealthScript() {
1911
+ const sid = getSiteId();
1912
+ if (!sid) { alert('Select a site first'); return; }
1913
+ const res = await fetch(PAPI + '/stealth/' + sid + '/script', { headers: headers() });
1914
+ const data = await res.json();
1915
+ document.getElementById('stealthScriptSection').style.display = 'block';
1916
+ document.getElementById('stealthScriptPreview').textContent = data.script || '';
1917
+ }
1918
+
1919
+ function copyStealthScript() {
1920
+ const text = document.getElementById('stealthScriptPreview').textContent;
1921
+ navigator.clipboard.writeText(text).then(() => {
1922
+ const btn = document.querySelector('#stealthScriptSection .copy-btn');
1923
+ btn.textContent = 'Copied!';
1924
+ setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
1925
+ });
1926
+ }
1927
+
1928
+ // ─── CDN ─────────────────────────────────────────────────────────────
1929
+ async function loadCdn() {
1930
+ const sid = getSiteId();
1931
+ if (!sid) return;
1932
+ try {
1933
+ const res = await fetch(PAPI + '/cdn/' + sid, { headers: headers() });
1934
+ const data = await res.json();
1935
+ const c = data.config;
1936
+ if (c) {
1937
+ document.getElementById('cdnDomain').value = c.custom_domain || '';
1938
+ document.getElementById('cdnTtl').value = c.cache_ttl || 86400;
1939
+ document.getElementById('cdnSslBadge').textContent = c.ssl_status || 'Pending';
1940
+ document.getElementById('cdnSslBadge').className = 'badge ' + (c.ssl_status === 'active' ? 'badge-active' : 'badge-free');
1941
+ let edges = [];
1942
+ try { edges = JSON.parse(c.edge_locations || '[]'); } catch {}
1943
+ document.querySelectorAll('#cdnEdges input').forEach(cb => { cb.checked = edges.includes(cb.value); });
1944
+ document.getElementById('cdnStatReqs').textContent = c.requests_count || 0;
1945
+ document.getElementById('cdnStatBw').textContent = fmtBytes(c.bandwidth_used || 0);
1946
+ const cdnUrl = c.custom_domain ? 'https://' + c.custom_domain + '/bridge/' + sid + '/ai-agent-bridge.js' : 'https://cdn.webagentbridge.com/bridge/' + sid + '/ai-agent-bridge.js';
1947
+ document.getElementById('cdnUrlDisplay').textContent = cdnUrl;
1948
+
1949
+ try {
1950
+ const statsRes = await fetch(PAPI + '/cdn/' + sid + '/stats?days=30', { headers: headers() });
1951
+ if (statsRes.ok) {
1952
+ const statsData = await statsRes.json();
1953
+ const totals = (statsData.stats || {}).totals || {};
1954
+ document.getElementById('cdnStatReqs').textContent = totals.requests || c.requests_count || 0;
1955
+ document.getElementById('cdnStatBw').textContent = fmtBytes(totals.bandwidth || c.bandwidth_used || 0);
1956
+ document.getElementById('cdnStatHits').textContent = totals.cache_hits || 0;
1957
+ }
1958
+ } catch {}
1959
+ } else {
1960
+ document.getElementById('cdnUrlDisplay').textContent = 'https://cdn.webagentbridge.com/bridge/' + sid + '/ai-agent-bridge.js';
1961
+ }
1962
+ } catch (err) { console.error('loadCdn:', err); }
1963
+ }
1964
+
1965
+ async function saveCdn() {
1966
+ const sid = getSiteId();
1967
+ if (!sid) { alert('Select a site first'); return; }
1968
+ const edgeLocations = [...document.querySelectorAll('#cdnEdges input:checked')].map(c => c.value);
1969
+ const body = {
1970
+ customDomain: document.getElementById('cdnDomain').value.trim() || null,
1971
+ cacheTtl: parseInt(document.getElementById('cdnTtl').value) || 86400,
1972
+ edgeLocations
1973
+ };
1974
+ await fetch(PAPI + '/cdn/' + sid, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
1975
+ alert('CDN config saved');
1976
+ loadCdn();
1977
+ }
1978
+
1979
+ function copyCdnUrl() {
1980
+ const text = document.getElementById('cdnUrlDisplay').textContent;
1981
+ navigator.clipboard.writeText(text).then(() => {
1982
+ const btn = document.querySelector('#view-cdn .copy-btn');
1983
+ btn.textContent = 'Copied!';
1984
+ setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
1985
+ });
1986
+ }
1987
+
1988
+ // ─── Audit & Compliance ──────────────────────────────────────────────
1989
+ async function loadAudit() {
1990
+ const sid = getSiteId();
1991
+ if (!sid) return;
1992
+ auditPage = 0;
1993
+ try {
1994
+ const res = await fetch(PAPI + '/audit/' + sid + '/compliance', { headers: headers() });
1995
+ const data = await res.json();
1996
+ const s = data.settings;
1997
+ if (s) {
1998
+ document.getElementById('auditRetention').value = s.retention_days || 90;
1999
+ document.getElementById('auditHipaa').checked = !!s.hipaa_mode;
2000
+ document.getElementById('auditGdpr').checked = !!s.gdpr_mode;
2001
+ document.getElementById('auditSoc2').checked = !!s.soc2_mode;
2002
+ document.getElementById('auditAutoPurge').checked = s.auto_purge !== undefined ? !!s.auto_purge : true;
2003
+ }
2004
+ } catch (err) { console.error('loadAudit:', err); }
2005
+ loadAuditLogs();
2006
+ }
2007
+
2008
+ async function loadAuditLogs() {
2009
+ const sid = getSiteId();
2010
+ if (!sid) return;
2011
+ const action = document.getElementById('auditActionFilter').value;
2012
+ let url = PAPI + '/audit/' + sid + '/logs?limit=' + AUDIT_LIMIT + '&offset=' + (auditPage * AUDIT_LIMIT);
2013
+ if (action) url += '&action=' + encodeURIComponent(action);
2014
+ try {
2015
+ const res = await fetch(url, { headers: headers() });
2016
+ const data = await res.json();
2017
+ const rows = data.rows || [];
2018
+ const total = data.total || 0;
2019
+ const tbody = document.getElementById('auditLogsTable');
2020
+ if (rows.length === 0) {
2021
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No audit logs</td></tr>';
2022
+ } else {
2023
+ tbody.innerHTML = rows.map(r => '<tr>' +
2024
+ '<td style="font-size:0.82rem;">' + fmtDate(r.created_at) + '</td>' +
2025
+ '<td>' + esc(r.action) + '</td>' +
2026
+ '<td style="font-size:0.82rem;">' + esc(r.resource_type || '—') + (r.resource_id ? '/' + esc(r.resource_id).substring(0, 8) : '') + '</td>' +
2027
+ '<td style="font-size:0.82rem;">' + esc(r.user_id || '—').substring(0, 8) + '</td>' +
2028
+ '<td style="font-size:0.82rem;font-family:var(--font-mono);">' + esc(r.ip_address || '—') + '</td>' +
2029
+ '</tr>').join('');
2030
+ }
2031
+ const totalPages = Math.ceil(total / AUDIT_LIMIT);
2032
+ const pag = document.getElementById('auditPagination');
2033
+ pag.innerHTML = '<span style="font-size:0.85rem;color:var(--text-muted);">Page ' + (auditPage + 1) + ' of ' + Math.max(totalPages, 1) + ' (' + total + ' total)</span>' +
2034
+ '<button class="btn btn-sm btn-secondary" onclick="auditPrev()" ' + (auditPage <= 0 ? 'disabled' : '') + '>← Prev</button>' +
2035
+ '<button class="btn btn-sm btn-secondary" onclick="auditNext()" ' + (auditPage >= totalPages - 1 ? 'disabled' : '') + '>Next →</button>';
2036
+ } catch (err) { console.error('loadAuditLogs:', err); }
2037
+ }
2038
+
2039
+ function auditPrev() { if (auditPage > 0) { auditPage--; loadAuditLogs(); } }
2040
+ function auditNext() { auditPage++; loadAuditLogs(); }
2041
+
2042
+ async function saveCompliance() {
2043
+ const sid = getSiteId();
2044
+ if (!sid) { alert('Select a site first'); return; }
2045
+ const body = {
2046
+ retentionDays: parseInt(document.getElementById('auditRetention').value) || 90,
2047
+ hipaaMode: document.getElementById('auditHipaa').checked,
2048
+ gdprMode: document.getElementById('auditGdpr').checked,
2049
+ soc2Mode: document.getElementById('auditSoc2').checked,
2050
+ autoPurge: document.getElementById('auditAutoPurge').checked
2051
+ };
2052
+ await fetch(PAPI + '/audit/' + sid + '/compliance', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
2053
+ alert('Compliance settings saved');
2054
+ }
2055
+
2056
+ async function exportAuditLogs(format) {
2057
+ const sid = getSiteId();
2058
+ if (!sid) { alert('Select a site first'); return; }
2059
+ let url = PAPI + '/audit/' + sid + '/export?format=' + format;
2060
+ const since = document.getElementById('auditSince').value;
2061
+ const until = document.getElementById('auditUntil').value;
2062
+ if (since) url += '&since=' + since + 'T00:00:00.000Z';
2063
+ if (until) url += '&until=' + until + 'T23:59:59.999Z';
2064
+ try {
2065
+ const res = await fetch(url, { headers: headers() });
2066
+ const blob = await res.blob();
2067
+ const disposition = res.headers.get('Content-Disposition') || '';
2068
+ const match = disposition.match(/filename="?([^"]+)"?/);
2069
+ const filename = match ? match[1] : 'audit-logs.' + format;
2070
+ const a = document.createElement('a');
2071
+ a.href = URL.createObjectURL(blob);
2072
+ a.download = filename;
2073
+ a.click();
2074
+ URL.revokeObjectURL(a.href);
2075
+ } catch (err) { alert('Export failed'); }
2076
+ }
2077
+
2078
+ async function purgeAuditLogs() {
2079
+ const sid = getSiteId();
2080
+ if (!sid) { alert('Select a site first'); return; }
2081
+ if (!confirm('Purge old audit logs beyond retention period? This cannot be undone.')) return;
2082
+ const res = await fetch(PAPI + '/audit/' + sid + '/purge', { method: 'POST', headers: headers() });
2083
+ const data = await res.json();
2084
+ alert('Purged ' + (data.deleted || 0) + ' old log entries.');
2085
+ loadAuditLogs();
2086
+ }
2087
+
2088
+ // ─── Sandbox ─────────────────────────────────────────────────────────
2089
+ async function loadSandboxes() {
2090
+ const sid = getSiteId();
2091
+ if (!sid) return;
2092
+ try {
2093
+ const res = await fetch(PAPI + '/sandbox/' + sid, { headers: headers() });
2094
+ const data = await res.json();
2095
+ const sandboxes = data.sandboxes || [];
2096
+ const tbody = document.getElementById('sandboxTable');
2097
+ if (sandboxes.length === 0) {
2098
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No sandboxes. Create one to get started.</td></tr>';
2099
+ document.getElementById('sandboxDetail').style.display = 'none';
2100
+ } else {
2101
+ tbody.innerHTML = sandboxes.map(s => '<tr>' +
2102
+ '<td><strong style="color:var(--text-primary)">' + esc(s.name) + '</strong></td>' +
2103
+ '<td><span class="badge badge-active">Active</span></td>' +
2104
+ '<td>' + (s.traffic_generated || 0) + '</td>' +
2105
+ '<td>' + fmtDate(s.created_at) + '</td>' +
2106
+ '<td style="display:flex;gap:4px;">' +
2107
+ '<button class="btn btn-sm btn-secondary" onclick="selectSandbox(\'' + s.id + '\')">Select</button>' +
2108
+ '<button class="btn btn-sm btn-danger" onclick="deleteSandbox(\'' + s.id + '\')">Delete</button></td>' +
2109
+ '</tr>').join('');
2110
+ if (!currentSandboxId && sandboxes.length > 0) selectSandbox(sandboxes[0].id);
2111
+ }
2112
+ } catch (err) { console.error('loadSandboxes:', err); }
2113
+ }
2114
+
2115
+ async function createSandbox() {
2116
+ const sid = getSiteId();
2117
+ if (!sid) { alert('Select a site first'); return; }
2118
+ const name = prompt('Sandbox name:');
2119
+ if (!name) return;
2120
+ await fetch(PAPI + '/sandbox/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ name }) });
2121
+ loadSandboxes();
2122
+ }
2123
+
2124
+ async function deleteSandbox(sandboxId) {
2125
+ const sid = getSiteId();
2126
+ if (!sid || !confirm('Delete this sandbox?')) return;
2127
+ await fetch(PAPI + '/sandbox/' + sid + '/' + sandboxId, { method: 'DELETE', headers: headers() });
2128
+ if (currentSandboxId === sandboxId) currentSandboxId = null;
2129
+ loadSandboxes();
2130
+ }
2131
+
2132
+ function selectSandbox(sandboxId) {
2133
+ currentSandboxId = sandboxId;
2134
+ document.getElementById('sandboxDetail').style.display = 'block';
2135
+ loadBenchmarks();
2136
+ }
2137
+
2138
+ async function simulateTraffic() {
2139
+ const sid = getSiteId();
2140
+ if (!sid || !currentSandboxId) { alert('Select a site and sandbox first'); return; }
2141
+ const body = {
2142
+ agentCount: parseInt(document.getElementById('simAgentCount').value) || 10,
2143
+ duration: parseInt(document.getElementById('simDuration').value) || 60,
2144
+ actionsPerAgent: parseInt(document.getElementById('simActions').value) || 5
2145
+ };
2146
+ const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/simulate', { method: 'POST', headers: headers(), body: JSON.stringify(body) });
2147
+ const data = await res.json();
2148
+ const r = data.result || {};
2149
+ document.getElementById('simResults').style.display = 'block';
2150
+ document.getElementById('simResultsContent').innerHTML =
2151
+ '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;">' +
2152
+ '<div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Total Agents</div><div style="font-size:1.5rem;font-weight:700;">' + (r.agentCount || 0) + '</div></div>' +
2153
+ '<div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Total Actions</div><div style="font-size:1.5rem;font-weight:700;">' + (r.totalActions || 0) + '</div></div>' +
2154
+ '<div><div style="font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;">Avg Actions/Agent</div><div style="font-size:1.5rem;font-weight:700;">' + (r.avgActionsPerAgent || 0) + '</div></div>' +
2155
+ '</div>' +
2156
+ (r.typeDist ? '<div style="margin-top:16px;"><h4 style="margin-bottom:8px;">Agent Distribution</h4><div style="display:flex;gap:8px;flex-wrap:wrap;">' +
2157
+ Object.entries(r.typeDist).map(([k, v]) => '<span class="badge type-' + k + '">' + k + ': ' + v + '</span>').join('') + '</div></div>' : '');
2158
+ loadSandboxes();
2159
+ }
2160
+
2161
+ async function runBenchmark() {
2162
+ const sid = getSiteId();
2163
+ if (!sid || !currentSandboxId) { alert('Select a site and sandbox first'); return; }
2164
+ const benchmarkType = document.getElementById('benchType').value;
2165
+ const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/benchmark', { method: 'POST', headers: headers(), body: JSON.stringify({ benchmarkType }) });
2166
+ const data = await res.json();
2167
+ const r = data.result || {};
2168
+ alert('Benchmark complete: ' + r.benchmarkType + ' — Before: ' + r.beforeValue + ' ' + (r.unit || '') + ', After: ' + r.afterValue + ' ' + (r.unit || '') + ' (' + r.improvement + '% improvement)');
2169
+ loadBenchmarks();
2170
+ }
2171
+
2172
+ async function loadBenchmarks() {
2173
+ const sid = getSiteId();
2174
+ if (!sid || !currentSandboxId) return;
2175
+ try {
2176
+ const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/benchmarks', { headers: headers() });
2177
+ const data = await res.json();
2178
+ const benchmarks = data.benchmarks || [];
2179
+ const tbody = document.getElementById('benchmarksTable');
2180
+ if (benchmarks.length === 0) {
2181
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No benchmarks yet</td></tr>';
2182
+ } else {
2183
+ tbody.innerHTML = benchmarks.map(b => {
2184
+ const imp = b.before_value > 0 ? (((b.before_value - b.after_value) / b.before_value) * 100).toFixed(1) : '—';
2185
+ return '<tr>' +
2186
+ '<td>' + esc(b.benchmark_type) + '</td>' +
2187
+ '<td>' + (Math.round(b.before_value * 100) / 100) + '</td>' +
2188
+ '<td>' + (Math.round(b.after_value * 100) / 100) + '</td>' +
2189
+ '<td><span style="color:' + (parseFloat(imp) > 0 ? 'var(--accent-green)' : 'var(--accent-red)') + ';">' + imp + '%</span></td>' +
2190
+ '<td>' + fmtDate(b.recorded_at) + '</td></tr>';
2191
+ }).join('');
2192
+ }
2193
+ } catch (err) { console.error('loadBenchmarks:', err); }
2194
+ }
2195
+
2196
+ async function compareBenchmarks() {
2197
+ const sid = getSiteId();
2198
+ if (!sid || !currentSandboxId) { alert('Select a sandbox first'); return; }
2199
+ try {
2200
+ const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/benchmarks', { headers: headers() });
2201
+ const data = await res.json();
2202
+ const benchmarks = data.benchmarks || [];
2203
+ const types = ['rate_limit', 'response_time', 'throughput'];
2204
+ let html = '';
2205
+ for (const type of types) {
2206
+ const typeData = benchmarks.filter(b => b.benchmark_type === type);
2207
+ if (typeData.length >= 2) {
2208
+ const latest = typeData[0];
2209
+ const prev = typeData[1];
2210
+ const delta = latest.after_value - prev.after_value;
2211
+ const improved = type === 'throughput' ? delta > 0 : delta < 0;
2212
+ html += '<div style="margin-bottom:16px;padding:12px;border-radius:var(--radius-md);background:var(--bg-surface);">' +
2213
+ '<strong>' + type.replace(/_/g, ' ') + '</strong>' +
2214
+ '<div style="display:flex;gap:20px;margin-top:8px;font-size:0.9rem;color:var(--text-secondary);">' +
2215
+ '<span>Latest: ' + (Math.round(latest.after_value * 100) / 100) + '</span>' +
2216
+ '<span>Previous: ' + (Math.round(prev.after_value * 100) / 100) + '</span>' +
2217
+ '<span style="color:' + (improved ? 'var(--accent-green)' : 'var(--accent-red)') + ';">' + (improved ? '↑ Improved' : '↓ Regressed') + '</span></div></div>';
2218
+ } else if (typeData.length === 1) {
2219
+ html += '<div style="margin-bottom:16px;padding:12px;border-radius:var(--radius-md);background:var(--bg-surface);">' +
2220
+ '<strong>' + type.replace(/_/g, ' ') + '</strong>' +
2221
+ '<div style="font-size:0.9rem;color:var(--text-muted);margin-top:8px;">Only 1 benchmark — need at least 2 to compare</div></div>';
2222
+ }
2223
+ }
2224
+ if (!html) html = '<p style="color:var(--text-muted);">No benchmarks to compare</p>';
2225
+ document.getElementById('benchCompare').style.display = 'block';
2226
+ document.getElementById('benchCompareContent').innerHTML = html;
2227
+ } catch (err) { console.error('compareBenchmarks:', err); }
2228
+ }
2229
+
2230
+ // ─── Init ────────────────────────────────────────────────────────────
2231
+ async function init() {
2232
+ if (user) {
2233
+ document.getElementById('userName').textContent = user.name || user.email;
2234
+ }
2235
+ await loadSites();
2236
+ loadOverview();
2237
+ }
2238
+
2239
+ // ═══════════ ADVANCED AI FUNCTIONS ═══════════
2240
+
2241
+ function getSiteId() {
2242
+ const sel = document.querySelector('.view.active .site-selector');
2243
+ return sel ? sel.value : '';
2244
+ }
2245
+ function premStatCard(lbl, val, clr) {
2246
+ return `<div class="stat-card"><div class="label">${lbl}</div><div class="value" style="color:${clr||'inherit'}">${val}</div></div>`;
2247
+ }
2248
+
2249
+ // ── Memory ──
2250
+ async function loadMemoryView() {
2251
+ const sid = getSiteId(); if (!sid) return;
2252
+ try {
2253
+ const [stats, prefs, sessions] = await Promise.all([
2254
+ fetch(`${PAPI}/memory/${sid}/stats`, { headers: headers() }).then(r=>r.json()),
2255
+ fetch(`${PAPI}/memory/${sid}/preferences`, { headers: headers() }).then(r=>r.json()),
2256
+ fetch(`${PAPI}/memory/${sid}/sessions`, { headers: headers() }).then(r=>r.json()),
2257
+ ]);
2258
+ const s = stats.stats || stats;
2259
+ document.getElementById('memoryStats').innerHTML = [
2260
+ premStatCard('Total', s.total||0, 'var(--accent-blue)'),
2261
+ premStatCard('Preferences', s.byType?.preference||0, 'var(--accent-green)'),
2262
+ premStatCard('Interactions', s.byType?.interaction||0),
2263
+ premStatCard('Patterns', s.byType?.pattern||0, 'var(--accent-purple)'),
2264
+ premStatCard('Avg Importance', ((s.avgImportance||0)*100).toFixed(0)+'%'),
2265
+ premStatCard('Storage', ((s.storageBytesEstimate||0)/1024).toFixed(1)+' KB'),
2266
+ ].join('');
2267
+ const pl = prefs.preferences || prefs || [];
2268
+ document.getElementById('memPreferences').innerHTML = pl.length ? pl.map(p=>`<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border);"><span style="font-weight:600;">${esc(p.key)}</span><span style="color:var(--text-muted);font-size:0.85rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;">${esc(typeof p.value==='string'?p.value:JSON.stringify(p.value))}</span></div>`).join('') : '<p class="empty-state">No preferences stored</p>';
2269
+ const sl = sessions.sessions || sessions || [];
2270
+ document.getElementById('memSessions').innerHTML = sl.length ? sl.map(s=>`<div style="padding:6px 0;border-bottom:1px solid var(--border);"><span style="font-size:0.85rem;">${esc(s.agent_id||'agent')}</span> <span style="font-size:0.8rem;color:var(--text-muted);">${s.started_at?new Date(s.started_at).toLocaleString():''}</span></div>`).join('') : '<p class="empty-state">No sessions</p>';
2271
+ } catch(e) { console.error(e); }
2272
+ }
2273
+
2274
+ async function storeMemory() {
2275
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2276
+ const key = document.getElementById('memKey').value;
2277
+ const value = document.getElementById('memValue').value;
2278
+ if (!key || !value) return alert('Key and Value required');
2279
+ try {
2280
+ let val; try { val = JSON.parse(value); } catch { val = value; }
2281
+ await fetch(`${PAPI}/memory/${sid}/store`, { method:'POST', headers: headers(), body: JSON.stringify({ agentId:'dashboard', type:document.getElementById('memType').value, category:document.getElementById('memCat').value, key, value:val, importance:0.7 }) });
2282
+ alert('Memory stored!'); loadMemoryView();
2283
+ } catch(e) { alert('Error: '+e.message); }
2284
+ }
2285
+
2286
+ async function recallMemories() {
2287
+ const sid = getSiteId(); if (!sid) return;
2288
+ try {
2289
+ const r = await fetch(`${PAPI}/memory/${sid}/recall`, { method:'POST', headers: headers(), body: JSON.stringify({ agentId:'dashboard', query:document.getElementById('memQuery').value, type:document.getElementById('memFilterType').value||undefined, limit:20 }) });
2290
+ const data = await r.json();
2291
+ const memories = data.memories || data || [];
2292
+ document.getElementById('memResults').innerHTML = memories.length ? memories.map(m=>`<div class="card" style="margin-bottom:8px;padding:12px;"><div style="display:flex;justify-content:space-between;"><strong>${esc(m.key)}</strong><span class="badge">${m.memory_type||m.type||''}</span></div><div style="font-size:0.85rem;color:var(--text-muted);margin-top:4px;">${esc(typeof m.value==='string'?m.value:JSON.stringify(m.value))}</div><div style="font-size:0.75rem;color:var(--text-muted);margin-top:4px;">Importance: ${((m.importance||0)*100).toFixed(0)}% | Accessed: ${m.access_count||0}x</div></div>`).join('') : '<p class="empty-state">No memories found</p>';
2293
+ } catch(e) { console.error(e); }
2294
+ }
2295
+
2296
+ async function consolidateMemories() {
2297
+ const sid = getSiteId(); if (!sid) return;
2298
+ try {
2299
+ const r = await fetch(`${PAPI}/memory/${sid}/consolidate`, { method:'POST', headers: headers(), body: JSON.stringify({ agentId:'dashboard' }) });
2300
+ const d = await r.json(); const s = d.stats || d;
2301
+ alert(`Consolidated! Merged:${s.merged||0} Boosted:${s.boosted||0} Decayed:${s.decayed||0} Expired:${s.expired||0}`);
2302
+ loadMemoryView();
2303
+ } catch(e) { alert('Error: '+e.message); }
2304
+ }
2305
+
2306
+ // ── Self-Healing ──
2307
+ async function loadHealingView() {
2308
+ const sid = getSiteId(); if (!sid) return;
2309
+ try {
2310
+ const [health, history] = await Promise.all([
2311
+ fetch(`${PAPI}/healing/${sid}/health`, { headers: headers() }).then(r=>r.json()),
2312
+ fetch(`${PAPI}/healing/${sid}/history?limit=20`, { headers: headers() }).then(r=>r.json()),
2313
+ ]);
2314
+ const s = health.health || health;
2315
+ document.getElementById('healingStats').innerHTML = [
2316
+ premStatCard('Total Selectors', s.total||0, 'var(--accent-blue)'),
2317
+ premStatCard('Verified', s.verified||0, 'var(--accent-green)'),
2318
+ premStatCard('Healed', s.healed||0, 'var(--accent-yellow)'),
2319
+ premStatCard('Avg Confidence', ((s.avgConfidence||0)*100).toFixed(0)+'%'),
2320
+ premStatCard('Broken', (s.broken||[]).length, 'var(--accent-red)'),
2321
+ ].join('');
2322
+ const logs = history.history || history || [];
2323
+ document.getElementById('healingHistoryBody').innerHTML = logs.length ? logs.map(l=>`<tr><td>${esc(l.action_name||'')}</td><td style="font-family:monospace;font-size:0.8rem;">${esc(l.old_selector||'')}</td><td style="font-family:monospace;font-size:0.8rem;">${esc(l.new_selector||'')}</td><td><span class="badge">${l.strategy||''}</span></td><td>${((l.confidence||0)*100).toFixed(0)}%</td><td style="color:${l.success?'var(--accent-green)':'var(--accent-red)'};">${l.success?'✓':'✗'}</td><td style="font-size:0.8rem;">${l.created_at?new Date(l.created_at).toLocaleString():''}</td></tr>`).join('') : '<tr><td colspan="7" class="empty-state">No healing history</td></tr>';
2324
+ } catch(e) { console.error(e); }
2325
+ }
2326
+
2327
+ async function registerSelector() {
2328
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2329
+ try {
2330
+ await fetch(`${PAPI}/healing/${sid}/register`, { method:'POST', headers: headers(), body: JSON.stringify({ actionName:document.getElementById('healAction').value, selector:document.getElementById('healSelector').value, selectorType:document.getElementById('healType').value }) });
2331
+ alert('Selector registered!'); loadHealingView();
2332
+ } catch(e) { alert('Error: '+e.message); }
2333
+ }
2334
+
2335
+ async function healSelector() {
2336
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2337
+ try {
2338
+ const r = await fetch(`${PAPI}/healing/${sid}/heal`, { method:'POST', headers: headers(), body: JSON.stringify({ actionName:document.getElementById('healTargetAction').value, failedSelector:document.getElementById('healFailed').value, pageElements:[] }) });
2339
+ const d = await r.json(); const res = d.result || d;
2340
+ document.getElementById('healResult').innerHTML = res.healed
2341
+ ? `<div class="alert" style="background:var(--accent-green);color:white;">✅ Healed! New: <code>${esc(res.newSelector)}</code> via <strong>${res.strategy}</strong> (${((res.confidence||0)*100).toFixed(0)}%)</div>`
2342
+ : `<div class="alert alert-error">❌ Could not heal selector</div>`;
2343
+ loadHealingView();
2344
+ } catch(e) { alert('Error: '+e.message); }
2345
+ }
2346
+
2347
+ // ── Vision ──
2348
+ async function loadVisionView() {
2349
+ const sid = getSiteId(); if (!sid) return;
2350
+ try {
2351
+ const [cfg, history] = await Promise.all([
2352
+ fetch(`${PAPI}/vision/${sid}/config`, { headers: headers() }).then(r=>r.json()).catch(()=>({})),
2353
+ fetch(`${PAPI}/vision/${sid}/history?limit=20`, { headers: headers() }).then(r=>r.json()),
2354
+ ]);
2355
+ const c = cfg.config || cfg;
2356
+ if (c.provider) document.getElementById('visionProvider').value = c.provider;
2357
+ if (c.model) document.getElementById('visionModel').value = c.model;
2358
+ if (c.endpoint) document.getElementById('visionEndpoint').value = c.endpoint;
2359
+ const logs = history.history || history || [];
2360
+ document.getElementById('visionHistoryBody').innerHTML = logs.length ? logs.map(l=>`<tr><td style="font-size:0.85rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;">${esc(l.url||'')}</td><td><span class="badge">${l.provider||''}</span></td><td>${(JSON.parse(l.elements_found||'[]')).length}</td><td>${l.tokens_used||0}</td><td>${l.latency_ms||0}ms</td><td style="font-size:0.8rem;">${l.created_at?new Date(l.created_at).toLocaleString():''}</td></tr>`).join('') : '<tr><td colspan="6" class="empty-state">No analyses</td></tr>';
2361
+ } catch(e) { console.error(e); }
2362
+ }
2363
+
2364
+ async function configureVision() {
2365
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2366
+ try {
2367
+ await fetch(`${PAPI}/vision/${sid}/config`, { method:'PUT', headers: headers(), body: JSON.stringify({ provider:document.getElementById('visionProvider').value, model:document.getElementById('visionModel').value, endpoint:document.getElementById('visionEndpoint').value, apiKey:document.getElementById('visionApiKey').value||undefined }) });
2368
+ alert('Vision configured!');
2369
+ } catch(e) { alert('Error: '+e.message); }
2370
+ }
2371
+
2372
+ async function analyzeScreenshot() {
2373
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2374
+ const ss = document.getElementById('visionScreenshot').value;
2375
+ if (!ss) return alert('Paste a base64 screenshot');
2376
+ document.getElementById('visionResult').innerHTML = '<p style="color:var(--text-muted);">Analyzing...</p>';
2377
+ try {
2378
+ const r = await fetch(`${PAPI}/vision/${sid}/analyze`, { method:'POST', headers: headers(), body: JSON.stringify({ screenshotBase64:ss, url:document.getElementById('visionUrl').value, prompt:document.getElementById('visionPrompt').value }) });
2379
+ const d = await r.json(); const res = d.result || d;
2380
+ const els = res.elements || [];
2381
+ document.getElementById('visionResult').innerHTML = `<div class="alert" style="background:var(--accent-blue);color:white;margin-bottom:12px;">${els.length} elements found | ${res.latency_ms||0}ms | ${res.tokens_used||0} tokens${res.cached?' (cached)':''}</div>` + els.map(e=>`<div class="card" style="margin-bottom:8px;padding:10px;"><div style="display:flex;justify-content:space-between;"><strong>${esc(e.label||e.description||'')}</strong><span class="badge">${e.element_type||e.type||''}</span></div>${e.suggested_selector?`<code style="font-size:0.8rem;color:var(--accent-blue);">${esc(e.suggested_selector)}</code>`:''}<div style="font-size:0.75rem;color:var(--text-muted);">Confidence: ${((e.confidence||0)*100).toFixed(0)}%${e.interactable?' | Interactable':''}</div></div>`).join('');
2382
+ loadVisionView();
2383
+ } catch(e) { document.getElementById('visionResult').innerHTML = `<div class="alert alert-error">${e.message}</div>`; }
2384
+ }
2385
+
2386
+ // ── Swarm ──
2387
+ async function loadSwarmView() {
2388
+ const sid = getSiteId(); if (!sid) return;
2389
+ try {
2390
+ const [cfg, stats, history] = await Promise.all([
2391
+ fetch(`${PAPI}/swarm/${sid}/config`, { headers: headers() }).then(r=>r.json()).catch(()=>({})),
2392
+ fetch(`${PAPI}/swarm/${sid}/stats`, { headers: headers() }).then(r=>r.json()),
2393
+ fetch(`${PAPI}/swarm/${sid}/history?limit=20`, { headers: headers() }).then(r=>r.json()),
2394
+ ]);
2395
+ const c = cfg.config || cfg;
2396
+ if (c.strategy) document.getElementById('swarmStrategy').value = c.strategy;
2397
+ if (c.max_agents) document.getElementById('swarmMaxAgents').value = c.max_agents;
2398
+ if (c.timeout_ms) document.getElementById('swarmTimeout').value = c.timeout_ms;
2399
+ const s = stats.stats || stats;
2400
+ document.getElementById('swarmStats').innerHTML = [
2401
+ premStatCard('Total Tasks', s.total||0, 'var(--accent-blue)'),
2402
+ premStatCard('Success Rate', ((s.successRate||0)*100).toFixed(0)+'%', 'var(--accent-green)'),
2403
+ premStatCard('Avg Agents', (s.avgAgents||0).toFixed(1)),
2404
+ premStatCard('Avg Latency', (s.avgLatencyMs||0).toFixed(0)+' ms'),
2405
+ ].join('');
2406
+ const logs = history.history || history || [];
2407
+ document.getElementById('swarmHistoryBody').innerHTML = logs.length ? logs.map(t=>`<tr><td style="font-family:monospace;font-size:0.8rem;">${esc((t.id||'').slice(0,8))}</td><td><span class="badge">${t.task_type||''}</span></td><td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;">${esc(t.objective||'')}</td><td>${t.agentCount||0}</td><td style="color:${t.status==='completed'?'var(--accent-green)':'var(--accent-red)'};">${t.status}</td><td style="font-size:0.8rem;">${t.created_at?new Date(t.created_at).toLocaleString():''}</td></tr>`).join('') : '<tr><td colspan="6" class="empty-state">No tasks</td></tr>';
2408
+ } catch(e) { console.error(e); }
2409
+ }
2410
+
2411
+ async function configureSwarm() {
2412
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2413
+ try {
2414
+ await fetch(`${PAPI}/swarm/${sid}/config`, { method:'PUT', headers: headers(), body: JSON.stringify({ strategy:document.getElementById('swarmStrategy').value, maxAgents:+document.getElementById('swarmMaxAgents').value, timeoutMs:+document.getElementById('swarmTimeout').value }) });
2415
+ alert('Swarm configured!');
2416
+ } catch(e) { alert('Error: '+e.message); }
2417
+ }
2418
+
2419
+ async function createAndRunSwarm() {
2420
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2421
+ const targets = document.getElementById('swarmTargets').value.split('\n').map(t=>t.trim()).filter(Boolean);
2422
+ if (!targets.length) return alert('Add at least one target URL');
2423
+ document.getElementById('swarmResult').innerHTML = '<p style="color:var(--text-muted);">🐝 Launching swarm...</p>';
2424
+ try {
2425
+ const taskRes = await fetch(`${PAPI}/swarm/${sid}/tasks`, { method:'POST', headers: headers(), body: JSON.stringify({ taskType:document.getElementById('swarmTaskType').value, objective:document.getElementById('swarmObjective').value, parameters:{ targets }, createdBy:'dashboard' }) });
2426
+ const task = await taskRes.json();
2427
+ const taskId = task.task?.id || task.id;
2428
+ const agents = targets.map((t,i)=>({ role:i===0?'searcher_main':'searcher_indie', type:'http', target:t }));
2429
+ await fetch(`${PAPI}/swarm/tasks/${taskId}/agents`, { method:'POST', headers: headers(), body: JSON.stringify({ agents }) });
2430
+ const runRes = await fetch(`${PAPI}/swarm/tasks/${taskId}/run`, { method:'POST', headers: headers() });
2431
+ const result = await runRes.json();
2432
+ const r = result.result || result;
2433
+ document.getElementById('swarmResult').innerHTML = `<div class="alert" style="background:var(--accent-green);color:white;margin-bottom:12px;">✅ Swarm complete! ${r.total_sources||targets.length} sources | Best: ${esc(r.best_source||'N/A')}</div><pre style="font-size:0.8rem;background:var(--bg-secondary);padding:12px;border-radius:8px;overflow-x:auto;max-height:300px;">${esc(JSON.stringify(r.merged_result||r,null,2))}</pre>`;
2434
+ loadSwarmView();
2435
+ } catch(e) { document.getElementById('swarmResult').innerHTML = `<div class="alert alert-error">${e.message}</div>`; }
2436
+ }
2437
+
2438
+ // ── Marketplace ──
2439
+ async function loadMarketplaceView() {
2440
+ const sid = getSiteId();
2441
+ try {
2442
+ const [plugins, installed] = await Promise.all([
2443
+ fetch(`${PAPI.replace('/premium','')}/plugins/marketplace`).then(r=>r.json()),
2444
+ sid ? fetch(`${PAPI}/plugins/installed/${sid}`, { headers: headers() }).then(r=>r.json()) : Promise.resolve([]),
2445
+ ]);
2446
+ const pl = plugins.plugins || plugins || [];
2447
+ const inst = installed.plugins || installed || [];
2448
+ const instIds = new Set(inst.map(i=>i.plugin_id||i.id));
2449
+ document.getElementById('pluginCards').innerHTML = pl.map(p=>`
2450
+ <div class="card" style="padding:20px;">
2451
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
2452
+ <span style="font-size:2rem;">${p.icon||'🔌'}</span>
2453
+ <div><h3 style="margin:0;font-size:1rem;">${esc(p.name)}</h3><span class="badge" style="font-size:0.65rem;">${p.category}</span>${p.is_official?'<span class="badge" style="margin-left:4px;background:var(--accent-purple);font-size:0.65rem;">Official</span>':''}</div>
2454
+ </div>
2455
+ <p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:12px;min-height:40px;">${esc(p.description)}</p>
2456
+ <div style="display:flex;justify-content:space-between;align-items:center;">
2457
+ <span style="font-size:0.8rem;color:var(--text-muted);">${(p.rating||0).toFixed(1)} ⭐ · ${p.downloads||0} installs</span>
2458
+ ${sid ? (instIds.has(p.id) ? `<button class="btn btn-danger btn-sm" onclick="uninstallPlugin('${p.id}')">Uninstall</button>` : `<button class="btn btn-primary btn-sm" onclick="installPlugin('${p.id}')">Install</button>`) : ''}
2459
+ </div>
2460
+ </div>
2461
+ `).join('');
2462
+ document.getElementById('installedPluginsBody').innerHTML = inst.length ? inst.map(i=>`<tr><td><strong>${esc(i.name||i.plugin_name||'')}</strong></td><td><span class="badge">${i.category||''}</span></td><td>${i.version||''}</td><td style="color:${i.enabled?'var(--accent-green)':'var(--accent-red)'};">${i.enabled?'Active':'Disabled'}</td><td><button class="btn btn-danger btn-sm" onclick="uninstallPlugin('${i.plugin_id||i.id}')">Remove</button></td></tr>`).join('') : `<tr><td colspan="5" class="empty-state">${sid?'No plugins installed':'Select a site'}</td></tr>`;
2463
+ } catch(e) { console.error(e); }
2464
+ }
2465
+
2466
+ async function installPlugin(pluginId) {
2467
+ const sid = getSiteId(); if (!sid) return alert('Select a site');
2468
+ try {
2469
+ await fetch(`${PAPI}/plugins/${pluginId}/install/${sid}`, { method:'POST', headers: headers(), body: JSON.stringify({}) });
2470
+ alert('Plugin installed!'); loadMarketplaceView();
2471
+ } catch(e) { alert('Error: '+e.message); }
2472
+ }
2473
+
2474
+ async function uninstallPlugin(pluginId) {
2475
+ const sid = getSiteId(); if (!sid) return;
2476
+ if (!confirm('Uninstall this plugin?')) return;
2477
+ try {
2478
+ await fetch(`${PAPI}/plugins/${pluginId}/uninstall/${sid}`, { method:'DELETE', headers: headers() });
2479
+ loadMarketplaceView();
2480
+ } catch(e) { alert('Error: '+e.message); }
2481
+ }
2482
+
2483
+ init();
2484
+ </script>
2485
+ <script src="/js/cookie-consent.js"></script>
2486
+ </body>
2487
+ </html>