web-agent-bridge 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.ar.md +446 -446
- package/README.md +844 -844
- package/bin/cli.js +80 -80
- package/bin/wab.js +80 -80
- package/docs/DEPLOY.md +118 -118
- package/docs/SPEC.md +1540 -1540
- package/examples/bidi-agent.js +119 -119
- package/examples/mcp-agent.js +94 -94
- package/examples/puppeteer-agent.js +108 -108
- package/examples/vision-agent.js +171 -171
- package/package.json +78 -78
- package/public/admin/dashboard.html +848 -848
- package/public/admin/login.html +84 -84
- package/public/cookies.html +208 -208
- package/public/css/styles.css +1235 -1235
- package/public/dashboard.html +704 -704
- package/public/docs.html +585 -585
- package/public/index.html +332 -332
- package/public/js/auth-nav.js +31 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/ws-client.js +74 -74
- package/public/login.html +83 -83
- package/public/privacy.html +295 -295
- package/public/register.html +103 -103
- package/public/terms.html +254 -254
- package/script/ai-agent-bridge.js +1513 -1513
- package/sdk/README.md +55 -55
- package/sdk/index.js +203 -203
- package/sdk/package.json +14 -14
- package/server/config/secrets.js +92 -92
- package/server/index.js +181 -181
- package/server/middleware/adminAuth.js +30 -30
- package/server/middleware/auth.js +41 -41
- package/server/middleware/rateLimits.js +24 -24
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +561 -561
- package/server/routes/admin.js +247 -247
- package/server/routes/api.js +138 -138
- package/server/routes/auth.js +51 -51
- package/server/routes/billing.js +45 -45
- package/server/routes/discovery.js +329 -329
- package/server/routes/license.js +240 -240
- package/server/routes/noscript.js +543 -543
- package/server/routes/wab-api.js +476 -476
- package/server/services/email.js +204 -204
- package/server/services/fairness.js +420 -420
- package/server/services/stripe.js +192 -192
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +101 -101
- package/wab-mcp-adapter/README.md +136 -136
- package/wab-mcp-adapter/index.js +555 -555
- package/wab-mcp-adapter/package.json +17 -17
- package/public/css/premium.css +0 -317
- package/public/premium-dashboard.html +0 -2075
- package/public/premium.html +0 -791
- package/server/migrations/002_premium_features.sql +0 -418
- package/server/routes/premium.js +0 -724
- package/server/services/premium.js +0 -1680
|
@@ -1,2075 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>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 & 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 & Compliance</a>
|
|
107
|
-
<a href="#" data-view="sandbox">🧪 Sandbox</a>
|
|
108
|
-
<a href="/dashboard" style="margin-top:20px;">← Back to Dashboard</a>
|
|
109
|
-
</nav>
|
|
110
|
-
<div class="sidebar-footer">
|
|
111
|
-
<div style="font-size:0.85rem; color:var(--text-muted); margin-bottom:8px;" id="userName"></div>
|
|
112
|
-
<button class="btn btn-ghost btn-sm" onclick="logout()" style="width:100%; justify-content:flex-start;">🚪 Sign Out</button>
|
|
113
|
-
</div>
|
|
114
|
-
</aside>
|
|
115
|
-
|
|
116
|
-
<main class="main-content">
|
|
117
|
-
|
|
118
|
-
<!-- ══════ OVERVIEW ══════ -->
|
|
119
|
-
<div id="view-overview" class="view active">
|
|
120
|
-
<div class="page-header"><h1>Premium Overview</h1></div>
|
|
121
|
-
<div class="prem-grid-6" id="overviewStats">
|
|
122
|
-
<div class="stat-card"><div class="label">Active Alerts</div><div class="value" id="ovAlerts">0</div></div>
|
|
123
|
-
<div class="stat-card"><div class="label">Security Events (24h)</div><div class="value" id="ovSecurity">0</div></div>
|
|
124
|
-
<div class="stat-card"><div class="label">Installed Packs</div><div class="value" id="ovPacks">0</div></div>
|
|
125
|
-
<div class="stat-card"><div class="label">Active Agents</div><div class="value" id="ovAgents">0</div></div>
|
|
126
|
-
<div class="stat-card"><div class="label">Team Members</div><div class="value" id="ovTeam">0</div></div>
|
|
127
|
-
<div class="stat-card"><div class="label">Open Tickets</div><div class="value" id="ovTickets">0</div></div>
|
|
128
|
-
</div>
|
|
129
|
-
<div class="actions-bar">
|
|
130
|
-
<button class="btn btn-primary btn-sm" onclick="quickCheckAnomalies()">🔍 Check Anomalies</button>
|
|
131
|
-
<button class="btn btn-secondary btn-sm" onclick="quickSecurityReport()">🛡️ View Security Report</button>
|
|
132
|
-
<button class="btn btn-secondary btn-sm" onclick="switchView('agents')">🤖 Create Agent</button>
|
|
133
|
-
<button class="btn btn-secondary btn-sm" onclick="switchView('support')">🎫 Create Ticket</button>
|
|
134
|
-
</div>
|
|
135
|
-
<h3 class="section-title">Recent Alerts</h3>
|
|
136
|
-
<div class="table-wrapper">
|
|
137
|
-
<table>
|
|
138
|
-
<thead><tr><th>Type</th><th>Severity</th><th>Message</th><th>Time</th><th>Actions</th></tr></thead>
|
|
139
|
-
<tbody id="ovAlertsTable"><tr><td colspan="5" class="empty-state">No recent alerts</td></tr></tbody>
|
|
140
|
-
</table>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
<!-- ══════ TRAFFIC INTELLIGENCE ══════ -->
|
|
145
|
-
<div id="view-traffic" class="view">
|
|
146
|
-
<div class="page-header"><h1>Traffic Intelligence</h1></div>
|
|
147
|
-
<div class="site-select-bar">
|
|
148
|
-
<label>Site:</label>
|
|
149
|
-
<select class="form-input site-selector" onchange="loadTraffic()"></select>
|
|
150
|
-
<button class="btn btn-primary btn-sm" onclick="checkAnomalies()">Check Anomalies</button>
|
|
151
|
-
</div>
|
|
152
|
-
<div class="stats-grid" id="trafficStats">
|
|
153
|
-
<div class="stat-card"><div class="label">Total Agents</div><div class="value" id="tfTotal">0</div></div>
|
|
154
|
-
<div class="stat-card"><div class="label">Friendly</div><div class="value" id="tfFriendly" style="color:var(--accent-green)">0</div></div>
|
|
155
|
-
<div class="stat-card"><div class="label">Suspicious</div><div class="value" id="tfSuspicious" style="color:var(--accent-orange)">0</div></div>
|
|
156
|
-
<div class="stat-card"><div class="label">Aggressive</div><div class="value" id="tfAggressive" style="color:var(--accent-red)">0</div></div>
|
|
157
|
-
</div>
|
|
158
|
-
<h3 class="section-title">Agent Profiles</h3>
|
|
159
|
-
<div class="table-wrapper">
|
|
160
|
-
<table>
|
|
161
|
-
<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>
|
|
162
|
-
<tbody id="trafficTable"><tr><td colspan="7" class="empty-state">Select a site</td></tr></tbody>
|
|
163
|
-
</table>
|
|
164
|
-
</div>
|
|
165
|
-
<h3 class="section-title">Alerts</h3>
|
|
166
|
-
<div class="table-wrapper">
|
|
167
|
-
<table>
|
|
168
|
-
<thead><tr><th>Type</th><th>Severity</th><th>Message</th><th>Time</th><th>Actions</th></tr></thead>
|
|
169
|
-
<tbody id="trafficAlerts"><tr><td colspan="5" class="empty-state">No alerts</td></tr></tbody>
|
|
170
|
-
</table>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
<!-- ══════ EXPLOIT SHIELD ══════ -->
|
|
175
|
-
<div id="view-security" class="view">
|
|
176
|
-
<div class="page-header"><h1>Exploit Shield</h1></div>
|
|
177
|
-
<div class="site-select-bar">
|
|
178
|
-
<label>Site:</label>
|
|
179
|
-
<select class="form-input site-selector" onchange="loadSecurity()"></select>
|
|
180
|
-
</div>
|
|
181
|
-
<div class="stats-grid" id="securityStats">
|
|
182
|
-
<div class="stat-card"><div class="label">Total Events</div><div class="value" id="secTotal">0</div></div>
|
|
183
|
-
<div class="stat-card"><div class="label">Critical</div><div class="value" id="secCritical" style="color:var(--accent-red)">0</div></div>
|
|
184
|
-
<div class="stat-card"><div class="label">High</div><div class="value" id="secHigh" style="color:var(--accent-orange)">0</div></div>
|
|
185
|
-
<div class="stat-card"><div class="label">Blocked Agents</div><div class="value" id="secBlocked">0</div></div>
|
|
186
|
-
</div>
|
|
187
|
-
<h3 class="section-title">Security Events</h3>
|
|
188
|
-
<div class="table-wrapper">
|
|
189
|
-
<table>
|
|
190
|
-
<thead><tr><th>Type</th><th>Severity</th><th>Agent</th><th>Details</th><th>Time</th></tr></thead>
|
|
191
|
-
<tbody id="secEventsTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
|
|
192
|
-
</table>
|
|
193
|
-
</div>
|
|
194
|
-
<h3 class="section-title">Blocked Agents</h3>
|
|
195
|
-
<div class="table-wrapper">
|
|
196
|
-
<table>
|
|
197
|
-
<thead><tr><th>Agent Signature</th><th>Reason</th><th>Blocked At</th><th>Expires</th><th>Actions</th></tr></thead>
|
|
198
|
-
<tbody id="secBlockedTable"><tr><td colspan="5" class="empty-state">No blocked agents</td></tr></tbody>
|
|
199
|
-
</table>
|
|
200
|
-
</div>
|
|
201
|
-
<h3 class="section-title">Block an Agent</h3>
|
|
202
|
-
<div class="card" style="max-width:500px;">
|
|
203
|
-
<div class="form-group">
|
|
204
|
-
<label>Agent Signature</label>
|
|
205
|
-
<input type="text" class="form-input" id="blockSig" placeholder="e.g. Scrapy">
|
|
206
|
-
</div>
|
|
207
|
-
<div class="form-group">
|
|
208
|
-
<label>Reason</label>
|
|
209
|
-
<input type="text" class="form-input" id="blockReason" placeholder="Reason for blocking">
|
|
210
|
-
</div>
|
|
211
|
-
<button class="btn btn-danger btn-sm" onclick="blockAgent()">Block Agent</button>
|
|
212
|
-
</div>
|
|
213
|
-
<h3 class="section-title">Security Report</h3>
|
|
214
|
-
<div class="site-select-bar">
|
|
215
|
-
<label>Period:</label>
|
|
216
|
-
<select class="form-input" id="reportDays" style="width:auto;min-width:120px;">
|
|
217
|
-
<option value="7">7 days</option>
|
|
218
|
-
<option value="30" selected>30 days</option>
|
|
219
|
-
<option value="90">90 days</option>
|
|
220
|
-
</select>
|
|
221
|
-
<button class="btn btn-secondary btn-sm" onclick="loadSecurityReport()">Generate Report</button>
|
|
222
|
-
</div>
|
|
223
|
-
<div id="secReportContent"></div>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<!-- ══════ ACTIONS LIBRARY ══════ -->
|
|
227
|
-
<div id="view-actions" class="view">
|
|
228
|
-
<div class="page-header"><h1>Actions Library</h1></div>
|
|
229
|
-
<div class="site-select-bar">
|
|
230
|
-
<label>Filter by platform:</label>
|
|
231
|
-
<select class="form-input" id="packPlatformFilter" onchange="loadPacks()" style="width:auto;min-width:160px;">
|
|
232
|
-
<option value="">All Platforms</option>
|
|
233
|
-
</select>
|
|
234
|
-
<label style="margin-left:16px;">Site for installs:</label>
|
|
235
|
-
<select class="form-input site-selector" onchange="loadInstalledPacks()" style="min-width:200px;"></select>
|
|
236
|
-
</div>
|
|
237
|
-
<h3 class="section-title">Available Packs</h3>
|
|
238
|
-
<div class="prem-grid-3" id="packsGrid"><div class="empty-state" style="grid-column:1/-1;">Loading packs...</div></div>
|
|
239
|
-
<h3 class="section-title">Installed Packs</h3>
|
|
240
|
-
<div class="table-wrapper">
|
|
241
|
-
<table>
|
|
242
|
-
<thead><tr><th>Pack</th><th>Platform</th><th>Version</th><th>Installed</th><th>Actions</th></tr></thead>
|
|
243
|
-
<tbody id="installedPacksTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
|
|
244
|
-
</table>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
|
|
248
|
-
<!-- ══════ CUSTOM AGENTS ══════ -->
|
|
249
|
-
<div id="view-agents" class="view">
|
|
250
|
-
<div class="page-header">
|
|
251
|
-
<h1>Custom Agents</h1>
|
|
252
|
-
<button class="btn btn-primary btn-sm" onclick="openModal('createAgentModal')">+ Create Agent</button>
|
|
253
|
-
</div>
|
|
254
|
-
<div class="site-select-bar">
|
|
255
|
-
<label>Site:</label>
|
|
256
|
-
<select class="form-input site-selector" onchange="loadAgents()"></select>
|
|
257
|
-
</div>
|
|
258
|
-
<div class="table-wrapper">
|
|
259
|
-
<table>
|
|
260
|
-
<thead><tr><th>Name</th><th>Status</th><th>Schedule</th><th>Runs</th><th>Last Run</th><th>Actions</th></tr></thead>
|
|
261
|
-
<tbody id="agentsTable"><tr><td colspan="6" class="empty-state">Select a site</td></tr></tbody>
|
|
262
|
-
</table>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
|
|
266
|
-
<!-- ══════ WEBHOOKS & CRM ══════ -->
|
|
267
|
-
<div id="view-webhooks" class="view">
|
|
268
|
-
<div class="page-header">
|
|
269
|
-
<h1>Webhooks & CRM</h1>
|
|
270
|
-
</div>
|
|
271
|
-
<div class="site-select-bar">
|
|
272
|
-
<label>Site:</label>
|
|
273
|
-
<select class="form-input site-selector" onchange="loadWebhooksView()"></select>
|
|
274
|
-
</div>
|
|
275
|
-
<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>
|
|
276
|
-
<div class="table-wrapper">
|
|
277
|
-
<table>
|
|
278
|
-
<thead><tr><th>Name</th><th>URL</th><th>Events</th><th>Status</th><th>Failures</th><th>Actions</th></tr></thead>
|
|
279
|
-
<tbody id="webhooksTable"><tr><td colspan="6" class="empty-state">Select a site</td></tr></tbody>
|
|
280
|
-
</table>
|
|
281
|
-
</div>
|
|
282
|
-
<div id="webhookLogsSection" style="display:none;">
|
|
283
|
-
<h3 class="section-title">Webhook Logs</h3>
|
|
284
|
-
<div class="table-wrapper">
|
|
285
|
-
<table>
|
|
286
|
-
<thead><tr><th>Event</th><th>Status</th><th>Response</th><th>Time</th></tr></thead>
|
|
287
|
-
<tbody id="webhookLogsTable"></tbody>
|
|
288
|
-
</table>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
<div class="actions-bar" style="margin-top:16px;">
|
|
292
|
-
<button class="btn btn-secondary btn-sm" onclick="testWebhook()">🧪 Test Webhooks</button>
|
|
293
|
-
</div>
|
|
294
|
-
<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>
|
|
295
|
-
<div class="table-wrapper">
|
|
296
|
-
<table>
|
|
297
|
-
<thead><tr><th>Provider</th><th>Status</th><th>Last Sync</th><th>Actions</th></tr></thead>
|
|
298
|
-
<tbody id="crmTable"><tr><td colspan="4" class="empty-state">No integrations</td></tr></tbody>
|
|
299
|
-
</table>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
|
|
303
|
-
<!-- ══════ TEAM MANAGEMENT ══════ -->
|
|
304
|
-
<div id="view-team" class="view">
|
|
305
|
-
<div class="page-header">
|
|
306
|
-
<h1>Team Management</h1>
|
|
307
|
-
<button class="btn btn-primary btn-sm" onclick="openModal('inviteTeamModal')">+ Invite Member</button>
|
|
308
|
-
</div>
|
|
309
|
-
<div class="table-wrapper">
|
|
310
|
-
<table>
|
|
311
|
-
<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>
|
|
312
|
-
<tbody id="teamTable"><tr><td colspan="7" class="empty-state">No team members</td></tr></tbody>
|
|
313
|
-
</table>
|
|
314
|
-
</div>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<!-- ══════ SUPPORT ══════ -->
|
|
318
|
-
<div id="view-support" class="view">
|
|
319
|
-
<div class="page-header">
|
|
320
|
-
<h1>Support</h1>
|
|
321
|
-
<button class="btn btn-primary btn-sm" onclick="openModal('createTicketModal')">+ Create Ticket</button>
|
|
322
|
-
</div>
|
|
323
|
-
<div class="stats-grid" id="supportStats">
|
|
324
|
-
<div class="stat-card"><div class="label">Open</div><div class="value" id="supOpen" style="color:var(--accent-blue)">0</div></div>
|
|
325
|
-
<div class="stat-card"><div class="label">In Progress</div><div class="value" id="supProgress" style="color:var(--accent-orange)">0</div></div>
|
|
326
|
-
<div class="stat-card"><div class="label">Waiting</div><div class="value" id="supWaiting" style="color:var(--accent-purple)">0</div></div>
|
|
327
|
-
<div class="stat-card"><div class="label">Resolved</div><div class="value" id="supResolved" style="color:var(--accent-green)">0</div></div>
|
|
328
|
-
</div>
|
|
329
|
-
<div class="table-wrapper">
|
|
330
|
-
<table>
|
|
331
|
-
<thead><tr><th>Subject</th><th>Priority</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead>
|
|
332
|
-
<tbody id="ticketsTable"><tr><td colspan="5" class="empty-state">No tickets</td></tr></tbody>
|
|
333
|
-
</table>
|
|
334
|
-
</div>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<!-- ══════ SCRIPT BUILDER ══════ -->
|
|
338
|
-
<div id="view-script" class="view">
|
|
339
|
-
<div class="page-header"><h1>Script Builder</h1></div>
|
|
340
|
-
<div class="site-select-bar">
|
|
341
|
-
<label>Site:</label>
|
|
342
|
-
<select class="form-input site-selector" onchange="loadScriptConfig()"></select>
|
|
343
|
-
</div>
|
|
344
|
-
<div class="prem-grid-2">
|
|
345
|
-
<div>
|
|
346
|
-
<h3 class="section-title">Plugins</h3>
|
|
347
|
-
<div class="card" id="pluginsList" style="max-height:300px;overflow-y:auto;">Loading plugins...</div>
|
|
348
|
-
<h3 class="section-title">Options</h3>
|
|
349
|
-
<div class="card">
|
|
350
|
-
<div class="toggle-wrap">
|
|
351
|
-
<div class="toggle-label">Minified<small>Remove whitespace and comments</small></div>
|
|
352
|
-
<label class="toggle"><input type="checkbox" id="scriptMinified" checked><span class="toggle-slider"></span></label>
|
|
353
|
-
</div>
|
|
354
|
-
<div class="toggle-wrap">
|
|
355
|
-
<div class="toggle-label">AMP Compatible<small>Remove document.write calls</small></div>
|
|
356
|
-
<label class="toggle"><input type="checkbox" id="scriptAmp"><span class="toggle-slider"></span></label>
|
|
357
|
-
</div>
|
|
358
|
-
<div class="toggle-wrap">
|
|
359
|
-
<div class="toggle-label">Auto Patch<small>Auto-apply updates</small></div>
|
|
360
|
-
<label class="toggle"><input type="checkbox" id="scriptAutoPatch" checked><span class="toggle-slider"></span></label>
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
<div>
|
|
365
|
-
<h3 class="section-title">Custom CSS</h3>
|
|
366
|
-
<textarea class="form-input" id="scriptCustomCss" rows="5" placeholder="/* Custom CSS for bridge overlay */"></textarea>
|
|
367
|
-
<h3 class="section-title">Custom JS</h3>
|
|
368
|
-
<textarea class="form-input" id="scriptCustomJs" rows="5" placeholder="// Custom JavaScript"></textarea>
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
<div class="actions-bar">
|
|
372
|
-
<button class="btn btn-primary btn-sm" onclick="buildScript()">🔨 Build Script</button>
|
|
373
|
-
<button class="btn btn-secondary btn-sm" onclick="saveScriptConfig()">💾 Save Config</button>
|
|
374
|
-
</div>
|
|
375
|
-
<div id="scriptPreviewSection" style="display:none;">
|
|
376
|
-
<h3 class="section-title">Built Script</h3>
|
|
377
|
-
<div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;">
|
|
378
|
-
<span class="badge badge-active" id="scriptHash"></span>
|
|
379
|
-
<span style="font-size:0.85rem;color:var(--text-muted);" id="scriptSize"></span>
|
|
380
|
-
</div>
|
|
381
|
-
<div class="snippet-box" style="max-height:300px;overflow:auto;">
|
|
382
|
-
<button class="copy-btn" onclick="copyBuiltScript()">Copy</button>
|
|
383
|
-
<pre id="scriptPreview"></pre>
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
|
|
388
|
-
<!-- ══════ STEALTH MODE ══════ -->
|
|
389
|
-
<div id="view-stealth" class="view">
|
|
390
|
-
<div class="page-header"><h1>Stealth Mode</h1></div>
|
|
391
|
-
<div class="site-select-bar">
|
|
392
|
-
<label>Site:</label>
|
|
393
|
-
<select class="form-input site-selector" onchange="loadStealth()"></select>
|
|
394
|
-
</div>
|
|
395
|
-
<div class="prem-grid-2">
|
|
396
|
-
<div class="card">
|
|
397
|
-
<h3 style="margin-bottom:20px;">Profile Settings</h3>
|
|
398
|
-
<div class="form-group">
|
|
399
|
-
<label>Typing Speed Min (ms)</label>
|
|
400
|
-
<div class="slider-group">
|
|
401
|
-
<input type="range" min="0" max="500" value="30" id="stTypingMin" oninput="document.getElementById('stTypingMinVal').textContent=this.value+'ms'">
|
|
402
|
-
<span class="slider-val" id="stTypingMinVal">30ms</span>
|
|
403
|
-
</div>
|
|
404
|
-
</div>
|
|
405
|
-
<div class="form-group">
|
|
406
|
-
<label>Typing Speed Max (ms)</label>
|
|
407
|
-
<div class="slider-group">
|
|
408
|
-
<input type="range" min="0" max="500" value="120" id="stTypingMax" oninput="document.getElementById('stTypingMaxVal').textContent=this.value+'ms'">
|
|
409
|
-
<span class="slider-val" id="stTypingMaxVal">120ms</span>
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
<div class="form-group">
|
|
413
|
-
<label>Mouse Speed</label>
|
|
414
|
-
<select class="form-input" id="stMouseSpeed">
|
|
415
|
-
<option value="slow">Slow</option>
|
|
416
|
-
<option value="natural" selected>Natural</option>
|
|
417
|
-
<option value="fast">Fast</option>
|
|
418
|
-
</select>
|
|
419
|
-
</div>
|
|
420
|
-
<div class="form-group">
|
|
421
|
-
<label>Scroll Behavior</label>
|
|
422
|
-
<select class="form-input" id="stScrollBehavior">
|
|
423
|
-
<option value="linear">Linear</option>
|
|
424
|
-
<option value="eased" selected>Eased</option>
|
|
425
|
-
<option value="natural">Natural</option>
|
|
426
|
-
</select>
|
|
427
|
-
</div>
|
|
428
|
-
<div class="form-group">
|
|
429
|
-
<label>Click Delay Min (ms)</label>
|
|
430
|
-
<div class="slider-group">
|
|
431
|
-
<input type="range" min="0" max="500" value="50" id="stClickMin" oninput="document.getElementById('stClickMinVal').textContent=this.value+'ms'">
|
|
432
|
-
<span class="slider-val" id="stClickMinVal">50ms</span>
|
|
433
|
-
</div>
|
|
434
|
-
</div>
|
|
435
|
-
<div class="form-group">
|
|
436
|
-
<label>Click Delay Max (ms)</label>
|
|
437
|
-
<div class="slider-group">
|
|
438
|
-
<input type="range" min="0" max="500" value="400" id="stClickMax" oninput="document.getElementById('stClickMaxVal').textContent=this.value+'ms'">
|
|
439
|
-
<span class="slider-val" id="stClickMaxVal">400ms</span>
|
|
440
|
-
</div>
|
|
441
|
-
</div>
|
|
442
|
-
</div>
|
|
443
|
-
<div>
|
|
444
|
-
<div class="card" style="margin-bottom:20px;">
|
|
445
|
-
<h3 style="margin-bottom:20px;">Anti-Detection Options</h3>
|
|
446
|
-
<div class="checkbox-group" style="flex-direction:column;">
|
|
447
|
-
<label><input type="checkbox" id="stHideWebdriver"> Hide WebDriver flag</label>
|
|
448
|
-
<label><input type="checkbox" id="stSpoofPlugins"> Plugin spoofing</label>
|
|
449
|
-
<label><input type="checkbox" id="stSpoofLang"> Language spoofing</label>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
<div class="actions-bar">
|
|
453
|
-
<button class="btn btn-primary btn-sm" onclick="saveStealth()">💾 Save Profile</button>
|
|
454
|
-
<button class="btn btn-secondary btn-sm" onclick="generateStealthScript()">⚡ Generate Script</button>
|
|
455
|
-
</div>
|
|
456
|
-
<div id="stealthScriptSection" style="display:none;">
|
|
457
|
-
<h3 class="section-title">Generated Stealth Script</h3>
|
|
458
|
-
<div class="snippet-box" style="max-height:300px;overflow:auto;">
|
|
459
|
-
<button class="copy-btn" onclick="copyStealthScript()">Copy</button>
|
|
460
|
-
<pre id="stealthScriptPreview"></pre>
|
|
461
|
-
</div>
|
|
462
|
-
</div>
|
|
463
|
-
</div>
|
|
464
|
-
</div>
|
|
465
|
-
</div>
|
|
466
|
-
|
|
467
|
-
<!-- ══════ CDN ══════ -->
|
|
468
|
-
<div id="view-cdn" class="view">
|
|
469
|
-
<div class="page-header"><h1>CDN</h1></div>
|
|
470
|
-
<div class="site-select-bar">
|
|
471
|
-
<label>Site:</label>
|
|
472
|
-
<select class="form-input site-selector" onchange="loadCdn()"></select>
|
|
473
|
-
</div>
|
|
474
|
-
<div class="prem-grid-2">
|
|
475
|
-
<div class="card">
|
|
476
|
-
<h3 style="margin-bottom:20px;">CDN Configuration</h3>
|
|
477
|
-
<div class="form-group">
|
|
478
|
-
<label>Custom Domain</label>
|
|
479
|
-
<input type="text" class="form-input" id="cdnDomain" placeholder="cdn.yourdomain.com">
|
|
480
|
-
</div>
|
|
481
|
-
<div class="form-group">
|
|
482
|
-
<label>Cache TTL (seconds)</label>
|
|
483
|
-
<input type="number" class="form-input" id="cdnTtl" value="86400" min="60">
|
|
484
|
-
</div>
|
|
485
|
-
<div class="form-group">
|
|
486
|
-
<label>Edge Locations</label>
|
|
487
|
-
<div class="checkbox-group" id="cdnEdges">
|
|
488
|
-
<label><input type="checkbox" value="us-east" checked> US East</label>
|
|
489
|
-
<label><input type="checkbox" value="us-west"> US West</label>
|
|
490
|
-
<label><input type="checkbox" value="eu-west" checked> EU West</label>
|
|
491
|
-
<label><input type="checkbox" value="eu-central"> EU Central</label>
|
|
492
|
-
<label><input type="checkbox" value="ap-southeast"> AP Southeast</label>
|
|
493
|
-
<label><input type="checkbox" value="ap-northeast"> AP Northeast</label>
|
|
494
|
-
</div>
|
|
495
|
-
</div>
|
|
496
|
-
<div class="form-group">
|
|
497
|
-
<label>SSL Status</label>
|
|
498
|
-
<span class="badge badge-active" id="cdnSslBadge">Pending</span>
|
|
499
|
-
</div>
|
|
500
|
-
<button class="btn btn-primary btn-sm" onclick="saveCdn()">Save CDN Config</button>
|
|
501
|
-
</div>
|
|
502
|
-
<div>
|
|
503
|
-
<div class="card" style="margin-bottom:20px;">
|
|
504
|
-
<h3 style="margin-bottom:20px;">CDN Stats</h3>
|
|
505
|
-
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;">
|
|
506
|
-
<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>
|
|
507
|
-
<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>
|
|
508
|
-
<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>
|
|
509
|
-
</div>
|
|
510
|
-
</div>
|
|
511
|
-
<div class="card">
|
|
512
|
-
<h3 style="margin-bottom:12px;">CDN URL</h3>
|
|
513
|
-
<div class="snippet-box">
|
|
514
|
-
<button class="copy-btn" onclick="copyCdnUrl()">Copy</button>
|
|
515
|
-
<pre id="cdnUrlDisplay">Configure a site to see the CDN URL</pre>
|
|
516
|
-
</div>
|
|
517
|
-
</div>
|
|
518
|
-
</div>
|
|
519
|
-
</div>
|
|
520
|
-
</div>
|
|
521
|
-
|
|
522
|
-
<!-- ══════ AUDIT & COMPLIANCE ══════ -->
|
|
523
|
-
<div id="view-audit" class="view">
|
|
524
|
-
<div class="page-header"><h1>Audit & Compliance</h1></div>
|
|
525
|
-
<div class="site-select-bar">
|
|
526
|
-
<label>Site:</label>
|
|
527
|
-
<select class="form-input site-selector" onchange="loadAudit()"></select>
|
|
528
|
-
</div>
|
|
529
|
-
<div class="prem-grid-2">
|
|
530
|
-
<div class="card">
|
|
531
|
-
<h3 style="margin-bottom:20px;">Compliance Settings</h3>
|
|
532
|
-
<div class="form-group">
|
|
533
|
-
<label>Retention Days</label>
|
|
534
|
-
<input type="number" class="form-input" id="auditRetention" value="90" min="1">
|
|
535
|
-
</div>
|
|
536
|
-
<div class="toggle-wrap">
|
|
537
|
-
<div class="toggle-label">HIPAA Mode<small>Enable HIPAA compliance</small></div>
|
|
538
|
-
<label class="toggle"><input type="checkbox" id="auditHipaa"><span class="toggle-slider"></span></label>
|
|
539
|
-
</div>
|
|
540
|
-
<div class="toggle-wrap">
|
|
541
|
-
<div class="toggle-label">GDPR Mode<small>Enable GDPR compliance</small></div>
|
|
542
|
-
<label class="toggle"><input type="checkbox" id="auditGdpr"><span class="toggle-slider"></span></label>
|
|
543
|
-
</div>
|
|
544
|
-
<div class="toggle-wrap">
|
|
545
|
-
<div class="toggle-label">SOC2 Mode<small>Enable SOC2 compliance</small></div>
|
|
546
|
-
<label class="toggle"><input type="checkbox" id="auditSoc2"><span class="toggle-slider"></span></label>
|
|
547
|
-
</div>
|
|
548
|
-
<div class="toggle-wrap">
|
|
549
|
-
<div class="toggle-label">Auto-Purge<small>Automatically purge old logs</small></div>
|
|
550
|
-
<label class="toggle"><input type="checkbox" id="auditAutoPurge" checked><span class="toggle-slider"></span></label>
|
|
551
|
-
</div>
|
|
552
|
-
<div style="margin-top:16px;display:flex;gap:10px;">
|
|
553
|
-
<button class="btn btn-primary btn-sm" onclick="saveCompliance()">Save Compliance</button>
|
|
554
|
-
<button class="btn btn-danger btn-sm" onclick="purgeAuditLogs()">Purge Old Logs</button>
|
|
555
|
-
</div>
|
|
556
|
-
</div>
|
|
557
|
-
<div>
|
|
558
|
-
<div class="card">
|
|
559
|
-
<h3 style="margin-bottom:16px;">Export Logs</h3>
|
|
560
|
-
<div class="form-group"><label>From</label><input type="date" class="form-input" id="auditSince"></div>
|
|
561
|
-
<div class="form-group"><label>Until</label><input type="date" class="form-input" id="auditUntil"></div>
|
|
562
|
-
<div style="display:flex;gap:10px;">
|
|
563
|
-
<button class="btn btn-secondary btn-sm" onclick="exportAuditLogs('csv')">📥 Export CSV</button>
|
|
564
|
-
<button class="btn btn-secondary btn-sm" onclick="exportAuditLogs('json')">📥 Export JSON</button>
|
|
565
|
-
</div>
|
|
566
|
-
</div>
|
|
567
|
-
</div>
|
|
568
|
-
</div>
|
|
569
|
-
<h3 class="section-title">Audit Logs</h3>
|
|
570
|
-
<div class="site-select-bar">
|
|
571
|
-
<label>Filter:</label>
|
|
572
|
-
<select class="form-input" id="auditActionFilter" onchange="loadAuditLogs()" style="width:auto;min-width:150px;">
|
|
573
|
-
<option value="">All Actions</option>
|
|
574
|
-
</select>
|
|
575
|
-
</div>
|
|
576
|
-
<div class="table-wrapper">
|
|
577
|
-
<table>
|
|
578
|
-
<thead><tr><th>Timestamp</th><th>Action</th><th>Resource</th><th>User</th><th>IP</th></tr></thead>
|
|
579
|
-
<tbody id="auditLogsTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
|
|
580
|
-
</table>
|
|
581
|
-
</div>
|
|
582
|
-
<div style="display:flex;gap:10px;margin-top:12px;align-items:center;" id="auditPagination"></div>
|
|
583
|
-
</div>
|
|
584
|
-
|
|
585
|
-
<!-- ══════ SANDBOX ══════ -->
|
|
586
|
-
<div id="view-sandbox" class="view">
|
|
587
|
-
<div class="page-header">
|
|
588
|
-
<h1>Sandbox</h1>
|
|
589
|
-
<button class="btn btn-primary btn-sm" onclick="createSandbox()">+ Create Sandbox</button>
|
|
590
|
-
</div>
|
|
591
|
-
<div class="site-select-bar">
|
|
592
|
-
<label>Site:</label>
|
|
593
|
-
<select class="form-input site-selector" onchange="loadSandboxes()"></select>
|
|
594
|
-
</div>
|
|
595
|
-
<h3 class="section-title">Environments</h3>
|
|
596
|
-
<div class="table-wrapper">
|
|
597
|
-
<table>
|
|
598
|
-
<thead><tr><th>Name</th><th>Status</th><th>Traffic Generated</th><th>Created</th><th>Actions</th></tr></thead>
|
|
599
|
-
<tbody id="sandboxTable"><tr><td colspan="5" class="empty-state">Select a site</td></tr></tbody>
|
|
600
|
-
</table>
|
|
601
|
-
</div>
|
|
602
|
-
<div id="sandboxDetail" style="display:none;">
|
|
603
|
-
<h3 class="section-title">Simulate Traffic</h3>
|
|
604
|
-
<div class="card" style="max-width:500px;">
|
|
605
|
-
<div class="form-group">
|
|
606
|
-
<label>Agent Count</label>
|
|
607
|
-
<input type="number" class="form-input" id="simAgentCount" value="10" min="1" max="1000">
|
|
608
|
-
</div>
|
|
609
|
-
<div class="form-group">
|
|
610
|
-
<label>Duration (seconds)</label>
|
|
611
|
-
<input type="number" class="form-input" id="simDuration" value="60" min="1" max="3600">
|
|
612
|
-
</div>
|
|
613
|
-
<div class="form-group">
|
|
614
|
-
<label>Actions per Agent</label>
|
|
615
|
-
<input type="number" class="form-input" id="simActions" value="5" min="1" max="100">
|
|
616
|
-
</div>
|
|
617
|
-
<button class="btn btn-primary btn-sm" onclick="simulateTraffic()">▶ Run Simulation</button>
|
|
618
|
-
</div>
|
|
619
|
-
<div id="simResults" style="display:none;">
|
|
620
|
-
<h3 class="section-title">Simulation Results</h3>
|
|
621
|
-
<div class="card" id="simResultsContent"></div>
|
|
622
|
-
</div>
|
|
623
|
-
<h3 class="section-title">Benchmarks</h3>
|
|
624
|
-
<div class="site-select-bar">
|
|
625
|
-
<label>Type:</label>
|
|
626
|
-
<select class="form-input" id="benchType" style="width:auto;min-width:180px;">
|
|
627
|
-
<option value="rate_limit">Rate Limit</option>
|
|
628
|
-
<option value="response_time">Response Time</option>
|
|
629
|
-
<option value="throughput">Throughput</option>
|
|
630
|
-
</select>
|
|
631
|
-
<button class="btn btn-primary btn-sm" onclick="runBenchmark()">▶ Run Benchmark</button>
|
|
632
|
-
<button class="btn btn-secondary btn-sm" onclick="compareBenchmarks()">📊 Compare</button>
|
|
633
|
-
</div>
|
|
634
|
-
<div class="table-wrapper">
|
|
635
|
-
<table>
|
|
636
|
-
<thead><tr><th>Type</th><th>Before</th><th>After</th><th>Improvement</th><th>Date</th></tr></thead>
|
|
637
|
-
<tbody id="benchmarksTable"><tr><td colspan="5" class="empty-state">No benchmarks</td></tr></tbody>
|
|
638
|
-
</table>
|
|
639
|
-
</div>
|
|
640
|
-
<div id="benchCompare" style="display:none;">
|
|
641
|
-
<h3 class="section-title">Benchmark Comparison</h3>
|
|
642
|
-
<div class="card" id="benchCompareContent"></div>
|
|
643
|
-
</div>
|
|
644
|
-
</div>
|
|
645
|
-
</div>
|
|
646
|
-
|
|
647
|
-
</main>
|
|
648
|
-
</div>
|
|
649
|
-
|
|
650
|
-
<!-- ══════ MODALS ══════ -->
|
|
651
|
-
|
|
652
|
-
<div class="modal-overlay" id="createAgentModal">
|
|
653
|
-
<div class="modal" style="max-width:600px;">
|
|
654
|
-
<div class="modal-header"><h2>Create Agent</h2><button class="modal-close" onclick="closeModal('createAgentModal')">×</button></div>
|
|
655
|
-
<div class="modal-body">
|
|
656
|
-
<div class="alert alert-error" id="createAgentError"></div>
|
|
657
|
-
<div class="form-group"><label>Name</label><input type="text" class="form-input" id="agentName" placeholder="My Agent"></div>
|
|
658
|
-
<div class="form-group"><label>Description</label><input type="text" class="form-input" id="agentDesc" placeholder="What does this agent do?"></div>
|
|
659
|
-
<div class="form-group">
|
|
660
|
-
<label>Steps (JSON array)</label>
|
|
661
|
-
<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>
|
|
662
|
-
<small style="color:var(--text-muted);display:block;margin-top:4px;">Each step: { action, selector, value, waitMs }</small>
|
|
663
|
-
</div>
|
|
664
|
-
<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>
|
|
665
|
-
</div>
|
|
666
|
-
<div class="modal-footer">
|
|
667
|
-
<button class="btn btn-secondary" onclick="closeModal('createAgentModal')">Cancel</button>
|
|
668
|
-
<button class="btn btn-primary" onclick="createAgent()">Create</button>
|
|
669
|
-
</div>
|
|
670
|
-
</div>
|
|
671
|
-
</div>
|
|
672
|
-
|
|
673
|
-
<div class="modal-overlay" id="agentDetailModal">
|
|
674
|
-
<div class="modal" style="max-width:700px;">
|
|
675
|
-
<div class="modal-header"><h2 id="agentDetailTitle">Agent Details</h2><button class="modal-close" onclick="closeModal('agentDetailModal')">×</button></div>
|
|
676
|
-
<div class="modal-body" id="agentDetailBody"></div>
|
|
677
|
-
<div class="modal-footer">
|
|
678
|
-
<button class="btn btn-danger btn-sm" id="agentDeleteBtn">Delete</button>
|
|
679
|
-
<button class="btn btn-secondary btn-sm" id="agentRunBtn">▶ Run Now</button>
|
|
680
|
-
<button class="btn btn-secondary" onclick="closeModal('agentDetailModal')">Close</button>
|
|
681
|
-
</div>
|
|
682
|
-
</div>
|
|
683
|
-
</div>
|
|
684
|
-
|
|
685
|
-
<div class="modal-overlay" id="createWebhookModal">
|
|
686
|
-
<div class="modal">
|
|
687
|
-
<div class="modal-header"><h2>Add Webhook</h2><button class="modal-close" onclick="closeModal('createWebhookModal')">×</button></div>
|
|
688
|
-
<div class="modal-body">
|
|
689
|
-
<div class="alert alert-error" id="createWebhookError"></div>
|
|
690
|
-
<div class="form-group"><label>Name</label><input type="text" class="form-input" id="whName" placeholder="My Webhook"></div>
|
|
691
|
-
<div class="form-group"><label>URL</label><input type="url" class="form-input" id="whUrl" placeholder="https://example.com/webhook"></div>
|
|
692
|
-
<div class="form-group">
|
|
693
|
-
<label>Events</label>
|
|
694
|
-
<div class="checkbox-group" id="whEvents">
|
|
695
|
-
<label><input type="checkbox" value="action.executed" checked> action.executed</label>
|
|
696
|
-
<label><input type="checkbox" value="agent.detected"> agent.detected</label>
|
|
697
|
-
<label><input type="checkbox" value="security.alert"> security.alert</label>
|
|
698
|
-
<label><input type="checkbox" value="agent.blocked"> agent.blocked</label>
|
|
699
|
-
</div>
|
|
700
|
-
</div>
|
|
701
|
-
<div class="form-group"><label>Secret (optional)</label><input type="text" class="form-input" id="whSecret" placeholder="Auto-generated if empty"></div>
|
|
702
|
-
</div>
|
|
703
|
-
<div class="modal-footer">
|
|
704
|
-
<button class="btn btn-secondary" onclick="closeModal('createWebhookModal')">Cancel</button>
|
|
705
|
-
<button class="btn btn-primary" onclick="createWebhook()">Create</button>
|
|
706
|
-
</div>
|
|
707
|
-
</div>
|
|
708
|
-
</div>
|
|
709
|
-
|
|
710
|
-
<div class="modal-overlay" id="createCrmModal">
|
|
711
|
-
<div class="modal">
|
|
712
|
-
<div class="modal-header"><h2>Add CRM Integration</h2><button class="modal-close" onclick="closeModal('createCrmModal')">×</button></div>
|
|
713
|
-
<div class="modal-body">
|
|
714
|
-
<div class="alert alert-error" id="createCrmError"></div>
|
|
715
|
-
<div class="form-group">
|
|
716
|
-
<label>Provider</label>
|
|
717
|
-
<select class="form-input" id="crmProvider">
|
|
718
|
-
<option value="salesforce">Salesforce</option>
|
|
719
|
-
<option value="hubspot">HubSpot</option>
|
|
720
|
-
<option value="zoho">Zoho</option>
|
|
721
|
-
</select>
|
|
722
|
-
</div>
|
|
723
|
-
<div class="form-group"><label>Config (JSON)</label><textarea class="form-input" id="crmConfig" rows="4" placeholder='{"apiKey":"...","instanceUrl":"..."}'></textarea></div>
|
|
724
|
-
</div>
|
|
725
|
-
<div class="modal-footer">
|
|
726
|
-
<button class="btn btn-secondary" onclick="closeModal('createCrmModal')">Cancel</button>
|
|
727
|
-
<button class="btn btn-primary" onclick="createCrmIntegration()">Add Integration</button>
|
|
728
|
-
</div>
|
|
729
|
-
</div>
|
|
730
|
-
</div>
|
|
731
|
-
|
|
732
|
-
<div class="modal-overlay" id="inviteTeamModal">
|
|
733
|
-
<div class="modal">
|
|
734
|
-
<div class="modal-header"><h2>Invite Team Member</h2><button class="modal-close" onclick="closeModal('inviteTeamModal')">×</button></div>
|
|
735
|
-
<div class="modal-body">
|
|
736
|
-
<div class="alert alert-error" id="inviteTeamError"></div>
|
|
737
|
-
<div class="form-group"><label>Email</label><input type="email" class="form-input" id="teamEmail" placeholder="user@example.com"></div>
|
|
738
|
-
<div class="form-group"><label>Name</label><input type="text" class="form-input" id="teamName" placeholder="Full Name"></div>
|
|
739
|
-
<div class="form-group"><label>Password</label><input type="password" class="form-input" id="teamPassword" placeholder="Initial password"></div>
|
|
740
|
-
<div class="form-group">
|
|
741
|
-
<label>Role</label>
|
|
742
|
-
<select class="form-input" id="teamRole">
|
|
743
|
-
<option value="viewer">Viewer</option>
|
|
744
|
-
<option value="editor">Editor</option>
|
|
745
|
-
<option value="admin">Admin</option>
|
|
746
|
-
</select>
|
|
747
|
-
</div>
|
|
748
|
-
<div class="form-group">
|
|
749
|
-
<label>Site Access</label>
|
|
750
|
-
<div class="checkbox-group" id="teamSiteAccess"></div>
|
|
751
|
-
</div>
|
|
752
|
-
<div class="form-group"><label>Monthly Quota (actions)</label><input type="number" class="form-input" id="teamQuota" placeholder="Leave empty for unlimited"></div>
|
|
753
|
-
</div>
|
|
754
|
-
<div class="modal-footer">
|
|
755
|
-
<button class="btn btn-secondary" onclick="closeModal('inviteTeamModal')">Cancel</button>
|
|
756
|
-
<button class="btn btn-primary" onclick="inviteTeamMember()">Invite</button>
|
|
757
|
-
</div>
|
|
758
|
-
</div>
|
|
759
|
-
</div>
|
|
760
|
-
|
|
761
|
-
<div class="modal-overlay" id="createTicketModal">
|
|
762
|
-
<div class="modal">
|
|
763
|
-
<div class="modal-header"><h2>Create Ticket</h2><button class="modal-close" onclick="closeModal('createTicketModal')">×</button></div>
|
|
764
|
-
<div class="modal-body">
|
|
765
|
-
<div class="alert alert-error" id="createTicketError"></div>
|
|
766
|
-
<div class="form-group"><label>Subject</label><input type="text" class="form-input" id="ticketSubject" placeholder="Describe your issue"></div>
|
|
767
|
-
<div class="form-group">
|
|
768
|
-
<label>Priority</label>
|
|
769
|
-
<select class="form-input" id="ticketPriority">
|
|
770
|
-
<option value="low">Low</option>
|
|
771
|
-
<option value="normal" selected>Normal</option>
|
|
772
|
-
<option value="high">High</option>
|
|
773
|
-
<option value="urgent">Urgent</option>
|
|
774
|
-
</select>
|
|
775
|
-
</div>
|
|
776
|
-
<div class="form-group">
|
|
777
|
-
<label>Category</label>
|
|
778
|
-
<select class="form-input" id="ticketCategory">
|
|
779
|
-
<option value="general">General</option>
|
|
780
|
-
<option value="billing">Billing</option>
|
|
781
|
-
<option value="technical">Technical</option>
|
|
782
|
-
<option value="feature">Feature Request</option>
|
|
783
|
-
<option value="bug">Bug Report</option>
|
|
784
|
-
</select>
|
|
785
|
-
</div>
|
|
786
|
-
</div>
|
|
787
|
-
<div class="modal-footer">
|
|
788
|
-
<button class="btn btn-secondary" onclick="closeModal('createTicketModal')">Cancel</button>
|
|
789
|
-
<button class="btn btn-primary" onclick="createTicket()">Create</button>
|
|
790
|
-
</div>
|
|
791
|
-
</div>
|
|
792
|
-
</div>
|
|
793
|
-
|
|
794
|
-
<div class="modal-overlay" id="ticketDetailModal">
|
|
795
|
-
<div class="modal" style="max-width:700px;">
|
|
796
|
-
<div class="modal-header"><h2 id="ticketDetailTitle">Ticket</h2><button class="modal-close" onclick="closeModal('ticketDetailModal')">×</button></div>
|
|
797
|
-
<div class="modal-body">
|
|
798
|
-
<div style="display:flex;gap:10px;align-items:center;margin-bottom:16px;">
|
|
799
|
-
<label style="font-size:0.85rem;font-weight:600;color:var(--text-secondary);">Status:</label>
|
|
800
|
-
<select class="form-input" id="ticketStatusSelect" style="width:auto;min-width:140px;" onchange="updateTicketStatus()">
|
|
801
|
-
<option value="open">Open</option>
|
|
802
|
-
<option value="in_progress">In Progress</option>
|
|
803
|
-
<option value="waiting">Waiting</option>
|
|
804
|
-
<option value="resolved">Resolved</option>
|
|
805
|
-
<option value="closed">Closed</option>
|
|
806
|
-
</select>
|
|
807
|
-
</div>
|
|
808
|
-
<div class="msg-list" id="ticketMessages"></div>
|
|
809
|
-
<div class="form-group" style="margin-bottom:0;">
|
|
810
|
-
<textarea class="form-input" id="ticketReply" rows="3" placeholder="Type your reply..."></textarea>
|
|
811
|
-
</div>
|
|
812
|
-
</div>
|
|
813
|
-
<div class="modal-footer">
|
|
814
|
-
<button class="btn btn-secondary" onclick="closeModal('ticketDetailModal')">Close</button>
|
|
815
|
-
<button class="btn btn-primary" onclick="sendTicketReply()">Send Reply</button>
|
|
816
|
-
</div>
|
|
817
|
-
</div>
|
|
818
|
-
</div>
|
|
819
|
-
|
|
820
|
-
<div class="modal-overlay" id="packDetailModal">
|
|
821
|
-
<div class="modal" style="max-width:600px;">
|
|
822
|
-
<div class="modal-header"><h2 id="packDetailTitle">Pack Details</h2><button class="modal-close" onclick="closeModal('packDetailModal')">×</button></div>
|
|
823
|
-
<div class="modal-body" id="packDetailBody"></div>
|
|
824
|
-
<div class="modal-footer">
|
|
825
|
-
<button class="btn btn-secondary" onclick="closeModal('packDetailModal')">Close</button>
|
|
826
|
-
</div>
|
|
827
|
-
</div>
|
|
828
|
-
</div>
|
|
829
|
-
|
|
830
|
-
<script>
|
|
831
|
-
const PAPI = '/api/premium';
|
|
832
|
-
const API = '/api';
|
|
833
|
-
let token = localStorage.getItem('wab_token');
|
|
834
|
-
let user = JSON.parse(localStorage.getItem('wab_user') || 'null');
|
|
835
|
-
let sites = [];
|
|
836
|
-
let currentSandboxId = null;
|
|
837
|
-
let currentTicketId = null;
|
|
838
|
-
let auditPage = 0;
|
|
839
|
-
const AUDIT_LIMIT = 25;
|
|
840
|
-
|
|
841
|
-
if (!token) window.location.href = '/login';
|
|
842
|
-
|
|
843
|
-
function headers() {
|
|
844
|
-
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.getItem('wab_token') };
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function logout() {
|
|
848
|
-
localStorage.removeItem('wab_token');
|
|
849
|
-
localStorage.removeItem('wab_user');
|
|
850
|
-
window.location.href = '/login';
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function esc(s) {
|
|
854
|
-
if (!s) return '';
|
|
855
|
-
const d = document.createElement('div');
|
|
856
|
-
d.textContent = s;
|
|
857
|
-
return d.innerHTML;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function fmtDate(d) {
|
|
861
|
-
if (!d) return '—';
|
|
862
|
-
try { return new Date(d).toLocaleString(); } catch { return d; }
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function fmtBytes(b) {
|
|
866
|
-
if (!b || b === 0) return '0 B';
|
|
867
|
-
const u = ['B', 'KB', 'MB', 'GB'];
|
|
868
|
-
let i = 0;
|
|
869
|
-
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
|
|
870
|
-
return b.toFixed(i ? 1 : 0) + ' ' + u[i];
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function openModal(id) { document.getElementById(id).classList.add('active'); }
|
|
874
|
-
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
|
875
|
-
|
|
876
|
-
function getSiteId() {
|
|
877
|
-
const view = document.querySelector('.view.active');
|
|
878
|
-
if (!view) return null;
|
|
879
|
-
const sel = view.querySelector('.site-selector');
|
|
880
|
-
return sel ? sel.value : null;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// ─── Navigation ──────────────────────────────────────────────────────
|
|
884
|
-
document.querySelectorAll('.sidebar-nav a[data-view]').forEach(link => {
|
|
885
|
-
link.addEventListener('click', (e) => {
|
|
886
|
-
e.preventDefault();
|
|
887
|
-
switchView(link.dataset.view);
|
|
888
|
-
});
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
function switchView(view) {
|
|
892
|
-
document.querySelectorAll('.sidebar-nav a').forEach(a => a.classList.remove('active'));
|
|
893
|
-
const link = document.querySelector(`.sidebar-nav a[data-view="${view}"]`);
|
|
894
|
-
if (link) link.classList.add('active');
|
|
895
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
896
|
-
const el = document.getElementById('view-' + view);
|
|
897
|
-
if (el) el.classList.add('active');
|
|
898
|
-
onViewActivated(view);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function onViewActivated(view) {
|
|
902
|
-
switch (view) {
|
|
903
|
-
case 'overview': loadOverview(); break;
|
|
904
|
-
case 'traffic': loadTraffic(); break;
|
|
905
|
-
case 'security': loadSecurity(); break;
|
|
906
|
-
case 'actions': loadPacks(); loadInstalledPacks(); break;
|
|
907
|
-
case 'agents': loadAgents(); break;
|
|
908
|
-
case 'webhooks': loadWebhooksView(); break;
|
|
909
|
-
case 'team': loadTeam(); break;
|
|
910
|
-
case 'support': loadSupport(); break;
|
|
911
|
-
case 'script': loadScriptPlugins(); loadScriptConfig(); break;
|
|
912
|
-
case 'stealth': loadStealth(); break;
|
|
913
|
-
case 'cdn': loadCdn(); break;
|
|
914
|
-
case 'audit': loadAudit(); break;
|
|
915
|
-
case 'sandbox': loadSandboxes(); break;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// ─── Site Loading ────────────────────────────────────────────────────
|
|
920
|
-
async function loadSites() {
|
|
921
|
-
try {
|
|
922
|
-
const res = await fetch(API + '/sites', { headers: headers() });
|
|
923
|
-
if (res.status === 401 || res.status === 403) return logout();
|
|
924
|
-
const data = await res.json();
|
|
925
|
-
sites = data.sites || [];
|
|
926
|
-
populateSiteSelectors();
|
|
927
|
-
} catch (err) { console.error('loadSites:', err); }
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
function populateSiteSelectors() {
|
|
931
|
-
const opts = '<option value="">Select a site</option>' + sites.map(s => '<option value="' + s.id + '">' + esc(s.name) + ' (' + esc(s.domain) + ')</option>').join('');
|
|
932
|
-
document.querySelectorAll('.site-selector').forEach(sel => { sel.innerHTML = opts; });
|
|
933
|
-
const teamSA = document.getElementById('teamSiteAccess');
|
|
934
|
-
if (teamSA) {
|
|
935
|
-
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('');
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// ─── Overview ────────────────────────────────────────────────────────
|
|
940
|
-
async function loadOverview() {
|
|
941
|
-
const promises = [];
|
|
942
|
-
if (sites.length > 0) {
|
|
943
|
-
const sid = sites[0].id;
|
|
944
|
-
promises.push(
|
|
945
|
-
fetch(PAPI + '/traffic/' + sid + '/alerts?limit=10&acknowledged=false', { headers: headers() }).then(r => r.json()).catch(() => ({ alerts: [] })),
|
|
946
|
-
fetch(PAPI + '/security/' + sid + '/events?limit=50&since=' + new Date(Date.now() - 86400000).toISOString(), { headers: headers() }).then(r => r.json()).catch(() => ({ events: [] })),
|
|
947
|
-
fetch(PAPI + '/actions/' + sid + '/installed', { headers: headers() }).then(r => r.json()).catch(() => ({ installed: [] })),
|
|
948
|
-
fetch(PAPI + '/agents/' + sid, { headers: headers() }).then(r => r.json()).catch(() => ({ agents: [] }))
|
|
949
|
-
);
|
|
950
|
-
} else {
|
|
951
|
-
promises.push(Promise.resolve({ alerts: [] }), Promise.resolve({ events: [] }), Promise.resolve({ installed: [] }), Promise.resolve({ agents: [] }));
|
|
952
|
-
}
|
|
953
|
-
promises.push(
|
|
954
|
-
fetch(PAPI + '/team', { headers: headers() }).then(r => r.json()).catch(() => ({ subUsers: [] })),
|
|
955
|
-
fetch(PAPI + '/support/stats', { headers: headers() }).then(r => r.json()).catch(() => ({ stats: { open: 0 } }))
|
|
956
|
-
);
|
|
957
|
-
|
|
958
|
-
const [alertsData, eventsData, packsData, agentsData, teamData, ticketData] = await Promise.all(promises);
|
|
959
|
-
|
|
960
|
-
document.getElementById('ovAlerts').textContent = (alertsData.alerts || []).length;
|
|
961
|
-
document.getElementById('ovSecurity').textContent = (eventsData.events || []).length;
|
|
962
|
-
document.getElementById('ovPacks').textContent = (packsData.installed || []).length;
|
|
963
|
-
document.getElementById('ovAgents').textContent = (agentsData.agents || []).filter(a => a.status === 'active').length;
|
|
964
|
-
document.getElementById('ovTeam').textContent = (teamData.subUsers || []).length;
|
|
965
|
-
document.getElementById('ovTickets').textContent = (ticketData.stats || {}).open || 0;
|
|
966
|
-
|
|
967
|
-
const alerts = alertsData.alerts || [];
|
|
968
|
-
const tbody = document.getElementById('ovAlertsTable');
|
|
969
|
-
if (alerts.length === 0) {
|
|
970
|
-
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No recent alerts</td></tr>';
|
|
971
|
-
} else {
|
|
972
|
-
tbody.innerHTML = alerts.slice(0, 10).map(a => '<tr>' +
|
|
973
|
-
'<td>' + esc(a.alert_type) + '</td>' +
|
|
974
|
-
'<td><span class="badge sev-' + (a.severity || 'low') + '">' + esc(a.severity) + '</span></td>' +
|
|
975
|
-
'<td>' + esc(a.message) + '</td>' +
|
|
976
|
-
'<td>' + fmtDate(a.created_at) + '</td>' +
|
|
977
|
-
'<td><button class="btn btn-sm btn-secondary" onclick="ackAlertOverview(\'' + a.id + '\')">Acknowledge</button></td>' +
|
|
978
|
-
'</tr>').join('');
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
async function ackAlertOverview(alertId) {
|
|
983
|
-
if (!sites.length) return;
|
|
984
|
-
await fetch(PAPI + '/traffic/' + sites[0].id + '/alerts/' + alertId + '/acknowledge', { method: 'POST', headers: headers() });
|
|
985
|
-
loadOverview();
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
async function quickCheckAnomalies() {
|
|
989
|
-
if (!sites.length) { alert('No sites available'); return; }
|
|
990
|
-
const res = await fetch(PAPI + '/traffic/' + sites[0].id + '/check-anomalies', { method: 'POST', headers: headers() });
|
|
991
|
-
const data = await res.json();
|
|
992
|
-
alert('Anomaly check complete. ' + (data.alerts || []).length + ' new alerts found.');
|
|
993
|
-
loadOverview();
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
async function quickSecurityReport() {
|
|
997
|
-
if (!sites.length) { alert('No sites available'); return; }
|
|
998
|
-
switchView('security');
|
|
999
|
-
setTimeout(() => {
|
|
1000
|
-
const sel = document.querySelector('#view-security .site-selector');
|
|
1001
|
-
if (sel && sites.length) { sel.value = sites[0].id; loadSecurity(); loadSecurityReport(); }
|
|
1002
|
-
}, 100);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// ─── Traffic Intelligence ────────────────────────────────────────────
|
|
1006
|
-
async function loadTraffic() {
|
|
1007
|
-
const sid = getSiteId();
|
|
1008
|
-
if (!sid) return;
|
|
1009
|
-
try {
|
|
1010
|
-
const [profilesRes, statsRes, alertsRes] = await Promise.all([
|
|
1011
|
-
fetch(PAPI + '/traffic/' + sid + '/profiles?limit=50', { headers: headers() }),
|
|
1012
|
-
fetch(PAPI + '/traffic/' + sid + '/stats?days=30', { headers: headers() }),
|
|
1013
|
-
fetch(PAPI + '/traffic/' + sid + '/alerts?limit=20', { headers: headers() })
|
|
1014
|
-
]);
|
|
1015
|
-
const profiles = await profilesRes.json();
|
|
1016
|
-
const stats = await statsRes.json();
|
|
1017
|
-
const alerts = await alertsRes.json();
|
|
1018
|
-
|
|
1019
|
-
const s = stats.stats || {};
|
|
1020
|
-
document.getElementById('tfTotal').textContent = s.totalAgents || 0;
|
|
1021
|
-
const byType = s.byType || [];
|
|
1022
|
-
document.getElementById('tfFriendly').textContent = (byType.find(t => t.agent_type === 'friendly') || {}).count || 0;
|
|
1023
|
-
document.getElementById('tfSuspicious').textContent = (byType.find(t => t.agent_type === 'suspicious') || {}).count || 0;
|
|
1024
|
-
document.getElementById('tfAggressive').textContent = (byType.find(t => t.agent_type === 'aggressive') || {}).count || 0;
|
|
1025
|
-
|
|
1026
|
-
const profs = profiles.profiles || [];
|
|
1027
|
-
const tbody = document.getElementById('trafficTable');
|
|
1028
|
-
if (profs.length === 0) {
|
|
1029
|
-
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No agent profiles</td></tr>';
|
|
1030
|
-
} else {
|
|
1031
|
-
tbody.innerHTML = profs.map(p => '<tr>' +
|
|
1032
|
-
'<td><strong style="color:var(--text-primary)">' + esc(p.agent_signature) + '</strong></td>' +
|
|
1033
|
-
'<td><span class="badge type-' + (p.agent_type || 'unknown') + '">' + esc(p.agent_type) + '</span></td>' +
|
|
1034
|
-
'<td>' + esc(p.platform) + '</td>' +
|
|
1035
|
-
'<td>' + esc(p.country || '—') + '</td>' +
|
|
1036
|
-
'<td>' + (p.total_requests || 0) + '</td>' +
|
|
1037
|
-
'<td>' + fmtDate(p.last_seen) + '</td>' +
|
|
1038
|
-
'<td><button class="btn btn-danger btn-sm" onclick="blockFromTraffic(\'' + esc(p.agent_signature) + '\')">Block</button></td>' +
|
|
1039
|
-
'</tr>').join('');
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const al = alerts.alerts || [];
|
|
1043
|
-
const atbody = document.getElementById('trafficAlerts');
|
|
1044
|
-
if (al.length === 0) {
|
|
1045
|
-
atbody.innerHTML = '<tr><td colspan="5" class="empty-state">No alerts</td></tr>';
|
|
1046
|
-
} else {
|
|
1047
|
-
atbody.innerHTML = al.map(a => '<tr>' +
|
|
1048
|
-
'<td>' + esc(a.alert_type) + '</td>' +
|
|
1049
|
-
'<td><span class="badge sev-' + (a.severity || 'low') + '">' + esc(a.severity) + '</span></td>' +
|
|
1050
|
-
'<td>' + esc(a.message) + '</td>' +
|
|
1051
|
-
'<td>' + fmtDate(a.created_at) + '</td>' +
|
|
1052
|
-
'<td>' + (a.acknowledged ? '<span class="badge badge-active">Ack</span>' : '<button class="btn btn-sm btn-secondary" onclick="ackAlert(\'' + a.id + '\')">Acknowledge</button>') + '</td>' +
|
|
1053
|
-
'</tr>').join('');
|
|
1054
|
-
}
|
|
1055
|
-
} catch (err) { console.error('loadTraffic:', err); }
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
async function blockFromTraffic(sig) {
|
|
1059
|
-
const sid = getSiteId();
|
|
1060
|
-
if (!sid || !confirm('Block agent "' + sig + '"?')) return;
|
|
1061
|
-
await fetch(PAPI + '/security/' + sid + '/block', { method: 'POST', headers: headers(), body: JSON.stringify({ agentSignature: sig, reason: 'Blocked from traffic view' }) });
|
|
1062
|
-
alert('Agent blocked');
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
async function ackAlert(alertId) {
|
|
1066
|
-
const sid = getSiteId();
|
|
1067
|
-
if (!sid) return;
|
|
1068
|
-
await fetch(PAPI + '/traffic/' + sid + '/alerts/' + alertId + '/acknowledge', { method: 'POST', headers: headers() });
|
|
1069
|
-
loadTraffic();
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
async function checkAnomalies() {
|
|
1073
|
-
const sid = getSiteId();
|
|
1074
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1075
|
-
const res = await fetch(PAPI + '/traffic/' + sid + '/check-anomalies', { method: 'POST', headers: headers() });
|
|
1076
|
-
const data = await res.json();
|
|
1077
|
-
alert('Check complete. ' + (data.alerts || []).length + ' new alerts.');
|
|
1078
|
-
loadTraffic();
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// ─── Exploit Shield ──────────────────────────────────────────────────
|
|
1082
|
-
async function loadSecurity() {
|
|
1083
|
-
const sid = getSiteId();
|
|
1084
|
-
if (!sid) return;
|
|
1085
|
-
try {
|
|
1086
|
-
const [eventsRes, blockedRes, reportRes] = await Promise.all([
|
|
1087
|
-
fetch(PAPI + '/security/' + sid + '/events?limit=50', { headers: headers() }),
|
|
1088
|
-
fetch(PAPI + '/security/' + sid + '/blocked', { headers: headers() }),
|
|
1089
|
-
fetch(PAPI + '/security/' + sid + '/report?days=1', { headers: headers() })
|
|
1090
|
-
]);
|
|
1091
|
-
const events = await eventsRes.json();
|
|
1092
|
-
const blocked = await blockedRes.json();
|
|
1093
|
-
const report = await reportRes.json();
|
|
1094
|
-
|
|
1095
|
-
const r = report.report || {};
|
|
1096
|
-
document.getElementById('secTotal').textContent = r.totalEvents || 0;
|
|
1097
|
-
const sevDist = r.severityDist || [];
|
|
1098
|
-
document.getElementById('secCritical').textContent = (sevDist.find(s => s.severity === 'critical') || {}).count || 0;
|
|
1099
|
-
document.getElementById('secHigh').textContent = (sevDist.find(s => s.severity === 'high') || {}).count || 0;
|
|
1100
|
-
document.getElementById('secBlocked').textContent = r.activeBlocks || 0;
|
|
1101
|
-
|
|
1102
|
-
const ev = events.events || [];
|
|
1103
|
-
const etbody = document.getElementById('secEventsTable');
|
|
1104
|
-
if (ev.length === 0) {
|
|
1105
|
-
etbody.innerHTML = '<tr><td colspan="5" class="empty-state">No security events</td></tr>';
|
|
1106
|
-
} else {
|
|
1107
|
-
etbody.innerHTML = ev.map(e => {
|
|
1108
|
-
let details = '';
|
|
1109
|
-
try { details = JSON.stringify(JSON.parse(e.details || '{}'), null, 0).substring(0, 80); } catch { details = e.details || ''; }
|
|
1110
|
-
return '<tr>' +
|
|
1111
|
-
'<td>' + esc(e.event_type) + '</td>' +
|
|
1112
|
-
'<td><span class="badge sev-' + (e.severity || 'low') + '">' + esc(e.severity) + '</span></td>' +
|
|
1113
|
-
'<td>' + esc(e.agent_signature || '—') + '</td>' +
|
|
1114
|
-
'<td style="font-size:0.82rem;color:var(--text-muted)">' + esc(details) + '</td>' +
|
|
1115
|
-
'<td>' + fmtDate(e.created_at) + '</td></tr>';
|
|
1116
|
-
}).join('');
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const bl = blocked.blocked || [];
|
|
1120
|
-
const btbody = document.getElementById('secBlockedTable');
|
|
1121
|
-
if (bl.length === 0) {
|
|
1122
|
-
btbody.innerHTML = '<tr><td colspan="5" class="empty-state">No blocked agents</td></tr>';
|
|
1123
|
-
} else {
|
|
1124
|
-
btbody.innerHTML = bl.map(b => '<tr>' +
|
|
1125
|
-
'<td><strong style="color:var(--text-primary)">' + esc(b.agent_signature) + '</strong></td>' +
|
|
1126
|
-
'<td>' + esc(b.reason || '—') + '</td>' +
|
|
1127
|
-
'<td>' + fmtDate(b.blocked_at) + '</td>' +
|
|
1128
|
-
'<td>' + (b.expires_at ? fmtDate(b.expires_at) : 'Never') + '</td>' +
|
|
1129
|
-
'<td><button class="btn btn-sm btn-secondary" onclick="unblockAgent(\'' + b.id + '\')">Unblock</button></td>' +
|
|
1130
|
-
'</tr>').join('');
|
|
1131
|
-
}
|
|
1132
|
-
} catch (err) { console.error('loadSecurity:', err); }
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
async function blockAgent() {
|
|
1136
|
-
const sid = getSiteId();
|
|
1137
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1138
|
-
const sig = document.getElementById('blockSig').value.trim();
|
|
1139
|
-
const reason = document.getElementById('blockReason').value.trim();
|
|
1140
|
-
if (!sig) { alert('Agent signature is required'); return; }
|
|
1141
|
-
await fetch(PAPI + '/security/' + sid + '/block', { method: 'POST', headers: headers(), body: JSON.stringify({ agentSignature: sig, reason: reason }) });
|
|
1142
|
-
document.getElementById('blockSig').value = '';
|
|
1143
|
-
document.getElementById('blockReason').value = '';
|
|
1144
|
-
loadSecurity();
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
async function unblockAgent(blockId) {
|
|
1148
|
-
const sid = getSiteId();
|
|
1149
|
-
if (!sid) return;
|
|
1150
|
-
await fetch(PAPI + '/security/' + sid + '/block/' + blockId, { method: 'DELETE', headers: headers() });
|
|
1151
|
-
loadSecurity();
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
async function loadSecurityReport() {
|
|
1155
|
-
const sid = getSiteId();
|
|
1156
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1157
|
-
const days = document.getElementById('reportDays').value;
|
|
1158
|
-
const res = await fetch(PAPI + '/security/' + sid + '/report?days=' + days, { headers: headers() });
|
|
1159
|
-
const data = await res.json();
|
|
1160
|
-
const r = data.report || {};
|
|
1161
|
-
document.getElementById('secReportContent').innerHTML =
|
|
1162
|
-
'<div class="card" style="margin-top:16px;">' +
|
|
1163
|
-
'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:20px;">' +
|
|
1164
|
-
'<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>' +
|
|
1165
|
-
'<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>' +
|
|
1166
|
-
'<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>' +
|
|
1167
|
-
'</div>' +
|
|
1168
|
-
'<h4 style="margin-bottom:8px;">Severity Distribution</h4>' +
|
|
1169
|
-
'<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px;">' +
|
|
1170
|
-
(r.severityDist || []).map(s => '<span class="badge sev-' + s.severity + '">' + s.severity + ': ' + s.count + '</span>').join('') +
|
|
1171
|
-
'</div>' +
|
|
1172
|
-
'<h4 style="margin-bottom:8px;">Top Blocked Agents</h4>' +
|
|
1173
|
-
((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>') +
|
|
1174
|
-
'</div>';
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// ─── Actions Library ─────────────────────────────────────────────────
|
|
1178
|
-
async function loadPacks() {
|
|
1179
|
-
const platform = document.getElementById('packPlatformFilter').value;
|
|
1180
|
-
const url = PAPI + '/actions/packs' + (platform ? '?platform=' + encodeURIComponent(platform) : '');
|
|
1181
|
-
try {
|
|
1182
|
-
const res = await fetch(url, { headers: headers() });
|
|
1183
|
-
const data = await res.json();
|
|
1184
|
-
const packs = data.packs || [];
|
|
1185
|
-
const grid = document.getElementById('packsGrid');
|
|
1186
|
-
if (packs.length === 0) {
|
|
1187
|
-
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;">No packs available</div>';
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
grid.innerHTML = packs.map(p => '<div class="card pack-card" onclick="showPackDetail(\'' + p.id + '\')">' +
|
|
1191
|
-
'<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px;">' +
|
|
1192
|
-
'<h3 style="font-size:1rem;">' + esc(p.icon || '📦') + ' ' + esc(p.name) + '</h3>' +
|
|
1193
|
-
'<span class="badge badge-' + (p.tier_required || 'free') + '">' + esc(p.tier_required || 'free') + '</span>' +
|
|
1194
|
-
'</div>' +
|
|
1195
|
-
'<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:12px;">' + esc(p.description) + '</p>' +
|
|
1196
|
-
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
|
|
1197
|
-
'<span style="font-size:0.8rem;color:var(--text-muted);">' + esc(p.platform || '—') + ' · v' + esc(p.version || '1.0') + '</span>' +
|
|
1198
|
-
'<button class="btn btn-primary btn-sm" onclick="event.stopPropagation();installPack(\'' + p.id + '\')">Install</button>' +
|
|
1199
|
-
'</div></div>').join('');
|
|
1200
|
-
|
|
1201
|
-
const platforms = [...new Set(packs.map(p => p.platform).filter(Boolean))];
|
|
1202
|
-
const filterSel = document.getElementById('packPlatformFilter');
|
|
1203
|
-
const current = filterSel.value;
|
|
1204
|
-
filterSel.innerHTML = '<option value="">All Platforms</option>' + platforms.map(p => '<option value="' + esc(p) + '"' + (p === current ? ' selected' : '') + '>' + esc(p) + '</option>').join('');
|
|
1205
|
-
} catch (err) { console.error('loadPacks:', err); }
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
async function showPackDetail(packId) {
|
|
1209
|
-
try {
|
|
1210
|
-
const [packRes, actionsRes] = await Promise.all([
|
|
1211
|
-
fetch(PAPI + '/actions/packs/' + packId, { headers: headers() }),
|
|
1212
|
-
fetch(PAPI + '/actions/packs/' + packId + '/actions', { headers: headers() })
|
|
1213
|
-
]);
|
|
1214
|
-
const packData = await packRes.json();
|
|
1215
|
-
const actionsData = await actionsRes.json();
|
|
1216
|
-
const p = packData.pack;
|
|
1217
|
-
const actions = actionsData.actions || [];
|
|
1218
|
-
document.getElementById('packDetailTitle').textContent = (p.icon || '📦') + ' ' + (p.name || 'Pack');
|
|
1219
|
-
document.getElementById('packDetailBody').innerHTML =
|
|
1220
|
-
'<p style="color:var(--text-secondary);margin-bottom:16px;">' + esc(p.description) + '</p>' +
|
|
1221
|
-
'<div style="display:flex;gap:12px;margin-bottom:16px;">' +
|
|
1222
|
-
'<span class="badge badge-' + (p.tier_required || 'free') + '">' + esc(p.tier_required) + '</span>' +
|
|
1223
|
-
'<span style="font-size:0.85rem;color:var(--text-muted);">' + esc(p.platform) + ' · v' + esc(p.version) + '</span></div>' +
|
|
1224
|
-
'<h4 style="margin-bottom:8px;">Actions (' + actions.length + ')</h4>' +
|
|
1225
|
-
(actions.length ? '<div class="table-wrapper"><table><thead><tr><th>Name</th><th>Type</th></tr></thead><tbody>' +
|
|
1226
|
-
actions.map(a => '<tr><td>' + esc(a.name || a.action || '—') + '</td><td>' + esc(a.type || a.trigger_type || '—') + '</td></tr>').join('') +
|
|
1227
|
-
'</tbody></table></div>' : '<p style="color:var(--text-muted);">No action details available</p>');
|
|
1228
|
-
openModal('packDetailModal');
|
|
1229
|
-
} catch (err) { console.error('showPackDetail:', err); }
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
async function installPack(packId) {
|
|
1233
|
-
const sid = getSiteId();
|
|
1234
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1235
|
-
try {
|
|
1236
|
-
const res = await fetch(PAPI + '/actions/' + sid + '/install', { method: 'POST', headers: headers(), body: JSON.stringify({ packId: packId }) });
|
|
1237
|
-
if (res.ok) { alert('Pack installed'); loadInstalledPacks(); } else { const d = await res.json(); alert(d.error || 'Install failed'); }
|
|
1238
|
-
} catch (err) { alert('Connection error'); }
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
async function loadInstalledPacks() {
|
|
1242
|
-
const sid = getSiteId();
|
|
1243
|
-
if (!sid) return;
|
|
1244
|
-
try {
|
|
1245
|
-
const res = await fetch(PAPI + '/actions/' + sid + '/installed', { headers: headers() });
|
|
1246
|
-
const data = await res.json();
|
|
1247
|
-
const installed = data.installed || [];
|
|
1248
|
-
const tbody = document.getElementById('installedPacksTable');
|
|
1249
|
-
if (installed.length === 0) {
|
|
1250
|
-
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No packs installed</td></tr>';
|
|
1251
|
-
} else {
|
|
1252
|
-
tbody.innerHTML = installed.map(i => '<tr>' +
|
|
1253
|
-
'<td><strong style="color:var(--text-primary)">' + esc(i.name) + '</strong></td>' +
|
|
1254
|
-
'<td>' + esc(i.platform || '—') + '</td>' +
|
|
1255
|
-
'<td>' + esc(i.version || '—') + '</td>' +
|
|
1256
|
-
'<td>' + fmtDate(i.installed_at) + '</td>' +
|
|
1257
|
-
'<td><button class="btn btn-danger btn-sm" onclick="uninstallPack(\'' + i.id + '\')">Uninstall</button></td>' +
|
|
1258
|
-
'</tr>').join('');
|
|
1259
|
-
}
|
|
1260
|
-
} catch (err) { console.error('loadInstalledPacks:', err); }
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
async function uninstallPack(installId) {
|
|
1264
|
-
const sid = getSiteId();
|
|
1265
|
-
if (!sid || !confirm('Uninstall this pack?')) return;
|
|
1266
|
-
await fetch(PAPI + '/actions/' + sid + '/install/' + installId, { method: 'DELETE', headers: headers() });
|
|
1267
|
-
loadInstalledPacks();
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// ─── Custom Agents ───────────────────────────────────────────────────
|
|
1271
|
-
async function loadAgents() {
|
|
1272
|
-
const sid = getSiteId();
|
|
1273
|
-
if (!sid) return;
|
|
1274
|
-
try {
|
|
1275
|
-
const res = await fetch(PAPI + '/agents/' + sid, { headers: headers() });
|
|
1276
|
-
const data = await res.json();
|
|
1277
|
-
const agents = data.agents || [];
|
|
1278
|
-
const tbody = document.getElementById('agentsTable');
|
|
1279
|
-
if (agents.length === 0) {
|
|
1280
|
-
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No agents. Create one to get started.</td></tr>';
|
|
1281
|
-
} else {
|
|
1282
|
-
tbody.innerHTML = agents.map(a => '<tr>' +
|
|
1283
|
-
'<td><strong style="color:var(--text-primary)">' + esc(a.name) + '</strong></td>' +
|
|
1284
|
-
'<td><span class="badge status-' + (a.status || 'active') + '">' + esc(a.status || 'active') + '</span></td>' +
|
|
1285
|
-
'<td style="font-family:var(--font-mono);font-size:0.82rem;">' + esc(a.schedule || '—') + '</td>' +
|
|
1286
|
-
'<td>' + (a.run_count || 0) + '</td>' +
|
|
1287
|
-
'<td>' + fmtDate(a.last_run) + '</td>' +
|
|
1288
|
-
'<td><button class="btn btn-sm btn-secondary" onclick="showAgentDetail(\'' + a.id + '\')">Details</button></td>' +
|
|
1289
|
-
'</tr>').join('');
|
|
1290
|
-
}
|
|
1291
|
-
} catch (err) { console.error('loadAgents:', err); }
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
async function createAgent() {
|
|
1295
|
-
const sid = getSiteId();
|
|
1296
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1297
|
-
const errEl = document.getElementById('createAgentError');
|
|
1298
|
-
errEl.style.display = 'none';
|
|
1299
|
-
const name = document.getElementById('agentName').value.trim();
|
|
1300
|
-
const desc = document.getElementById('agentDesc').value.trim();
|
|
1301
|
-
const schedule = document.getElementById('agentSchedule').value.trim();
|
|
1302
|
-
let steps;
|
|
1303
|
-
try { steps = JSON.parse(document.getElementById('agentSteps').value); } catch { errEl.textContent = 'Steps must be valid JSON'; errEl.style.display = 'block'; return; }
|
|
1304
|
-
if (!name) { errEl.textContent = 'Name is required'; errEl.style.display = 'block'; return; }
|
|
1305
|
-
try {
|
|
1306
|
-
const res = await fetch(PAPI + '/agents/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ name, description: desc, steps, schedule: schedule || undefined }) });
|
|
1307
|
-
if (res.ok) {
|
|
1308
|
-
closeModal('createAgentModal');
|
|
1309
|
-
document.getElementById('agentName').value = '';
|
|
1310
|
-
document.getElementById('agentDesc').value = '';
|
|
1311
|
-
document.getElementById('agentSteps').value = '';
|
|
1312
|
-
document.getElementById('agentSchedule').value = '';
|
|
1313
|
-
loadAgents();
|
|
1314
|
-
} else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
|
|
1315
|
-
} catch { errEl.textContent = 'Connection error'; errEl.style.display = 'block'; }
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
async function showAgentDetail(agentId) {
|
|
1319
|
-
const sid = getSiteId();
|
|
1320
|
-
if (!sid) return;
|
|
1321
|
-
try {
|
|
1322
|
-
const [agentRes, runsRes] = await Promise.all([
|
|
1323
|
-
fetch(PAPI + '/agents/' + sid + '/' + agentId, { headers: headers() }),
|
|
1324
|
-
fetch(PAPI + '/agents/' + sid + '/' + agentId + '/runs?limit=10', { headers: headers() })
|
|
1325
|
-
]);
|
|
1326
|
-
const agentData = await agentRes.json();
|
|
1327
|
-
const runsData = await runsRes.json();
|
|
1328
|
-
const a = agentData.agent;
|
|
1329
|
-
const runs = runsData.runs || [];
|
|
1330
|
-
document.getElementById('agentDetailTitle').textContent = a.name;
|
|
1331
|
-
let steps = [];
|
|
1332
|
-
try { steps = JSON.parse(a.steps_json || '[]'); } catch {}
|
|
1333
|
-
document.getElementById('agentDetailBody').innerHTML =
|
|
1334
|
-
'<p style="color:var(--text-secondary);margin-bottom:16px;">' + esc(a.description || 'No description') + '</p>' +
|
|
1335
|
-
'<div style="display:flex;gap:12px;margin-bottom:16px;">' +
|
|
1336
|
-
'<span class="badge status-' + (a.status || 'active') + '">' + esc(a.status) + '</span>' +
|
|
1337
|
-
(a.schedule ? '<span style="font-size:0.85rem;color:var(--text-muted);">Schedule: ' + esc(a.schedule) + '</span>' : '') +
|
|
1338
|
-
'<span style="font-size:0.85rem;color:var(--text-muted);">Runs: ' + (a.run_count || 0) + '</span></div>' +
|
|
1339
|
-
'<h4 style="margin-bottom:8px;">Steps</h4>' +
|
|
1340
|
-
'<div class="snippet-box" style="margin-bottom:20px;"><pre>' + esc(JSON.stringify(steps, null, 2)) + '</pre></div>' +
|
|
1341
|
-
'<h4 style="margin-bottom:8px;">Run History</h4>' +
|
|
1342
|
-
(runs.length ? '<div class="table-wrapper"><table><thead><tr><th>Status</th><th>Started</th><th>Finished</th></tr></thead><tbody>' +
|
|
1343
|
-
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('') +
|
|
1344
|
-
'</tbody></table></div>' : '<p style="color:var(--text-muted);">No runs yet</p>');
|
|
1345
|
-
|
|
1346
|
-
document.getElementById('agentRunBtn').onclick = async () => {
|
|
1347
|
-
await fetch(PAPI + '/agents/' + sid + '/' + agentId + '/run', { method: 'POST', headers: headers() });
|
|
1348
|
-
alert('Agent run started');
|
|
1349
|
-
showAgentDetail(agentId);
|
|
1350
|
-
loadAgents();
|
|
1351
|
-
};
|
|
1352
|
-
document.getElementById('agentDeleteBtn').onclick = async () => {
|
|
1353
|
-
if (!confirm('Delete agent "' + a.name + '"?')) return;
|
|
1354
|
-
await fetch(PAPI + '/agents/' + sid + '/' + agentId, { method: 'DELETE', headers: headers() });
|
|
1355
|
-
closeModal('agentDetailModal');
|
|
1356
|
-
loadAgents();
|
|
1357
|
-
};
|
|
1358
|
-
openModal('agentDetailModal');
|
|
1359
|
-
} catch (err) { console.error('showAgentDetail:', err); }
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// ─── Webhooks & CRM ─────────────────────────────────────────────────
|
|
1363
|
-
async function loadWebhooksView() {
|
|
1364
|
-
const sid = getSiteId();
|
|
1365
|
-
if (!sid) return;
|
|
1366
|
-
try {
|
|
1367
|
-
const [whRes, crmRes] = await Promise.all([
|
|
1368
|
-
fetch(PAPI + '/webhooks/' + sid, { headers: headers() }),
|
|
1369
|
-
fetch(PAPI + '/crm/' + sid, { headers: headers() })
|
|
1370
|
-
]);
|
|
1371
|
-
const whData = await whRes.json();
|
|
1372
|
-
const crmData = await crmRes.json();
|
|
1373
|
-
|
|
1374
|
-
const wh = whData.webhooks || [];
|
|
1375
|
-
const wtb = document.getElementById('webhooksTable');
|
|
1376
|
-
if (wh.length === 0) {
|
|
1377
|
-
wtb.innerHTML = '<tr><td colspan="6" class="empty-state">No webhooks</td></tr>';
|
|
1378
|
-
} else {
|
|
1379
|
-
wtb.innerHTML = wh.map(w => {
|
|
1380
|
-
let events = [];
|
|
1381
|
-
try { events = JSON.parse(w.events || '[]'); } catch {}
|
|
1382
|
-
return '<tr>' +
|
|
1383
|
-
'<td><strong style="color:var(--text-primary)">' + esc(w.name) + '</strong></td>' +
|
|
1384
|
-
'<td style="font-family:var(--font-mono);font-size:0.8rem;">' + esc(w.url) + '</td>' +
|
|
1385
|
-
'<td>' + events.map(e => '<span class="badge badge-free" style="margin:1px;">' + esc(e) + '</span>').join(' ') + '</td>' +
|
|
1386
|
-
'<td><span class="badge ' + (w.active ? 'badge-active' : 'badge-inactive') + '">' + (w.active ? 'Active' : 'Inactive') + '</span></td>' +
|
|
1387
|
-
'<td>' + (w.failure_count || 0) + '</td>' +
|
|
1388
|
-
'<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>' +
|
|
1389
|
-
'</tr>';
|
|
1390
|
-
}).join('');
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
const crm = crmData.integrations || [];
|
|
1394
|
-
const ctb = document.getElementById('crmTable');
|
|
1395
|
-
if (crm.length === 0) {
|
|
1396
|
-
ctb.innerHTML = '<tr><td colspan="4" class="empty-state">No CRM integrations</td></tr>';
|
|
1397
|
-
} else {
|
|
1398
|
-
ctb.innerHTML = crm.map(c => '<tr>' +
|
|
1399
|
-
'<td><strong style="color:var(--text-primary);text-transform:capitalize;">' + esc(c.provider) + '</strong></td>' +
|
|
1400
|
-
'<td><span class="badge ' + (c.active ? 'badge-active' : 'badge-inactive') + '">' + (c.active ? 'Active' : 'Inactive') + '</span></td>' +
|
|
1401
|
-
'<td>' + fmtDate(c.last_sync) + '</td>' +
|
|
1402
|
-
'<td><button class="btn btn-sm btn-danger" onclick="deleteCrm(\'' + c.id + '\')">Delete</button></td>' +
|
|
1403
|
-
'</tr>').join('');
|
|
1404
|
-
}
|
|
1405
|
-
} catch (err) { console.error('loadWebhooksView:', err); }
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
async function createWebhook() {
|
|
1409
|
-
const sid = getSiteId();
|
|
1410
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1411
|
-
const errEl = document.getElementById('createWebhookError');
|
|
1412
|
-
errEl.style.display = 'none';
|
|
1413
|
-
const name = document.getElementById('whName').value.trim();
|
|
1414
|
-
const url = document.getElementById('whUrl').value.trim();
|
|
1415
|
-
const secret = document.getElementById('whSecret').value.trim();
|
|
1416
|
-
const events = [...document.querySelectorAll('#whEvents input:checked')].map(c => c.value);
|
|
1417
|
-
if (!name || !url) { errEl.textContent = 'Name and URL are required'; errEl.style.display = 'block'; return; }
|
|
1418
|
-
if (events.length === 0) { errEl.textContent = 'Select at least one event'; errEl.style.display = 'block'; return; }
|
|
1419
|
-
const res = await fetch(PAPI + '/webhooks/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ name, url, events, secret: secret || undefined }) });
|
|
1420
|
-
if (res.ok) {
|
|
1421
|
-
closeModal('createWebhookModal');
|
|
1422
|
-
document.getElementById('whName').value = '';
|
|
1423
|
-
document.getElementById('whUrl').value = '';
|
|
1424
|
-
document.getElementById('whSecret').value = '';
|
|
1425
|
-
loadWebhooksView();
|
|
1426
|
-
} else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
async function deleteWebhook(whId) {
|
|
1430
|
-
const sid = getSiteId();
|
|
1431
|
-
if (!sid || !confirm('Delete this webhook?')) return;
|
|
1432
|
-
await fetch(PAPI + '/webhooks/' + sid + '/' + whId, { method: 'DELETE', headers: headers() });
|
|
1433
|
-
loadWebhooksView();
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
async function viewWebhookLogs(whId) {
|
|
1437
|
-
const sid = getSiteId();
|
|
1438
|
-
if (!sid) return;
|
|
1439
|
-
const res = await fetch(PAPI + '/webhooks/' + sid + '/' + whId + '/logs?limit=20', { headers: headers() });
|
|
1440
|
-
const data = await res.json();
|
|
1441
|
-
const logs = data.logs || [];
|
|
1442
|
-
document.getElementById('webhookLogsSection').style.display = 'block';
|
|
1443
|
-
const tbody = document.getElementById('webhookLogsTable');
|
|
1444
|
-
if (logs.length === 0) {
|
|
1445
|
-
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No logs</td></tr>';
|
|
1446
|
-
} else {
|
|
1447
|
-
tbody.innerHTML = logs.map(l => '<tr>' +
|
|
1448
|
-
'<td>' + esc(l.event_type) + '</td>' +
|
|
1449
|
-
'<td><span class="badge ' + (l.response_code < 400 ? 'badge-active' : 'badge-inactive') + '">' + l.response_code + '</span></td>' +
|
|
1450
|
-
'<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>' +
|
|
1451
|
-
'<td>' + fmtDate(l.created_at) + '</td></tr>').join('');
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
async function testWebhook() {
|
|
1456
|
-
const sid = getSiteId();
|
|
1457
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1458
|
-
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() } }) });
|
|
1459
|
-
const data = await res.json();
|
|
1460
|
-
const results = data.results || [];
|
|
1461
|
-
alert('Test sent to ' + results.length + ' webhook(s). Success: ' + results.filter(r => r.success).length);
|
|
1462
|
-
loadWebhooksView();
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
async function createCrmIntegration() {
|
|
1466
|
-
const sid = getSiteId();
|
|
1467
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1468
|
-
const errEl = document.getElementById('createCrmError');
|
|
1469
|
-
errEl.style.display = 'none';
|
|
1470
|
-
const provider = document.getElementById('crmProvider').value;
|
|
1471
|
-
let config;
|
|
1472
|
-
try { config = JSON.parse(document.getElementById('crmConfig').value || '{}'); } catch { errEl.textContent = 'Config must be valid JSON'; errEl.style.display = 'block'; return; }
|
|
1473
|
-
const res = await fetch(PAPI + '/crm/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ provider, config }) });
|
|
1474
|
-
if (res.ok) { closeModal('createCrmModal'); document.getElementById('crmConfig').value = ''; loadWebhooksView(); }
|
|
1475
|
-
else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
async function deleteCrm(intId) {
|
|
1479
|
-
const sid = getSiteId();
|
|
1480
|
-
if (!sid || !confirm('Delete this integration?')) return;
|
|
1481
|
-
await fetch(PAPI + '/crm/' + sid + '/' + intId, { method: 'DELETE', headers: headers() });
|
|
1482
|
-
loadWebhooksView();
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
// ─── Team Management ─────────────────────────────────────────────────
|
|
1486
|
-
async function loadTeam() {
|
|
1487
|
-
try {
|
|
1488
|
-
const res = await fetch(PAPI + '/team', { headers: headers() });
|
|
1489
|
-
const data = await res.json();
|
|
1490
|
-
const members = data.subUsers || [];
|
|
1491
|
-
const tbody = document.getElementById('teamTable');
|
|
1492
|
-
if (members.length === 0) {
|
|
1493
|
-
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No team members. Invite someone to get started.</td></tr>';
|
|
1494
|
-
} else {
|
|
1495
|
-
tbody.innerHTML = members.map(m => {
|
|
1496
|
-
let access = [];
|
|
1497
|
-
try { access = JSON.parse(m.site_access || '[]'); } catch {}
|
|
1498
|
-
return '<tr>' +
|
|
1499
|
-
'<td>' + esc(m.email) + '</td>' +
|
|
1500
|
-
'<td><strong style="color:var(--text-primary)">' + esc(m.name) + '</strong></td>' +
|
|
1501
|
-
'<td><span class="badge role-' + (m.role || 'viewer') + '">' + esc(m.role) + '</span></td>' +
|
|
1502
|
-
'<td style="font-size:0.82rem;">' + (access.includes('*') ? 'All' : access.length + ' site(s)') + '</td>' +
|
|
1503
|
-
'<td>' + (m.quota_actions_month !== null ? m.quota_actions_month : '∞') + '</td>' +
|
|
1504
|
-
'<td>' + (m.actions_used_month || 0) + '</td>' +
|
|
1505
|
-
'<td><button class="btn btn-sm btn-danger" onclick="deleteTeamMember(\'' + m.id + '\')">Remove</button></td>' +
|
|
1506
|
-
'</tr>';
|
|
1507
|
-
}).join('');
|
|
1508
|
-
}
|
|
1509
|
-
} catch (err) { console.error('loadTeam:', err); }
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
async function inviteTeamMember() {
|
|
1513
|
-
const errEl = document.getElementById('inviteTeamError');
|
|
1514
|
-
errEl.style.display = 'none';
|
|
1515
|
-
const email = document.getElementById('teamEmail').value.trim();
|
|
1516
|
-
const name = document.getElementById('teamName').value.trim();
|
|
1517
|
-
const password = document.getElementById('teamPassword').value;
|
|
1518
|
-
const role = document.getElementById('teamRole').value;
|
|
1519
|
-
const quota = document.getElementById('teamQuota').value;
|
|
1520
|
-
const siteAccess = [...document.querySelectorAll('#teamSiteAccess input:checked')].map(c => c.value);
|
|
1521
|
-
if (!email || !name || !password) { errEl.textContent = 'Email, name, and password are required'; errEl.style.display = 'block'; return; }
|
|
1522
|
-
const res = await fetch(PAPI + '/team', { method: 'POST', headers: headers(), body: JSON.stringify({ email, name, password, role, siteAccess, quotaActionsMonth: quota ? parseInt(quota) : undefined }) });
|
|
1523
|
-
if (res.ok) {
|
|
1524
|
-
closeModal('inviteTeamModal');
|
|
1525
|
-
document.getElementById('teamEmail').value = '';
|
|
1526
|
-
document.getElementById('teamName').value = '';
|
|
1527
|
-
document.getElementById('teamPassword').value = '';
|
|
1528
|
-
document.getElementById('teamQuota').value = '';
|
|
1529
|
-
loadTeam();
|
|
1530
|
-
} else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
async function deleteTeamMember(subId) {
|
|
1534
|
-
if (!confirm('Remove this team member?')) return;
|
|
1535
|
-
await fetch(PAPI + '/team/' + subId, { method: 'DELETE', headers: headers() });
|
|
1536
|
-
loadTeam();
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
// ─── Support ─────────────────────────────────────────────────────────
|
|
1540
|
-
async function loadSupport() {
|
|
1541
|
-
try {
|
|
1542
|
-
const [statsRes, ticketsRes] = await Promise.all([
|
|
1543
|
-
fetch(PAPI + '/support/stats', { headers: headers() }),
|
|
1544
|
-
fetch(PAPI + '/support/tickets?limit=50', { headers: headers() })
|
|
1545
|
-
]);
|
|
1546
|
-
const statsData = await statsRes.json();
|
|
1547
|
-
const ticketsData = await ticketsRes.json();
|
|
1548
|
-
|
|
1549
|
-
const st = statsData.stats || {};
|
|
1550
|
-
document.getElementById('supOpen').textContent = st.open || 0;
|
|
1551
|
-
document.getElementById('supProgress').textContent = st.in_progress || 0;
|
|
1552
|
-
document.getElementById('supWaiting').textContent = st.waiting || 0;
|
|
1553
|
-
document.getElementById('supResolved').textContent = (st.resolved || 0) + (st.closed || 0);
|
|
1554
|
-
|
|
1555
|
-
const tickets = ticketsData.tickets || [];
|
|
1556
|
-
const tbody = document.getElementById('ticketsTable');
|
|
1557
|
-
if (tickets.length === 0) {
|
|
1558
|
-
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No tickets</td></tr>';
|
|
1559
|
-
} else {
|
|
1560
|
-
tbody.innerHTML = tickets.map(t => '<tr>' +
|
|
1561
|
-
'<td><strong style="color:var(--text-primary)">' + esc(t.subject) + '</strong></td>' +
|
|
1562
|
-
'<td><span class="badge priority-' + (t.priority || 'normal') + '">' + esc(t.priority) + '</span></td>' +
|
|
1563
|
-
'<td><span class="badge status-' + (t.status || 'open') + '">' + esc(t.status) + '</span></td>' +
|
|
1564
|
-
'<td>' + fmtDate(t.created_at) + '</td>' +
|
|
1565
|
-
'<td><button class="btn btn-sm btn-secondary" onclick="showTicketDetail(\'' + t.id + '\')">View</button></td>' +
|
|
1566
|
-
'</tr>').join('');
|
|
1567
|
-
}
|
|
1568
|
-
} catch (err) { console.error('loadSupport:', err); }
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
async function createTicket() {
|
|
1572
|
-
const errEl = document.getElementById('createTicketError');
|
|
1573
|
-
errEl.style.display = 'none';
|
|
1574
|
-
const subject = document.getElementById('ticketSubject').value.trim();
|
|
1575
|
-
const priority = document.getElementById('ticketPriority').value;
|
|
1576
|
-
const category = document.getElementById('ticketCategory').value;
|
|
1577
|
-
if (!subject) { errEl.textContent = 'Subject is required'; errEl.style.display = 'block'; return; }
|
|
1578
|
-
const res = await fetch(PAPI + '/support/tickets', { method: 'POST', headers: headers(), body: JSON.stringify({ subject, priority, category }) });
|
|
1579
|
-
if (res.ok) {
|
|
1580
|
-
closeModal('createTicketModal');
|
|
1581
|
-
document.getElementById('ticketSubject').value = '';
|
|
1582
|
-
loadSupport();
|
|
1583
|
-
} else { const d = await res.json(); errEl.textContent = d.error || 'Failed'; errEl.style.display = 'block'; }
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
async function showTicketDetail(ticketId) {
|
|
1587
|
-
currentTicketId = ticketId;
|
|
1588
|
-
try {
|
|
1589
|
-
const res = await fetch(PAPI + '/support/tickets/' + ticketId, { headers: headers() });
|
|
1590
|
-
const data = await res.json();
|
|
1591
|
-
const t = data.ticket;
|
|
1592
|
-
const msgs = data.messages || [];
|
|
1593
|
-
document.getElementById('ticketDetailTitle').textContent = t.subject;
|
|
1594
|
-
document.getElementById('ticketStatusSelect').value = t.status || 'open';
|
|
1595
|
-
document.getElementById('ticketMessages').innerHTML = msgs.map(m => '<div class="msg-item msg-' + (m.sender_type || 'user') + '">' +
|
|
1596
|
-
'<div class="msg-meta"><strong>' + (m.sender_type === 'bot' ? '🤖 Bot' : '👤 You') + '</strong><span>' + fmtDate(m.created_at) + '</span></div>' +
|
|
1597
|
-
'<div class="msg-text">' + esc(m.message) + '</div></div>').join('');
|
|
1598
|
-
openModal('ticketDetailModal');
|
|
1599
|
-
const msgList = document.getElementById('ticketMessages');
|
|
1600
|
-
msgList.scrollTop = msgList.scrollHeight;
|
|
1601
|
-
} catch (err) { console.error('showTicketDetail:', err); }
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
async function updateTicketStatus() {
|
|
1605
|
-
if (!currentTicketId) return;
|
|
1606
|
-
const status = document.getElementById('ticketStatusSelect').value;
|
|
1607
|
-
await fetch(PAPI + '/support/tickets/' + currentTicketId + '/status', { method: 'PUT', headers: headers(), body: JSON.stringify({ status }) });
|
|
1608
|
-
loadSupport();
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
async function sendTicketReply() {
|
|
1612
|
-
if (!currentTicketId) return;
|
|
1613
|
-
const msg = document.getElementById('ticketReply').value.trim();
|
|
1614
|
-
if (!msg) return;
|
|
1615
|
-
await fetch(PAPI + '/support/tickets/' + currentTicketId + '/messages', { method: 'POST', headers: headers(), body: JSON.stringify({ message: msg }) });
|
|
1616
|
-
document.getElementById('ticketReply').value = '';
|
|
1617
|
-
showTicketDetail(currentTicketId);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
// ─── Script Builder ──────────────────────────────────────────────────
|
|
1621
|
-
let availablePlugins = [];
|
|
1622
|
-
|
|
1623
|
-
async function loadScriptPlugins() {
|
|
1624
|
-
try {
|
|
1625
|
-
const res = await fetch(PAPI + '/script/plugins', { headers: headers() });
|
|
1626
|
-
const data = await res.json();
|
|
1627
|
-
availablePlugins = data.plugins || [];
|
|
1628
|
-
const container = document.getElementById('pluginsList');
|
|
1629
|
-
container.innerHTML = availablePlugins.map(p => '<div class="toggle-wrap">' +
|
|
1630
|
-
'<div class="toggle-label">' + esc(p.name) + '<small>' + esc(p.description) + '</small></div>' +
|
|
1631
|
-
'<label class="toggle"><input type="checkbox" data-plugin="' + esc(p.id) + '"><span class="toggle-slider"></span></label>' +
|
|
1632
|
-
'</div>').join('');
|
|
1633
|
-
} catch (err) { console.error('loadScriptPlugins:', err); }
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
async function loadScriptConfig() {
|
|
1637
|
-
const sid = getSiteId();
|
|
1638
|
-
if (!sid) return;
|
|
1639
|
-
try {
|
|
1640
|
-
const res = await fetch(PAPI + '/script/' + sid + '/config', { headers: headers() });
|
|
1641
|
-
const data = await res.json();
|
|
1642
|
-
const c = data.config;
|
|
1643
|
-
if (c) {
|
|
1644
|
-
document.getElementById('scriptMinified').checked = !!c.minified;
|
|
1645
|
-
document.getElementById('scriptAmp').checked = !!c.amp_compatible;
|
|
1646
|
-
document.getElementById('scriptAutoPatch').checked = !!c.auto_patch;
|
|
1647
|
-
document.getElementById('scriptCustomCss').value = c.custom_css || '';
|
|
1648
|
-
document.getElementById('scriptCustomJs').value = c.custom_js || '';
|
|
1649
|
-
let plugins = [];
|
|
1650
|
-
try { plugins = JSON.parse(c.plugins_json || '[]'); } catch {}
|
|
1651
|
-
document.querySelectorAll('#pluginsList input[data-plugin]').forEach(cb => { cb.checked = plugins.includes(cb.dataset.plugin); });
|
|
1652
|
-
}
|
|
1653
|
-
} catch (err) { console.error('loadScriptConfig:', err); }
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
async function saveScriptConfig() {
|
|
1657
|
-
const sid = getSiteId();
|
|
1658
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1659
|
-
const plugins = [...document.querySelectorAll('#pluginsList input[data-plugin]:checked')].map(c => c.dataset.plugin);
|
|
1660
|
-
const body = {
|
|
1661
|
-
plugins,
|
|
1662
|
-
minified: document.getElementById('scriptMinified').checked,
|
|
1663
|
-
ampCompatible: document.getElementById('scriptAmp').checked,
|
|
1664
|
-
autoPatch: document.getElementById('scriptAutoPatch').checked,
|
|
1665
|
-
customCss: document.getElementById('scriptCustomCss').value,
|
|
1666
|
-
customJs: document.getElementById('scriptCustomJs').value
|
|
1667
|
-
};
|
|
1668
|
-
await fetch(PAPI + '/script/' + sid + '/config', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
|
|
1669
|
-
alert('Script config saved');
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
async function buildScript() {
|
|
1673
|
-
const sid = getSiteId();
|
|
1674
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1675
|
-
await saveScriptConfig();
|
|
1676
|
-
const res = await fetch(PAPI + '/script/' + sid + '/build', { method: 'POST', headers: headers() });
|
|
1677
|
-
const data = await res.json();
|
|
1678
|
-
if (data.error) { alert(data.error); return; }
|
|
1679
|
-
document.getElementById('scriptPreviewSection').style.display = 'block';
|
|
1680
|
-
document.getElementById('scriptHash').textContent = 'Hash: ' + (data.hash || '—');
|
|
1681
|
-
document.getElementById('scriptSize').textContent = data.size ? fmtBytes(data.size) : '';
|
|
1682
|
-
document.getElementById('scriptPreview').textContent = data.script || '';
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function copyBuiltScript() {
|
|
1686
|
-
const text = document.getElementById('scriptPreview').textContent;
|
|
1687
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
1688
|
-
const btn = document.querySelector('#scriptPreviewSection .copy-btn');
|
|
1689
|
-
btn.textContent = 'Copied!';
|
|
1690
|
-
setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// ─── Stealth Mode ────────────────────────────────────────────────────
|
|
1695
|
-
async function loadStealth() {
|
|
1696
|
-
const sid = getSiteId();
|
|
1697
|
-
if (!sid) return;
|
|
1698
|
-
try {
|
|
1699
|
-
const res = await fetch(PAPI + '/stealth/' + sid, { headers: headers() });
|
|
1700
|
-
const data = await res.json();
|
|
1701
|
-
const p = data.profile;
|
|
1702
|
-
if (p) {
|
|
1703
|
-
document.getElementById('stTypingMin').value = p.typing_speed_min || 30;
|
|
1704
|
-
document.getElementById('stTypingMinVal').textContent = (p.typing_speed_min || 30) + 'ms';
|
|
1705
|
-
document.getElementById('stTypingMax').value = p.typing_speed_max || 120;
|
|
1706
|
-
document.getElementById('stTypingMaxVal').textContent = (p.typing_speed_max || 120) + 'ms';
|
|
1707
|
-
document.getElementById('stMouseSpeed').value = p.mouse_speed || 'natural';
|
|
1708
|
-
document.getElementById('stScrollBehavior').value = p.scroll_behavior || 'eased';
|
|
1709
|
-
document.getElementById('stClickMin').value = p.click_delay_min || 50;
|
|
1710
|
-
document.getElementById('stClickMinVal').textContent = (p.click_delay_min || 50) + 'ms';
|
|
1711
|
-
document.getElementById('stClickMax').value = p.click_delay_max || 400;
|
|
1712
|
-
document.getElementById('stClickMaxVal').textContent = (p.click_delay_max || 400) + 'ms';
|
|
1713
|
-
let ad = {};
|
|
1714
|
-
try { ad = JSON.parse(p.anti_detection_json || '{}'); } catch {}
|
|
1715
|
-
document.getElementById('stHideWebdriver').checked = !!ad.hideWebdriver;
|
|
1716
|
-
document.getElementById('stSpoofPlugins').checked = !!ad.spoofPlugins;
|
|
1717
|
-
document.getElementById('stSpoofLang').checked = !!ad.spoofLanguages;
|
|
1718
|
-
}
|
|
1719
|
-
} catch (err) { console.error('loadStealth:', err); }
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
async function saveStealth() {
|
|
1723
|
-
const sid = getSiteId();
|
|
1724
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1725
|
-
const body = {
|
|
1726
|
-
typingSpeedMin: parseInt(document.getElementById('stTypingMin').value),
|
|
1727
|
-
typingSpeedMax: parseInt(document.getElementById('stTypingMax').value),
|
|
1728
|
-
mouseSpeed: document.getElementById('stMouseSpeed').value,
|
|
1729
|
-
scrollBehavior: document.getElementById('stScrollBehavior').value,
|
|
1730
|
-
clickDelayMin: parseInt(document.getElementById('stClickMin').value),
|
|
1731
|
-
clickDelayMax: parseInt(document.getElementById('stClickMax').value),
|
|
1732
|
-
antiDetection: {
|
|
1733
|
-
hideWebdriver: document.getElementById('stHideWebdriver').checked,
|
|
1734
|
-
spoofPlugins: document.getElementById('stSpoofPlugins').checked,
|
|
1735
|
-
spoofLanguages: document.getElementById('stSpoofLang').checked
|
|
1736
|
-
}
|
|
1737
|
-
};
|
|
1738
|
-
await fetch(PAPI + '/stealth/' + sid, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
|
|
1739
|
-
alert('Stealth profile saved');
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
async function generateStealthScript() {
|
|
1743
|
-
const sid = getSiteId();
|
|
1744
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1745
|
-
const res = await fetch(PAPI + '/stealth/' + sid + '/script', { headers: headers() });
|
|
1746
|
-
const data = await res.json();
|
|
1747
|
-
document.getElementById('stealthScriptSection').style.display = 'block';
|
|
1748
|
-
document.getElementById('stealthScriptPreview').textContent = data.script || '';
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
function copyStealthScript() {
|
|
1752
|
-
const text = document.getElementById('stealthScriptPreview').textContent;
|
|
1753
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
1754
|
-
const btn = document.querySelector('#stealthScriptSection .copy-btn');
|
|
1755
|
-
btn.textContent = 'Copied!';
|
|
1756
|
-
setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
// ─── CDN ─────────────────────────────────────────────────────────────
|
|
1761
|
-
async function loadCdn() {
|
|
1762
|
-
const sid = getSiteId();
|
|
1763
|
-
if (!sid) return;
|
|
1764
|
-
try {
|
|
1765
|
-
const res = await fetch(PAPI + '/cdn/' + sid, { headers: headers() });
|
|
1766
|
-
const data = await res.json();
|
|
1767
|
-
const c = data.config;
|
|
1768
|
-
if (c) {
|
|
1769
|
-
document.getElementById('cdnDomain').value = c.custom_domain || '';
|
|
1770
|
-
document.getElementById('cdnTtl').value = c.cache_ttl || 86400;
|
|
1771
|
-
document.getElementById('cdnSslBadge').textContent = c.ssl_status || 'Pending';
|
|
1772
|
-
document.getElementById('cdnSslBadge').className = 'badge ' + (c.ssl_status === 'active' ? 'badge-active' : 'badge-free');
|
|
1773
|
-
let edges = [];
|
|
1774
|
-
try { edges = JSON.parse(c.edge_locations || '[]'); } catch {}
|
|
1775
|
-
document.querySelectorAll('#cdnEdges input').forEach(cb => { cb.checked = edges.includes(cb.value); });
|
|
1776
|
-
document.getElementById('cdnStatReqs').textContent = c.requests_count || 0;
|
|
1777
|
-
document.getElementById('cdnStatBw').textContent = fmtBytes(c.bandwidth_used || 0);
|
|
1778
|
-
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';
|
|
1779
|
-
document.getElementById('cdnUrlDisplay').textContent = cdnUrl;
|
|
1780
|
-
|
|
1781
|
-
try {
|
|
1782
|
-
const statsRes = await fetch(PAPI + '/cdn/' + sid + '/stats?days=30', { headers: headers() });
|
|
1783
|
-
if (statsRes.ok) {
|
|
1784
|
-
const statsData = await statsRes.json();
|
|
1785
|
-
const totals = (statsData.stats || {}).totals || {};
|
|
1786
|
-
document.getElementById('cdnStatReqs').textContent = totals.requests || c.requests_count || 0;
|
|
1787
|
-
document.getElementById('cdnStatBw').textContent = fmtBytes(totals.bandwidth || c.bandwidth_used || 0);
|
|
1788
|
-
document.getElementById('cdnStatHits').textContent = totals.cache_hits || 0;
|
|
1789
|
-
}
|
|
1790
|
-
} catch {}
|
|
1791
|
-
} else {
|
|
1792
|
-
document.getElementById('cdnUrlDisplay').textContent = 'https://cdn.webagentbridge.com/bridge/' + sid + '/ai-agent-bridge.js';
|
|
1793
|
-
}
|
|
1794
|
-
} catch (err) { console.error('loadCdn:', err); }
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
async function saveCdn() {
|
|
1798
|
-
const sid = getSiteId();
|
|
1799
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1800
|
-
const edgeLocations = [...document.querySelectorAll('#cdnEdges input:checked')].map(c => c.value);
|
|
1801
|
-
const body = {
|
|
1802
|
-
customDomain: document.getElementById('cdnDomain').value.trim() || null,
|
|
1803
|
-
cacheTtl: parseInt(document.getElementById('cdnTtl').value) || 86400,
|
|
1804
|
-
edgeLocations
|
|
1805
|
-
};
|
|
1806
|
-
await fetch(PAPI + '/cdn/' + sid, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
|
|
1807
|
-
alert('CDN config saved');
|
|
1808
|
-
loadCdn();
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
function copyCdnUrl() {
|
|
1812
|
-
const text = document.getElementById('cdnUrlDisplay').textContent;
|
|
1813
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
1814
|
-
const btn = document.querySelector('#view-cdn .copy-btn');
|
|
1815
|
-
btn.textContent = 'Copied!';
|
|
1816
|
-
setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
// ─── Audit & Compliance ──────────────────────────────────────────────
|
|
1821
|
-
async function loadAudit() {
|
|
1822
|
-
const sid = getSiteId();
|
|
1823
|
-
if (!sid) return;
|
|
1824
|
-
auditPage = 0;
|
|
1825
|
-
try {
|
|
1826
|
-
const res = await fetch(PAPI + '/audit/' + sid + '/compliance', { headers: headers() });
|
|
1827
|
-
const data = await res.json();
|
|
1828
|
-
const s = data.settings;
|
|
1829
|
-
if (s) {
|
|
1830
|
-
document.getElementById('auditRetention').value = s.retention_days || 90;
|
|
1831
|
-
document.getElementById('auditHipaa').checked = !!s.hipaa_mode;
|
|
1832
|
-
document.getElementById('auditGdpr').checked = !!s.gdpr_mode;
|
|
1833
|
-
document.getElementById('auditSoc2').checked = !!s.soc2_mode;
|
|
1834
|
-
document.getElementById('auditAutoPurge').checked = s.auto_purge !== undefined ? !!s.auto_purge : true;
|
|
1835
|
-
}
|
|
1836
|
-
} catch (err) { console.error('loadAudit:', err); }
|
|
1837
|
-
loadAuditLogs();
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
async function loadAuditLogs() {
|
|
1841
|
-
const sid = getSiteId();
|
|
1842
|
-
if (!sid) return;
|
|
1843
|
-
const action = document.getElementById('auditActionFilter').value;
|
|
1844
|
-
let url = PAPI + '/audit/' + sid + '/logs?limit=' + AUDIT_LIMIT + '&offset=' + (auditPage * AUDIT_LIMIT);
|
|
1845
|
-
if (action) url += '&action=' + encodeURIComponent(action);
|
|
1846
|
-
try {
|
|
1847
|
-
const res = await fetch(url, { headers: headers() });
|
|
1848
|
-
const data = await res.json();
|
|
1849
|
-
const rows = data.rows || [];
|
|
1850
|
-
const total = data.total || 0;
|
|
1851
|
-
const tbody = document.getElementById('auditLogsTable');
|
|
1852
|
-
if (rows.length === 0) {
|
|
1853
|
-
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No audit logs</td></tr>';
|
|
1854
|
-
} else {
|
|
1855
|
-
tbody.innerHTML = rows.map(r => '<tr>' +
|
|
1856
|
-
'<td style="font-size:0.82rem;">' + fmtDate(r.created_at) + '</td>' +
|
|
1857
|
-
'<td>' + esc(r.action) + '</td>' +
|
|
1858
|
-
'<td style="font-size:0.82rem;">' + esc(r.resource_type || '—') + (r.resource_id ? '/' + esc(r.resource_id).substring(0, 8) : '') + '</td>' +
|
|
1859
|
-
'<td style="font-size:0.82rem;">' + esc(r.user_id || '—').substring(0, 8) + '</td>' +
|
|
1860
|
-
'<td style="font-size:0.82rem;font-family:var(--font-mono);">' + esc(r.ip_address || '—') + '</td>' +
|
|
1861
|
-
'</tr>').join('');
|
|
1862
|
-
}
|
|
1863
|
-
const totalPages = Math.ceil(total / AUDIT_LIMIT);
|
|
1864
|
-
const pag = document.getElementById('auditPagination');
|
|
1865
|
-
pag.innerHTML = '<span style="font-size:0.85rem;color:var(--text-muted);">Page ' + (auditPage + 1) + ' of ' + Math.max(totalPages, 1) + ' (' + total + ' total)</span>' +
|
|
1866
|
-
'<button class="btn btn-sm btn-secondary" onclick="auditPrev()" ' + (auditPage <= 0 ? 'disabled' : '') + '>← Prev</button>' +
|
|
1867
|
-
'<button class="btn btn-sm btn-secondary" onclick="auditNext()" ' + (auditPage >= totalPages - 1 ? 'disabled' : '') + '>Next →</button>';
|
|
1868
|
-
} catch (err) { console.error('loadAuditLogs:', err); }
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
function auditPrev() { if (auditPage > 0) { auditPage--; loadAuditLogs(); } }
|
|
1872
|
-
function auditNext() { auditPage++; loadAuditLogs(); }
|
|
1873
|
-
|
|
1874
|
-
async function saveCompliance() {
|
|
1875
|
-
const sid = getSiteId();
|
|
1876
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1877
|
-
const body = {
|
|
1878
|
-
retentionDays: parseInt(document.getElementById('auditRetention').value) || 90,
|
|
1879
|
-
hipaaMode: document.getElementById('auditHipaa').checked,
|
|
1880
|
-
gdprMode: document.getElementById('auditGdpr').checked,
|
|
1881
|
-
soc2Mode: document.getElementById('auditSoc2').checked,
|
|
1882
|
-
autoPurge: document.getElementById('auditAutoPurge').checked
|
|
1883
|
-
};
|
|
1884
|
-
await fetch(PAPI + '/audit/' + sid + '/compliance', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
|
|
1885
|
-
alert('Compliance settings saved');
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
async function exportAuditLogs(format) {
|
|
1889
|
-
const sid = getSiteId();
|
|
1890
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1891
|
-
let url = PAPI + '/audit/' + sid + '/export?format=' + format;
|
|
1892
|
-
const since = document.getElementById('auditSince').value;
|
|
1893
|
-
const until = document.getElementById('auditUntil').value;
|
|
1894
|
-
if (since) url += '&since=' + since + 'T00:00:00.000Z';
|
|
1895
|
-
if (until) url += '&until=' + until + 'T23:59:59.999Z';
|
|
1896
|
-
try {
|
|
1897
|
-
const res = await fetch(url, { headers: headers() });
|
|
1898
|
-
const blob = await res.blob();
|
|
1899
|
-
const disposition = res.headers.get('Content-Disposition') || '';
|
|
1900
|
-
const match = disposition.match(/filename="?([^"]+)"?/);
|
|
1901
|
-
const filename = match ? match[1] : 'audit-logs.' + format;
|
|
1902
|
-
const a = document.createElement('a');
|
|
1903
|
-
a.href = URL.createObjectURL(blob);
|
|
1904
|
-
a.download = filename;
|
|
1905
|
-
a.click();
|
|
1906
|
-
URL.revokeObjectURL(a.href);
|
|
1907
|
-
} catch (err) { alert('Export failed'); }
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
async function purgeAuditLogs() {
|
|
1911
|
-
const sid = getSiteId();
|
|
1912
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1913
|
-
if (!confirm('Purge old audit logs beyond retention period? This cannot be undone.')) return;
|
|
1914
|
-
const res = await fetch(PAPI + '/audit/' + sid + '/purge', { method: 'POST', headers: headers() });
|
|
1915
|
-
const data = await res.json();
|
|
1916
|
-
alert('Purged ' + (data.deleted || 0) + ' old log entries.');
|
|
1917
|
-
loadAuditLogs();
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
// ─── Sandbox ─────────────────────────────────────────────────────────
|
|
1921
|
-
async function loadSandboxes() {
|
|
1922
|
-
const sid = getSiteId();
|
|
1923
|
-
if (!sid) return;
|
|
1924
|
-
try {
|
|
1925
|
-
const res = await fetch(PAPI + '/sandbox/' + sid, { headers: headers() });
|
|
1926
|
-
const data = await res.json();
|
|
1927
|
-
const sandboxes = data.sandboxes || [];
|
|
1928
|
-
const tbody = document.getElementById('sandboxTable');
|
|
1929
|
-
if (sandboxes.length === 0) {
|
|
1930
|
-
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No sandboxes. Create one to get started.</td></tr>';
|
|
1931
|
-
document.getElementById('sandboxDetail').style.display = 'none';
|
|
1932
|
-
} else {
|
|
1933
|
-
tbody.innerHTML = sandboxes.map(s => '<tr>' +
|
|
1934
|
-
'<td><strong style="color:var(--text-primary)">' + esc(s.name) + '</strong></td>' +
|
|
1935
|
-
'<td><span class="badge badge-active">Active</span></td>' +
|
|
1936
|
-
'<td>' + (s.traffic_generated || 0) + '</td>' +
|
|
1937
|
-
'<td>' + fmtDate(s.created_at) + '</td>' +
|
|
1938
|
-
'<td style="display:flex;gap:4px;">' +
|
|
1939
|
-
'<button class="btn btn-sm btn-secondary" onclick="selectSandbox(\'' + s.id + '\')">Select</button>' +
|
|
1940
|
-
'<button class="btn btn-sm btn-danger" onclick="deleteSandbox(\'' + s.id + '\')">Delete</button></td>' +
|
|
1941
|
-
'</tr>').join('');
|
|
1942
|
-
if (!currentSandboxId && sandboxes.length > 0) selectSandbox(sandboxes[0].id);
|
|
1943
|
-
}
|
|
1944
|
-
} catch (err) { console.error('loadSandboxes:', err); }
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
async function createSandbox() {
|
|
1948
|
-
const sid = getSiteId();
|
|
1949
|
-
if (!sid) { alert('Select a site first'); return; }
|
|
1950
|
-
const name = prompt('Sandbox name:');
|
|
1951
|
-
if (!name) return;
|
|
1952
|
-
await fetch(PAPI + '/sandbox/' + sid, { method: 'POST', headers: headers(), body: JSON.stringify({ name }) });
|
|
1953
|
-
loadSandboxes();
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
async function deleteSandbox(sandboxId) {
|
|
1957
|
-
const sid = getSiteId();
|
|
1958
|
-
if (!sid || !confirm('Delete this sandbox?')) return;
|
|
1959
|
-
await fetch(PAPI + '/sandbox/' + sid + '/' + sandboxId, { method: 'DELETE', headers: headers() });
|
|
1960
|
-
if (currentSandboxId === sandboxId) currentSandboxId = null;
|
|
1961
|
-
loadSandboxes();
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
function selectSandbox(sandboxId) {
|
|
1965
|
-
currentSandboxId = sandboxId;
|
|
1966
|
-
document.getElementById('sandboxDetail').style.display = 'block';
|
|
1967
|
-
loadBenchmarks();
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
async function simulateTraffic() {
|
|
1971
|
-
const sid = getSiteId();
|
|
1972
|
-
if (!sid || !currentSandboxId) { alert('Select a site and sandbox first'); return; }
|
|
1973
|
-
const body = {
|
|
1974
|
-
agentCount: parseInt(document.getElementById('simAgentCount').value) || 10,
|
|
1975
|
-
duration: parseInt(document.getElementById('simDuration').value) || 60,
|
|
1976
|
-
actionsPerAgent: parseInt(document.getElementById('simActions').value) || 5
|
|
1977
|
-
};
|
|
1978
|
-
const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/simulate', { method: 'POST', headers: headers(), body: JSON.stringify(body) });
|
|
1979
|
-
const data = await res.json();
|
|
1980
|
-
const r = data.result || {};
|
|
1981
|
-
document.getElementById('simResults').style.display = 'block';
|
|
1982
|
-
document.getElementById('simResultsContent').innerHTML =
|
|
1983
|
-
'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;">' +
|
|
1984
|
-
'<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>' +
|
|
1985
|
-
'<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>' +
|
|
1986
|
-
'<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>' +
|
|
1987
|
-
'</div>' +
|
|
1988
|
-
(r.typeDist ? '<div style="margin-top:16px;"><h4 style="margin-bottom:8px;">Agent Distribution</h4><div style="display:flex;gap:8px;flex-wrap:wrap;">' +
|
|
1989
|
-
Object.entries(r.typeDist).map(([k, v]) => '<span class="badge type-' + k + '">' + k + ': ' + v + '</span>').join('') + '</div></div>' : '');
|
|
1990
|
-
loadSandboxes();
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
async function runBenchmark() {
|
|
1994
|
-
const sid = getSiteId();
|
|
1995
|
-
if (!sid || !currentSandboxId) { alert('Select a site and sandbox first'); return; }
|
|
1996
|
-
const benchmarkType = document.getElementById('benchType').value;
|
|
1997
|
-
const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/benchmark', { method: 'POST', headers: headers(), body: JSON.stringify({ benchmarkType }) });
|
|
1998
|
-
const data = await res.json();
|
|
1999
|
-
const r = data.result || {};
|
|
2000
|
-
alert('Benchmark complete: ' + r.benchmarkType + ' — Before: ' + r.beforeValue + ' ' + (r.unit || '') + ', After: ' + r.afterValue + ' ' + (r.unit || '') + ' (' + r.improvement + '% improvement)');
|
|
2001
|
-
loadBenchmarks();
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
async function loadBenchmarks() {
|
|
2005
|
-
const sid = getSiteId();
|
|
2006
|
-
if (!sid || !currentSandboxId) return;
|
|
2007
|
-
try {
|
|
2008
|
-
const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/benchmarks', { headers: headers() });
|
|
2009
|
-
const data = await res.json();
|
|
2010
|
-
const benchmarks = data.benchmarks || [];
|
|
2011
|
-
const tbody = document.getElementById('benchmarksTable');
|
|
2012
|
-
if (benchmarks.length === 0) {
|
|
2013
|
-
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No benchmarks yet</td></tr>';
|
|
2014
|
-
} else {
|
|
2015
|
-
tbody.innerHTML = benchmarks.map(b => {
|
|
2016
|
-
const imp = b.before_value > 0 ? (((b.before_value - b.after_value) / b.before_value) * 100).toFixed(1) : '—';
|
|
2017
|
-
return '<tr>' +
|
|
2018
|
-
'<td>' + esc(b.benchmark_type) + '</td>' +
|
|
2019
|
-
'<td>' + (Math.round(b.before_value * 100) / 100) + '</td>' +
|
|
2020
|
-
'<td>' + (Math.round(b.after_value * 100) / 100) + '</td>' +
|
|
2021
|
-
'<td><span style="color:' + (parseFloat(imp) > 0 ? 'var(--accent-green)' : 'var(--accent-red)') + ';">' + imp + '%</span></td>' +
|
|
2022
|
-
'<td>' + fmtDate(b.recorded_at) + '</td></tr>';
|
|
2023
|
-
}).join('');
|
|
2024
|
-
}
|
|
2025
|
-
} catch (err) { console.error('loadBenchmarks:', err); }
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
async function compareBenchmarks() {
|
|
2029
|
-
const sid = getSiteId();
|
|
2030
|
-
if (!sid || !currentSandboxId) { alert('Select a sandbox first'); return; }
|
|
2031
|
-
try {
|
|
2032
|
-
const res = await fetch(PAPI + '/sandbox/' + sid + '/' + currentSandboxId + '/benchmarks', { headers: headers() });
|
|
2033
|
-
const data = await res.json();
|
|
2034
|
-
const benchmarks = data.benchmarks || [];
|
|
2035
|
-
const types = ['rate_limit', 'response_time', 'throughput'];
|
|
2036
|
-
let html = '';
|
|
2037
|
-
for (const type of types) {
|
|
2038
|
-
const typeData = benchmarks.filter(b => b.benchmark_type === type);
|
|
2039
|
-
if (typeData.length >= 2) {
|
|
2040
|
-
const latest = typeData[0];
|
|
2041
|
-
const prev = typeData[1];
|
|
2042
|
-
const delta = latest.after_value - prev.after_value;
|
|
2043
|
-
const improved = type === 'throughput' ? delta > 0 : delta < 0;
|
|
2044
|
-
html += '<div style="margin-bottom:16px;padding:12px;border-radius:var(--radius-md);background:var(--bg-surface);">' +
|
|
2045
|
-
'<strong>' + type.replace(/_/g, ' ') + '</strong>' +
|
|
2046
|
-
'<div style="display:flex;gap:20px;margin-top:8px;font-size:0.9rem;color:var(--text-secondary);">' +
|
|
2047
|
-
'<span>Latest: ' + (Math.round(latest.after_value * 100) / 100) + '</span>' +
|
|
2048
|
-
'<span>Previous: ' + (Math.round(prev.after_value * 100) / 100) + '</span>' +
|
|
2049
|
-
'<span style="color:' + (improved ? 'var(--accent-green)' : 'var(--accent-red)') + ';">' + (improved ? '↑ Improved' : '↓ Regressed') + '</span></div></div>';
|
|
2050
|
-
} else if (typeData.length === 1) {
|
|
2051
|
-
html += '<div style="margin-bottom:16px;padding:12px;border-radius:var(--radius-md);background:var(--bg-surface);">' +
|
|
2052
|
-
'<strong>' + type.replace(/_/g, ' ') + '</strong>' +
|
|
2053
|
-
'<div style="font-size:0.9rem;color:var(--text-muted);margin-top:8px;">Only 1 benchmark — need at least 2 to compare</div></div>';
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
if (!html) html = '<p style="color:var(--text-muted);">No benchmarks to compare</p>';
|
|
2057
|
-
document.getElementById('benchCompare').style.display = 'block';
|
|
2058
|
-
document.getElementById('benchCompareContent').innerHTML = html;
|
|
2059
|
-
} catch (err) { console.error('compareBenchmarks:', err); }
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
// ─── Init ────────────────────────────────────────────────────────────
|
|
2063
|
-
async function init() {
|
|
2064
|
-
if (user) {
|
|
2065
|
-
document.getElementById('userName').textContent = user.name || user.email;
|
|
2066
|
-
}
|
|
2067
|
-
await loadSites();
|
|
2068
|
-
loadOverview();
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
init();
|
|
2072
|
-
</script>
|
|
2073
|
-
<script src="/js/cookie-consent.js"></script>
|
|
2074
|
-
</body>
|
|
2075
|
-
</html>
|