@kaikybrofc/omnizap-system 2.3.1 → 2.3.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/README.md +20 -18
- package/app/controllers/messageController.js +473 -255
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
- package/app/observability/metrics.js +6 -3
- package/app/services/googleWebLinkService.js +77 -0
- package/database/index.js +2 -0
- package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
- package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
- package/package.json +1 -1
- package/public/index.html +12 -8
- package/public/js/apps/homeApp.js +75 -30
- package/public/js/apps/loginApp.js +184 -29
- package/public/js/apps/stickersAdminApp.js +3 -9
- package/public/js/apps/userApp.js +985 -55
- package/public/js/apps/userProfileApp.js +244 -0
- package/public/login/index.html +430 -100
- package/public/termos-de-uso/index.html +1 -1
- package/public/user/index.html +2 -180
- package/public/user/systemadm/index.html +774 -0
- package/server/controllers/stickerCatalog/nonCatalogHandlers.js +208 -0
- package/server/controllers/stickerCatalogController.js +1186 -363
- package/server/controllers/systemAdminController.js +141 -0
- package/server/controllers/userController.js +87 -0
- package/server/http/httpServer.js +72 -32
- package/server/middleware/cachePolicy.js +24 -0
- package/server/middleware/cachePolicyHelpers.js +2 -0
- package/server/middleware/rateLimit.js +82 -0
- package/server/middleware/requestLogger.js +16 -0
- package/server/middleware/requireAdminAuth.js +42 -0
- package/server/middleware/securityHeaders.js +6 -0
- package/server/routes/admin/systemAdminRouter.js +56 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +203 -0
- package/server/routes/metrics/metricsRouter.js +13 -0
- package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
- package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
- package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
- package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
- package/server/routes/user/userRouter.js +56 -0
- package/server/utils/safePath.js +26 -0
- package/server/routes/metricsRoute.js +0 -7
- package/server/routes/stickerCatalogRoute.js +0 -20
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* global document, window, fetch, URL, URLSearchParams */
|
|
1
|
+
/* global document, window, fetch, URL, URLSearchParams, Blob, Element */
|
|
2
2
|
|
|
3
3
|
const DEFAULT_API_BASE_PATH = '/api/sticker-packs';
|
|
4
4
|
const DEFAULT_STICKERS_PATH = '/stickers';
|
|
@@ -31,6 +31,7 @@ if (root) {
|
|
|
31
31
|
manageHeadLink: document.getElementById('user-manage-head-link'),
|
|
32
32
|
manageMainLink: document.getElementById('user-manage-main-link'),
|
|
33
33
|
currentYear: document.getElementById('user-current-year'),
|
|
34
|
+
|
|
34
35
|
adminPanel: document.getElementById('user-admin-panel'),
|
|
35
36
|
adminRole: document.getElementById('user-admin-role'),
|
|
36
37
|
adminStatus: document.getElementById('user-admin-status'),
|
|
@@ -41,6 +42,12 @@ if (root) {
|
|
|
41
42
|
adminOverview: document.getElementById('user-admin-overview'),
|
|
42
43
|
adminRefreshBtn: document.getElementById('user-admin-refresh-btn'),
|
|
43
44
|
adminLogoutBtn: document.getElementById('user-admin-logout-btn'),
|
|
45
|
+
|
|
46
|
+
adminBotsOnline: document.getElementById('user-admin-bots-online'),
|
|
47
|
+
adminMessagesToday: document.getElementById('user-admin-messages-today'),
|
|
48
|
+
adminSpamBlocked: document.getElementById('user-admin-spam-blocked'),
|
|
49
|
+
adminUptime: document.getElementById('user-admin-uptime'),
|
|
50
|
+
adminErrors5xx: document.getElementById('user-admin-errors-5xx'),
|
|
44
51
|
adminTotalPacks: document.getElementById('user-admin-total-packs'),
|
|
45
52
|
adminTotalStickers: document.getElementById('user-admin-total-stickers'),
|
|
46
53
|
adminActiveBans: document.getElementById('user-admin-active-bans'),
|
|
@@ -49,6 +56,32 @@ if (root) {
|
|
|
49
56
|
adminVisits24h: document.getElementById('user-admin-visits-24h'),
|
|
50
57
|
adminVisits7d: document.getElementById('user-admin-visits-7d'),
|
|
51
58
|
adminUniqueVisitors7d: document.getElementById('user-admin-unique-visitors-7d'),
|
|
59
|
+
|
|
60
|
+
adminHealthCpu: document.getElementById('user-admin-health-cpu'),
|
|
61
|
+
adminHealthRam: document.getElementById('user-admin-health-ram'),
|
|
62
|
+
adminHealthLatency: document.getElementById('user-admin-health-latency'),
|
|
63
|
+
adminHealthQueue: document.getElementById('user-admin-health-queue'),
|
|
64
|
+
adminHealthDb: document.getElementById('user-admin-health-db'),
|
|
65
|
+
|
|
66
|
+
adminModerationList: document.getElementById('user-admin-moderation-list'),
|
|
67
|
+
adminSessionsList: document.getElementById('user-admin-sessions-list'),
|
|
68
|
+
adminUsersList: document.getElementById('user-admin-users-list'),
|
|
69
|
+
adminBansList: document.getElementById('user-admin-bans-list'),
|
|
70
|
+
adminAuditList: document.getElementById('user-admin-audit-list'),
|
|
71
|
+
adminFlagsList: document.getElementById('user-admin-flags-list'),
|
|
72
|
+
adminAlertsList: document.getElementById('user-admin-alerts-list'),
|
|
73
|
+
adminOpsStatus: document.getElementById('user-admin-ops-status'),
|
|
74
|
+
|
|
75
|
+
adminSearchForm: document.getElementById('user-admin-search-form'),
|
|
76
|
+
adminSearchInput: document.getElementById('user-admin-search-input'),
|
|
77
|
+
adminSearchBtn: document.getElementById('user-admin-search-btn'),
|
|
78
|
+
adminSearchResults: document.getElementById('user-admin-search-results'),
|
|
79
|
+
|
|
80
|
+
adminExportMetricsJsonBtn: document.getElementById('user-admin-export-metrics-json'),
|
|
81
|
+
adminExportMetricsCsvBtn: document.getElementById('user-admin-export-metrics-csv'),
|
|
82
|
+
adminExportEventsJsonBtn: document.getElementById('user-admin-export-events-json'),
|
|
83
|
+
adminExportEventsCsvBtn: document.getElementById('user-admin-export-events-csv'),
|
|
84
|
+
adminOpButtons: Array.from(document.querySelectorAll('[data-admin-op-action]')),
|
|
52
85
|
};
|
|
53
86
|
|
|
54
87
|
const state = {
|
|
@@ -59,6 +92,8 @@ if (root) {
|
|
|
59
92
|
adminBusy: false,
|
|
60
93
|
adminStatusPayload: null,
|
|
61
94
|
adminOverviewPayload: null,
|
|
95
|
+
adminSearchPayload: null,
|
|
96
|
+
adminOpsMessage: '',
|
|
62
97
|
};
|
|
63
98
|
|
|
64
99
|
const sessionApiPath = `${state.apiBasePath}/auth/google/session`;
|
|
@@ -66,12 +101,23 @@ if (root) {
|
|
|
66
101
|
const botContactApiPath = `${state.apiBasePath}/bot-contact`;
|
|
67
102
|
const adminSessionApiPath = `${state.apiBasePath}/admin/session`;
|
|
68
103
|
const adminOverviewApiPath = `${state.apiBasePath}/admin/overview`;
|
|
104
|
+
const adminForceLogoutApiPath = `${state.apiBasePath}/admin/users/force-logout`;
|
|
105
|
+
const adminFeatureFlagsApiPath = `${state.apiBasePath}/admin/feature-flags`;
|
|
106
|
+
const adminOpsApiPath = `${state.apiBasePath}/admin/ops`;
|
|
107
|
+
const adminSearchApiPath = `${state.apiBasePath}/admin/search`;
|
|
108
|
+
const adminExportApiPath = `${state.apiBasePath}/admin/export`;
|
|
109
|
+
const adminBansApiPath = `${state.apiBasePath}/admin/bans`;
|
|
69
110
|
|
|
70
111
|
const setText = (el, value) => {
|
|
71
112
|
if (!el) return;
|
|
72
113
|
el.textContent = String(value || '');
|
|
73
114
|
};
|
|
74
115
|
|
|
116
|
+
const clearNode = (el) => {
|
|
117
|
+
if (!el) return;
|
|
118
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
119
|
+
};
|
|
120
|
+
|
|
75
121
|
const showError = (message) => {
|
|
76
122
|
if (!ui.error) return;
|
|
77
123
|
const safeMessage = String(message || '').trim();
|
|
@@ -87,6 +133,8 @@ if (root) {
|
|
|
87
133
|
};
|
|
88
134
|
|
|
89
135
|
const normalizeDigits = (value) => String(value || '').replace(/\D+/g, '');
|
|
136
|
+
const normalizeString = (value) => String(value || '').trim();
|
|
137
|
+
const isObject = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
90
138
|
|
|
91
139
|
const formatPhone = (digits) => {
|
|
92
140
|
const value = normalizeDigits(digits);
|
|
@@ -106,9 +154,28 @@ if (root) {
|
|
|
106
154
|
return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(ms));
|
|
107
155
|
};
|
|
108
156
|
|
|
157
|
+
const formatPercent = (value) => {
|
|
158
|
+
const numeric = Number(value);
|
|
159
|
+
if (!Number.isFinite(numeric)) return 'n/d';
|
|
160
|
+
return `${numeric.toFixed(1)}%`;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const formatMilliseconds = (value) => {
|
|
164
|
+
const numeric = Number(value);
|
|
165
|
+
if (!Number.isFinite(numeric)) return 'n/d';
|
|
166
|
+
return `${Math.round(numeric)} ms`;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const formatIntegerOrNd = (value) => {
|
|
170
|
+
const numeric = Number(value);
|
|
171
|
+
if (!Number.isFinite(numeric)) return 'n/d';
|
|
172
|
+
return formatNumber(numeric);
|
|
173
|
+
};
|
|
174
|
+
|
|
109
175
|
const buildLoginRedirectUrl = () => {
|
|
110
176
|
const loginUrl = new URL(state.loginPath, window.location.origin);
|
|
111
|
-
|
|
177
|
+
const nextPath = `${window.location.pathname || '/user/systemadm/'}${window.location.search || ''}`;
|
|
178
|
+
loginUrl.searchParams.set('next', nextPath);
|
|
112
179
|
return `${loginUrl.pathname}${loginUrl.search}`;
|
|
113
180
|
};
|
|
114
181
|
|
|
@@ -128,6 +195,7 @@ if (root) {
|
|
|
128
195
|
credentials: 'include',
|
|
129
196
|
...init,
|
|
130
197
|
});
|
|
198
|
+
|
|
131
199
|
let payload = null;
|
|
132
200
|
try {
|
|
133
201
|
payload = await response.json();
|
|
@@ -143,24 +211,190 @@ if (root) {
|
|
|
143
211
|
return payload || {};
|
|
144
212
|
};
|
|
145
213
|
|
|
214
|
+
const fetchWithAuth = async (url, init = {}) => {
|
|
215
|
+
const response = await fetch(url, {
|
|
216
|
+
credentials: 'include',
|
|
217
|
+
...init,
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
let message = `Falha HTTP ${response.status}`;
|
|
221
|
+
try {
|
|
222
|
+
const payload = await response.json();
|
|
223
|
+
if (payload?.error) message = payload.error;
|
|
224
|
+
} catch {
|
|
225
|
+
// noop
|
|
226
|
+
}
|
|
227
|
+
const error = new Error(message);
|
|
228
|
+
error.statusCode = response.status;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
return response;
|
|
232
|
+
};
|
|
233
|
+
|
|
146
234
|
const redirectToLogin = () => {
|
|
147
235
|
window.location.assign(buildLoginRedirectUrl());
|
|
148
236
|
};
|
|
149
237
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
238
|
+
const normalizeOwnerJidCandidate = (value) => {
|
|
239
|
+
const jid = normalizeString(value);
|
|
240
|
+
if (!jid || !jid.includes('@')) return '';
|
|
241
|
+
if (jid.endsWith('@g.us')) return '';
|
|
242
|
+
return jid;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const compactIdentityPayload = (raw = {}) => {
|
|
246
|
+
const payload = {};
|
|
247
|
+
const sessionToken = normalizeString(raw.session_token);
|
|
248
|
+
const googleSub = normalizeString(raw.google_sub);
|
|
249
|
+
const email = normalizeString(raw.email);
|
|
250
|
+
const ownerJid = normalizeOwnerJidCandidate(raw.owner_jid);
|
|
251
|
+
if (sessionToken) payload.session_token = sessionToken;
|
|
252
|
+
if (googleSub) payload.google_sub = googleSub;
|
|
253
|
+
if (email) payload.email = email;
|
|
254
|
+
if (ownerJid) payload.owner_jid = ownerJid;
|
|
255
|
+
return payload;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const buildIdentityLabel = (identity = {}) => {
|
|
259
|
+
const email = normalizeString(identity.email);
|
|
260
|
+
const ownerJid = normalizeString(identity.owner_jid);
|
|
261
|
+
const googleSub = normalizeString(identity.google_sub);
|
|
262
|
+
const sessionToken = normalizeString(identity.session_token);
|
|
263
|
+
if (email) return email;
|
|
264
|
+
if (ownerJid) return ownerJid;
|
|
265
|
+
if (googleSub) return googleSub;
|
|
266
|
+
if (sessionToken) return `${sessionToken.slice(0, 8)}...`;
|
|
267
|
+
return 'identidade';
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const getAdminSession = () => state.adminStatusPayload?.session || null;
|
|
271
|
+
const isAdminAuthenticated = () => Boolean(getAdminSession()?.authenticated);
|
|
272
|
+
const isAdminEligible = () => Boolean(state.adminStatusPayload?.eligible_google_login || isAdminAuthenticated());
|
|
273
|
+
|
|
274
|
+
const resolveAdminRole = () =>
|
|
275
|
+
String(getAdminSession()?.role || state.adminStatusPayload?.eligible_role || '')
|
|
276
|
+
.trim()
|
|
277
|
+
.toLowerCase();
|
|
278
|
+
|
|
279
|
+
const formatAdminRole = (role) => {
|
|
280
|
+
if (role === 'owner') return 'dono';
|
|
281
|
+
if (role === 'moderator') return 'moderador';
|
|
282
|
+
return 'admin';
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const createItemMeta = (text) => {
|
|
286
|
+
const p = document.createElement('p');
|
|
287
|
+
p.className = 'admin-item-meta';
|
|
288
|
+
p.textContent = text;
|
|
289
|
+
return p;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const createBadge = (label, severity = 'low') => {
|
|
293
|
+
const normalizedSeverity = ['critical', 'high', 'medium', 'low'].includes(String(severity)) ? String(severity) : 'low';
|
|
294
|
+
const badge = document.createElement('span');
|
|
295
|
+
badge.className = `admin-badge ${normalizedSeverity}`;
|
|
296
|
+
badge.textContent = String(label || '').trim() || normalizedSeverity.toUpperCase();
|
|
297
|
+
return badge;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const createMiniButton = (label, onClick) => {
|
|
301
|
+
const button = document.createElement('button');
|
|
302
|
+
button.type = 'button';
|
|
303
|
+
button.className = 'admin-mini-btn';
|
|
304
|
+
button.textContent = label;
|
|
305
|
+
button.addEventListener('click', () => {
|
|
306
|
+
if (typeof onClick === 'function') void onClick();
|
|
307
|
+
});
|
|
308
|
+
return button;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const createMiniLink = (label, href) => {
|
|
312
|
+
const link = document.createElement('a');
|
|
313
|
+
link.className = 'admin-mini-btn';
|
|
314
|
+
link.textContent = label;
|
|
315
|
+
link.href = href;
|
|
316
|
+
link.target = '_blank';
|
|
317
|
+
link.rel = 'noreferrer noopener';
|
|
318
|
+
return link;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const renderListPlaceholder = (container, message) => {
|
|
322
|
+
if (!container) return;
|
|
323
|
+
clearNode(container);
|
|
324
|
+
container.appendChild(createItemMeta(message));
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const appendListItem = ({ container, title, severity = '', badgeLabel = '', meta = [], actions = [], customNode = null }) => {
|
|
328
|
+
if (!container) return;
|
|
329
|
+
|
|
330
|
+
const item = document.createElement('article');
|
|
331
|
+
item.className = 'admin-item';
|
|
332
|
+
|
|
333
|
+
const titleEl = document.createElement('p');
|
|
334
|
+
titleEl.className = 'admin-item-title';
|
|
335
|
+
titleEl.textContent = title;
|
|
336
|
+
item.appendChild(titleEl);
|
|
337
|
+
|
|
338
|
+
if (badgeLabel) {
|
|
339
|
+
item.appendChild(createBadge(badgeLabel, severity));
|
|
156
340
|
}
|
|
157
|
-
|
|
341
|
+
|
|
342
|
+
for (const line of meta) {
|
|
343
|
+
const text = normalizeString(line);
|
|
344
|
+
if (!text) continue;
|
|
345
|
+
item.appendChild(createItemMeta(text));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (customNode) item.appendChild(customNode);
|
|
349
|
+
|
|
350
|
+
if (Array.isArray(actions) && actions.length) {
|
|
351
|
+
const actionsWrap = document.createElement('div');
|
|
352
|
+
actionsWrap.className = 'admin-item-actions';
|
|
353
|
+
for (const actionNode of actions) {
|
|
354
|
+
if (actionNode instanceof Element) actionsWrap.appendChild(actionNode);
|
|
355
|
+
}
|
|
356
|
+
if (actionsWrap.childNodes.length > 0) {
|
|
357
|
+
item.appendChild(actionsWrap);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
container.appendChild(item);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const setAdminBusy = (value) => {
|
|
365
|
+
const busy = Boolean(value);
|
|
366
|
+
state.adminBusy = busy;
|
|
367
|
+
|
|
368
|
+
const authenticated = isAdminAuthenticated();
|
|
369
|
+
const eligible = isAdminEligible();
|
|
370
|
+
|
|
371
|
+
if (ui.adminPanel) {
|
|
372
|
+
const controls = ui.adminPanel.querySelectorAll('button, input, select, textarea');
|
|
373
|
+
for (const control of controls) {
|
|
374
|
+
const inUnlockForm = Boolean(control.closest('#user-admin-unlock-form'));
|
|
375
|
+
const inOverview = Boolean(control.closest('#user-admin-overview'));
|
|
376
|
+
if (inUnlockForm) {
|
|
377
|
+
control.disabled = busy || !eligible || authenticated;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (inOverview) {
|
|
381
|
+
control.disabled = busy || !authenticated;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
control.disabled = busy;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (ui.adminPassword) ui.adminPassword.disabled = busy || !isAdminEligible() || isAdminAuthenticated();
|
|
389
|
+
if (ui.adminUnlockBtn) ui.adminUnlockBtn.disabled = busy || !isAdminEligible() || isAdminAuthenticated();
|
|
390
|
+
if (ui.adminRefreshBtn) ui.adminRefreshBtn.disabled = busy || !isAdminAuthenticated();
|
|
391
|
+
if (ui.adminLogoutBtn) ui.adminLogoutBtn.disabled = busy || !isAdminAuthenticated();
|
|
158
392
|
};
|
|
159
393
|
|
|
160
394
|
const renderSession = (sessionData) => {
|
|
161
395
|
const user = sessionData?.user || {};
|
|
162
|
-
const ownerPhone =
|
|
163
|
-
const ownerJid =
|
|
396
|
+
const ownerPhone = normalizeString(sessionData?.owner_phone);
|
|
397
|
+
const ownerJid = normalizeString(sessionData?.owner_jid);
|
|
164
398
|
|
|
165
399
|
setText(ui.name, user?.name || 'Conta Google');
|
|
166
400
|
setText(ui.email, user?.email || 'Email não disponível');
|
|
@@ -173,7 +407,7 @@ if (root) {
|
|
|
173
407
|
}
|
|
174
408
|
|
|
175
409
|
if (ui.avatar) {
|
|
176
|
-
const picture =
|
|
410
|
+
const picture = normalizeString(user?.picture) || FALLBACK_AVATAR;
|
|
177
411
|
ui.avatar.src = picture;
|
|
178
412
|
ui.avatar.onerror = () => {
|
|
179
413
|
ui.avatar.src = FALLBACK_AVATAR;
|
|
@@ -181,7 +415,7 @@ if (root) {
|
|
|
181
415
|
}
|
|
182
416
|
|
|
183
417
|
setText(ui.ownerJid, ownerJid || 'n/d');
|
|
184
|
-
setText(ui.googleSub,
|
|
418
|
+
setText(ui.googleSub, normalizeString(user?.sub) || 'n/d');
|
|
185
419
|
setText(ui.expiresAt, formatDateTime(sessionData?.expires_at));
|
|
186
420
|
|
|
187
421
|
if (ui.profile) ui.profile.hidden = false;
|
|
@@ -192,7 +426,7 @@ if (root) {
|
|
|
192
426
|
const renderPackMetrics = (payload) => {
|
|
193
427
|
const data = payload?.data || {};
|
|
194
428
|
const packs = Array.isArray(data?.packs) ? data.packs : [];
|
|
195
|
-
const stats = data?.stats
|
|
429
|
+
const stats = isObject(data?.stats) ? data.stats : {};
|
|
196
430
|
|
|
197
431
|
let stickers = 0;
|
|
198
432
|
let downloads = 0;
|
|
@@ -211,32 +445,12 @@ if (root) {
|
|
|
211
445
|
if (ui.grid) ui.grid.hidden = false;
|
|
212
446
|
};
|
|
213
447
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.trim()
|
|
221
|
-
.toLowerCase();
|
|
222
|
-
|
|
223
|
-
const formatAdminRole = (role) => {
|
|
224
|
-
if (role === 'owner') return 'dono';
|
|
225
|
-
if (role === 'moderator') return 'moderador';
|
|
226
|
-
return 'admin';
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const setAdminBusy = (value) => {
|
|
230
|
-
const busy = Boolean(value);
|
|
231
|
-
state.adminBusy = busy;
|
|
232
|
-
|
|
233
|
-
if (ui.adminPassword) ui.adminPassword.disabled = busy || isAdminAuthenticated();
|
|
234
|
-
if (ui.adminUnlockBtn) ui.adminUnlockBtn.disabled = busy || !isAdminEligible() || isAdminAuthenticated();
|
|
235
|
-
if (ui.adminRefreshBtn) ui.adminRefreshBtn.disabled = busy || !isAdminAuthenticated();
|
|
236
|
-
if (ui.adminLogoutBtn) ui.adminLogoutBtn.disabled = busy || !isAdminAuthenticated();
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const resetAdminMetrics = () => {
|
|
448
|
+
const setAdminMetricsDefaults = () => {
|
|
449
|
+
setText(ui.adminBotsOnline, '0');
|
|
450
|
+
setText(ui.adminMessagesToday, 'n/d');
|
|
451
|
+
setText(ui.adminSpamBlocked, 'n/d');
|
|
452
|
+
setText(ui.adminUptime, 'n/d');
|
|
453
|
+
setText(ui.adminErrors5xx, '0');
|
|
240
454
|
setText(ui.adminTotalPacks, '0');
|
|
241
455
|
setText(ui.adminTotalStickers, '0');
|
|
242
456
|
setText(ui.adminActiveBans, '0');
|
|
@@ -245,18 +459,594 @@ if (root) {
|
|
|
245
459
|
setText(ui.adminVisits24h, '0');
|
|
246
460
|
setText(ui.adminVisits7d, '0');
|
|
247
461
|
setText(ui.adminUniqueVisitors7d, '0');
|
|
462
|
+
|
|
463
|
+
setText(ui.adminHealthCpu, 'n/d');
|
|
464
|
+
setText(ui.adminHealthRam, 'n/d');
|
|
465
|
+
setText(ui.adminHealthLatency, 'n/d');
|
|
466
|
+
setText(ui.adminHealthQueue, 'n/d');
|
|
467
|
+
setText(ui.adminHealthDb, 'n/d');
|
|
468
|
+
|
|
469
|
+
renderListPlaceholder(ui.adminModerationList, 'Nenhum evento recente de moderação.');
|
|
470
|
+
renderListPlaceholder(ui.adminSessionsList, 'Nenhuma sessão ativa encontrada.');
|
|
471
|
+
renderListPlaceholder(ui.adminUsersList, 'Nenhum usuário encontrado.');
|
|
472
|
+
renderListPlaceholder(ui.adminBansList, 'Nenhuma conta bloqueada.');
|
|
473
|
+
renderListPlaceholder(ui.adminAuditList, 'Sem eventos de auditoria recentes.');
|
|
474
|
+
renderListPlaceholder(ui.adminFlagsList, 'Nenhuma feature flag disponível.');
|
|
475
|
+
renderListPlaceholder(ui.adminAlertsList, 'Sem alertas ativos no momento.');
|
|
476
|
+
renderListPlaceholder(ui.adminSearchResults, 'Faça uma busca para ver usuários, grupos, packs e sessões.');
|
|
477
|
+
setText(ui.adminOpsStatus, state.adminOpsMessage || 'Ações operacionais disponíveis.');
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const buildBanPayloadFromEvent = (event) => {
|
|
481
|
+
const metadata = isObject(event?.metadata) ? event.metadata : {};
|
|
482
|
+
const payload = compactIdentityPayload({
|
|
483
|
+
google_sub: metadata.google_sub,
|
|
484
|
+
email: metadata.email,
|
|
485
|
+
owner_jid: event?.sender_id || metadata.owner_jid,
|
|
486
|
+
});
|
|
487
|
+
if (!Object.keys(payload).length) return null;
|
|
488
|
+
payload.reason = `Ban via moderação (${normalizeString(event?.event_type) || 'evento'})`;
|
|
489
|
+
return payload;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const buildForceLogoutPayloadFromAny = (entry = {}) =>
|
|
493
|
+
compactIdentityPayload({
|
|
494
|
+
session_token: entry?.session_token,
|
|
495
|
+
google_sub: entry?.google_sub,
|
|
496
|
+
email: entry?.email,
|
|
497
|
+
owner_jid: entry?.owner_jid,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const handleAdminForceLogout = async (identity, contextLabel = '') => {
|
|
501
|
+
const payload = compactIdentityPayload(identity);
|
|
502
|
+
if (!Object.keys(payload).length) {
|
|
503
|
+
showAdminError('Não foi possível forçar logout: identidade ausente.');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (state.adminBusy) return;
|
|
507
|
+
|
|
508
|
+
showAdminError('');
|
|
509
|
+
setAdminBusy(true);
|
|
510
|
+
try {
|
|
511
|
+
const response = await fetchJson(adminForceLogoutApiPath, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
514
|
+
body: JSON.stringify(payload),
|
|
515
|
+
});
|
|
516
|
+
const removed = Number(response?.data?.removed_sessions || 0);
|
|
517
|
+
const label = normalizeString(contextLabel) || buildIdentityLabel(payload);
|
|
518
|
+
state.adminOpsMessage = `Logout forçado concluído para ${label}. Sessões removidas: ${removed}.`;
|
|
519
|
+
await refreshAdminArea({ keepCurrentError: true });
|
|
520
|
+
} catch (error) {
|
|
521
|
+
showAdminError(error?.message || 'Falha ao forçar logout.');
|
|
522
|
+
} finally {
|
|
523
|
+
setAdminBusy(false);
|
|
524
|
+
renderAdminPanel();
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const handleAdminBanCreate = async (banPayload, contextLabel = '') => {
|
|
529
|
+
if (!isObject(banPayload)) return;
|
|
530
|
+
const payload = {
|
|
531
|
+
...compactIdentityPayload(banPayload),
|
|
532
|
+
reason: normalizeString(banPayload.reason),
|
|
533
|
+
};
|
|
534
|
+
if (!payload.google_sub && !payload.email && !payload.owner_jid) {
|
|
535
|
+
showAdminError('Não foi possível banir: identidade ausente.');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (state.adminBusy) return;
|
|
539
|
+
|
|
540
|
+
showAdminError('');
|
|
541
|
+
setAdminBusy(true);
|
|
542
|
+
try {
|
|
543
|
+
const response = await fetchJson(adminBansApiPath, {
|
|
544
|
+
method: 'POST',
|
|
545
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
546
|
+
body: JSON.stringify(payload),
|
|
547
|
+
});
|
|
548
|
+
const created = Boolean(response?.data?.created);
|
|
549
|
+
const label = normalizeString(contextLabel) || buildIdentityLabel(payload);
|
|
550
|
+
state.adminOpsMessage = created ? `Conta banida: ${label}.` : `Conta já estava banida: ${label}.`;
|
|
551
|
+
await refreshAdminArea({ keepCurrentError: true });
|
|
552
|
+
} catch (error) {
|
|
553
|
+
showAdminError(error?.message || 'Falha ao criar ban.');
|
|
554
|
+
} finally {
|
|
555
|
+
setAdminBusy(false);
|
|
556
|
+
renderAdminPanel();
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const handleAdminBanRevoke = async (banId) => {
|
|
561
|
+
const normalizedId = normalizeString(banId);
|
|
562
|
+
if (!normalizedId || state.adminBusy) return;
|
|
563
|
+
|
|
564
|
+
showAdminError('');
|
|
565
|
+
setAdminBusy(true);
|
|
566
|
+
try {
|
|
567
|
+
await fetchJson(`${adminBansApiPath}/${encodeURIComponent(normalizedId)}/revoke`, { method: 'DELETE' });
|
|
568
|
+
state.adminOpsMessage = `Ban ${normalizedId} revogado com sucesso.`;
|
|
569
|
+
await refreshAdminArea({ keepCurrentError: true });
|
|
570
|
+
} catch (error) {
|
|
571
|
+
showAdminError(error?.message || 'Falha ao revogar ban.');
|
|
572
|
+
} finally {
|
|
573
|
+
setAdminBusy(false);
|
|
574
|
+
renderAdminPanel();
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const handleAdminFeatureFlagUpdate = async ({ flagName = '', isEnabled = false, rolloutPercent = 100, description = '' } = {}) => {
|
|
579
|
+
const normalizedName = normalizeString(flagName);
|
|
580
|
+
if (!normalizedName || state.adminBusy) return;
|
|
581
|
+
|
|
582
|
+
showAdminError('');
|
|
583
|
+
setAdminBusy(true);
|
|
584
|
+
try {
|
|
585
|
+
await fetchJson(adminFeatureFlagsApiPath, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
588
|
+
body: JSON.stringify({
|
|
589
|
+
flag_name: normalizedName,
|
|
590
|
+
is_enabled: Boolean(isEnabled),
|
|
591
|
+
rollout_percent: Math.max(0, Math.min(100, Math.floor(Number(rolloutPercent) || 0))),
|
|
592
|
+
description: normalizeString(description),
|
|
593
|
+
}),
|
|
594
|
+
});
|
|
595
|
+
state.adminOpsMessage = `Feature flag ${normalizedName} atualizada.`;
|
|
596
|
+
await refreshAdminArea({ keepCurrentError: true });
|
|
597
|
+
} catch (error) {
|
|
598
|
+
showAdminError(error?.message || 'Falha ao atualizar feature flag.');
|
|
599
|
+
} finally {
|
|
600
|
+
setAdminBusy(false);
|
|
601
|
+
renderAdminPanel();
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const handleAdminOpsAction = async (action) => {
|
|
606
|
+
const normalizedAction = normalizeString(action);
|
|
607
|
+
if (!normalizedAction || state.adminBusy) return;
|
|
608
|
+
|
|
609
|
+
showAdminError('');
|
|
610
|
+
setAdminBusy(true);
|
|
611
|
+
try {
|
|
612
|
+
const response = await fetchJson(adminOpsApiPath, {
|
|
613
|
+
method: 'POST',
|
|
614
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
615
|
+
body: JSON.stringify({ action: normalizedAction }),
|
|
616
|
+
});
|
|
617
|
+
const message = normalizeString(response?.data?.message) || `Ação ${normalizedAction} concluída.`;
|
|
618
|
+
state.adminOpsMessage = `${message} (${formatDateTime(response?.data?.updated_at)})`;
|
|
619
|
+
setText(ui.adminOpsStatus, state.adminOpsMessage);
|
|
620
|
+
await refreshAdminArea({ keepCurrentError: true });
|
|
621
|
+
} catch (error) {
|
|
622
|
+
showAdminError(error?.message || 'Falha ao executar ação operacional.');
|
|
623
|
+
} finally {
|
|
624
|
+
setAdminBusy(false);
|
|
625
|
+
renderAdminPanel();
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const renderModerationQueue = (events) => {
|
|
630
|
+
const list = Array.isArray(events) ? events : [];
|
|
631
|
+
clearNode(ui.adminModerationList);
|
|
632
|
+
|
|
633
|
+
if (!list.length) {
|
|
634
|
+
renderListPlaceholder(ui.adminModerationList, 'Nenhum evento recente de moderação.');
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
for (const event of list) {
|
|
639
|
+
const title = normalizeString(event?.title) || 'Evento de moderação';
|
|
640
|
+
const severity = normalizeString(event?.severity) || 'low';
|
|
641
|
+
const badgeLabel = severity.toUpperCase();
|
|
642
|
+
const createdAt = formatDateTime(event?.created_at || event?.revoked_at);
|
|
643
|
+
const meta = [
|
|
644
|
+
normalizeString(event?.subtitle),
|
|
645
|
+
`Tipo: ${normalizeString(event?.event_type) || 'evento'} • ${createdAt}`,
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
if (normalizeString(event?.reason)) meta.push(`Motivo: ${normalizeString(event.reason)}`);
|
|
649
|
+
|
|
650
|
+
const actions = [];
|
|
651
|
+
if (event?.event_type === 'ban') {
|
|
652
|
+
if (event?.ban_id && !event?.revoked_at) {
|
|
653
|
+
actions.push(createMiniButton('Revogar ban', () => handleAdminBanRevoke(event.ban_id)));
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
const banPayload = buildBanPayloadFromEvent(event);
|
|
657
|
+
if (banPayload) {
|
|
658
|
+
actions.push(createMiniButton('Banir conta', () => handleAdminBanCreate(banPayload, buildIdentityLabel(banPayload))));
|
|
659
|
+
}
|
|
660
|
+
const logoutPayload = compactIdentityPayload({
|
|
661
|
+
session_token: event?.metadata?.session_token,
|
|
662
|
+
google_sub: event?.metadata?.google_sub,
|
|
663
|
+
email: event?.metadata?.email,
|
|
664
|
+
owner_jid: event?.sender_id || event?.metadata?.owner_jid,
|
|
665
|
+
});
|
|
666
|
+
if (Object.keys(logoutPayload).length) {
|
|
667
|
+
actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(logoutPayload, buildIdentityLabel(logoutPayload))));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
appendListItem({
|
|
672
|
+
container: ui.adminModerationList,
|
|
673
|
+
title,
|
|
674
|
+
severity,
|
|
675
|
+
badgeLabel,
|
|
676
|
+
meta,
|
|
677
|
+
actions,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const renderActiveSessions = (sessions) => {
|
|
683
|
+
const list = Array.isArray(sessions) ? sessions : [];
|
|
684
|
+
clearNode(ui.adminSessionsList);
|
|
685
|
+
|
|
686
|
+
if (!list.length) {
|
|
687
|
+
renderListPlaceholder(ui.adminSessionsList, 'Nenhuma sessão ativa encontrada.');
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
for (const session of list) {
|
|
692
|
+
const identity = compactIdentityPayload(session);
|
|
693
|
+
const title = normalizeString(session?.name || session?.email || session?.owner_jid || 'Sessão ativa');
|
|
694
|
+
const meta = [
|
|
695
|
+
`Email: ${normalizeString(session?.email) || 'n/d'}`,
|
|
696
|
+
`Owner: ${normalizeString(session?.owner_jid) || 'n/d'}`,
|
|
697
|
+
`Último acesso: ${formatDateTime(session?.last_seen_at)} • Expira: ${formatDateTime(session?.expires_at)}`,
|
|
698
|
+
];
|
|
699
|
+
const actions = [];
|
|
700
|
+
if (Object.keys(identity).length) {
|
|
701
|
+
actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
|
|
702
|
+
}
|
|
703
|
+
appendListItem({
|
|
704
|
+
container: ui.adminSessionsList,
|
|
705
|
+
title,
|
|
706
|
+
severity: 'low',
|
|
707
|
+
badgeLabel: 'ATIVA',
|
|
708
|
+
meta,
|
|
709
|
+
actions,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const renderKnownUsers = (users) => {
|
|
715
|
+
const list = Array.isArray(users) ? users : [];
|
|
716
|
+
clearNode(ui.adminUsersList);
|
|
717
|
+
|
|
718
|
+
if (!list.length) {
|
|
719
|
+
renderListPlaceholder(ui.adminUsersList, 'Nenhum usuário encontrado.');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
for (const user of list) {
|
|
724
|
+
const identity = buildForceLogoutPayloadFromAny(user);
|
|
725
|
+
const title = normalizeString(user?.name || user?.email || user?.owner_jid || 'Usuário');
|
|
726
|
+
const meta = [
|
|
727
|
+
`Email: ${normalizeString(user?.email) || 'n/d'}`,
|
|
728
|
+
`Owner: ${normalizeString(user?.owner_jid) || 'n/d'}`,
|
|
729
|
+
`Último login: ${formatDateTime(user?.last_login_at)} • Último acesso: ${formatDateTime(user?.last_seen_at)}`,
|
|
730
|
+
];
|
|
731
|
+
const actions = [];
|
|
732
|
+
if (Object.keys(identity).length) {
|
|
733
|
+
actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
|
|
734
|
+
}
|
|
735
|
+
appendListItem({
|
|
736
|
+
container: ui.adminUsersList,
|
|
737
|
+
title,
|
|
738
|
+
severity: 'low',
|
|
739
|
+
badgeLabel: 'USER',
|
|
740
|
+
meta,
|
|
741
|
+
actions,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const renderBlockedAccounts = (bans) => {
|
|
747
|
+
const list = Array.isArray(bans) ? bans : [];
|
|
748
|
+
clearNode(ui.adminBansList);
|
|
749
|
+
|
|
750
|
+
if (!list.length) {
|
|
751
|
+
renderListPlaceholder(ui.adminBansList, 'Nenhuma conta bloqueada.');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
for (const ban of list) {
|
|
756
|
+
const identity = normalizeString(ban?.email || ban?.owner_jid || ban?.google_sub || `ban:${ban?.id || ''}`);
|
|
757
|
+
const isRevoked = Boolean(ban?.revoked_at);
|
|
758
|
+
const meta = [
|
|
759
|
+
`Criado: ${formatDateTime(ban?.created_at)}${isRevoked ? ` • Revogado: ${formatDateTime(ban?.revoked_at)}` : ''}`,
|
|
760
|
+
`Motivo: ${normalizeString(ban?.reason) || 'não informado'}`,
|
|
761
|
+
];
|
|
762
|
+
const actions = [];
|
|
763
|
+
if (!isRevoked && normalizeString(ban?.id)) {
|
|
764
|
+
actions.push(createMiniButton('Revogar ban', () => handleAdminBanRevoke(ban.id)));
|
|
765
|
+
}
|
|
766
|
+
appendListItem({
|
|
767
|
+
container: ui.adminBansList,
|
|
768
|
+
title: identity,
|
|
769
|
+
severity: isRevoked ? 'low' : 'critical',
|
|
770
|
+
badgeLabel: isRevoked ? 'REVOGADO' : 'BLOQUEADO',
|
|
771
|
+
meta,
|
|
772
|
+
actions,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const renderAuditLog = (events) => {
|
|
778
|
+
const list = Array.isArray(events) ? events : [];
|
|
779
|
+
clearNode(ui.adminAuditList);
|
|
780
|
+
|
|
781
|
+
if (!list.length) {
|
|
782
|
+
renderListPlaceholder(ui.adminAuditList, 'Sem eventos de auditoria recentes.');
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
for (const item of list.slice(0, 80)) {
|
|
787
|
+
const action = normalizeString(item?.action || 'action');
|
|
788
|
+
const targetType = normalizeString(item?.target_type || 'target');
|
|
789
|
+
const targetId = normalizeString(item?.target_id || '');
|
|
790
|
+
const status = normalizeString(item?.status || 'success');
|
|
791
|
+
const details = isObject(item?.details) ? Object.entries(item.details).slice(0, 3) : [];
|
|
792
|
+
const detailLine = details.length ? `Detalhes: ${details.map(([key, value]) => `${key}=${value}`).join(' • ')}` : '';
|
|
793
|
+
|
|
794
|
+
const meta = [
|
|
795
|
+
`Admin: ${normalizeString(item?.admin_email || item?.admin_google_sub || item?.admin_owner_jid || 'n/d')} (${formatAdminRole(normalizeString(item?.admin_role || 'admin'))})`,
|
|
796
|
+
`Alvo: ${targetType}${targetId ? ` / ${targetId}` : ''} • Em: ${formatDateTime(item?.created_at)}`,
|
|
797
|
+
];
|
|
798
|
+
if (detailLine) meta.push(detailLine);
|
|
799
|
+
|
|
800
|
+
appendListItem({
|
|
801
|
+
container: ui.adminAuditList,
|
|
802
|
+
title: action,
|
|
803
|
+
severity: status === 'success' ? 'low' : 'high',
|
|
804
|
+
badgeLabel: status.toUpperCase(),
|
|
805
|
+
meta,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
const renderFeatureFlags = (flags) => {
|
|
811
|
+
const list = Array.isArray(flags) ? flags : [];
|
|
812
|
+
clearNode(ui.adminFlagsList);
|
|
813
|
+
|
|
814
|
+
if (!list.length) {
|
|
815
|
+
renderListPlaceholder(ui.adminFlagsList, 'Nenhuma feature flag disponível.');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
for (const flag of list) {
|
|
820
|
+
const flagName = normalizeString(flag?.flag_name);
|
|
821
|
+
const isEnabled = Boolean(flag?.is_enabled);
|
|
822
|
+
const rollout = Math.max(0, Math.min(100, Math.floor(Number(flag?.rollout_percent) || 0)));
|
|
823
|
+
const description = normalizeString(flag?.description);
|
|
824
|
+
const updatedBy = normalizeString(flag?.updated_by);
|
|
825
|
+
|
|
826
|
+
const rolloutForm = document.createElement('form');
|
|
827
|
+
rolloutForm.className = 'admin-inline-form';
|
|
828
|
+
|
|
829
|
+
const rolloutInput = document.createElement('input');
|
|
830
|
+
rolloutInput.className = 'admin-input';
|
|
831
|
+
rolloutInput.type = 'number';
|
|
832
|
+
rolloutInput.min = '0';
|
|
833
|
+
rolloutInput.max = '100';
|
|
834
|
+
rolloutInput.step = '1';
|
|
835
|
+
rolloutInput.value = String(rollout);
|
|
836
|
+
rolloutInput.setAttribute('aria-label', `Rollout de ${flagName}`);
|
|
837
|
+
|
|
838
|
+
const rolloutBtn = document.createElement('button');
|
|
839
|
+
rolloutBtn.type = 'submit';
|
|
840
|
+
rolloutBtn.className = 'admin-mini-btn';
|
|
841
|
+
rolloutBtn.textContent = 'Salvar rollout';
|
|
842
|
+
|
|
843
|
+
rolloutForm.appendChild(rolloutInput);
|
|
844
|
+
rolloutForm.appendChild(rolloutBtn);
|
|
845
|
+
rolloutForm.addEventListener('submit', (event) => {
|
|
846
|
+
event.preventDefault();
|
|
847
|
+
void handleAdminFeatureFlagUpdate({
|
|
848
|
+
flagName,
|
|
849
|
+
isEnabled,
|
|
850
|
+
rolloutPercent: rolloutInput.value,
|
|
851
|
+
description,
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
const actions = [
|
|
856
|
+
createMiniButton(isEnabled ? 'Desativar' : 'Ativar', () =>
|
|
857
|
+
handleAdminFeatureFlagUpdate({
|
|
858
|
+
flagName,
|
|
859
|
+
isEnabled: !isEnabled,
|
|
860
|
+
rolloutPercent: rollout,
|
|
861
|
+
description,
|
|
862
|
+
}),
|
|
863
|
+
),
|
|
864
|
+
];
|
|
865
|
+
|
|
866
|
+
appendListItem({
|
|
867
|
+
container: ui.adminFlagsList,
|
|
868
|
+
title: flagName || 'feature_flag',
|
|
869
|
+
severity: isEnabled ? 'low' : 'medium',
|
|
870
|
+
badgeLabel: isEnabled ? 'ON' : 'OFF',
|
|
871
|
+
meta: [
|
|
872
|
+
`Rollout: ${rollout}%`,
|
|
873
|
+
description ? `Descrição: ${description}` : 'Descrição: n/d',
|
|
874
|
+
`Atualizado por: ${updatedBy || 'n/d'} • ${formatDateTime(flag?.updated_at)}`,
|
|
875
|
+
],
|
|
876
|
+
actions,
|
|
877
|
+
customNode: rolloutForm,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const renderAlerts = (alerts) => {
|
|
883
|
+
const list = Array.isArray(alerts) ? alerts : [];
|
|
884
|
+
clearNode(ui.adminAlertsList);
|
|
885
|
+
|
|
886
|
+
if (!list.length) {
|
|
887
|
+
renderListPlaceholder(ui.adminAlertsList, 'Sem alertas ativos no momento.');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
for (const alert of list) {
|
|
892
|
+
const severity = normalizeString(alert?.severity || 'low');
|
|
893
|
+
const title = normalizeString(alert?.title || alert?.code || 'Alerta');
|
|
894
|
+
const meta = [
|
|
895
|
+
normalizeString(alert?.message || ''),
|
|
896
|
+
`Código: ${normalizeString(alert?.code || 'n/d')} • ${formatDateTime(alert?.created_at)}`,
|
|
897
|
+
];
|
|
898
|
+
appendListItem({
|
|
899
|
+
container: ui.adminAlertsList,
|
|
900
|
+
title,
|
|
901
|
+
severity,
|
|
902
|
+
badgeLabel: severity.toUpperCase(),
|
|
903
|
+
meta,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const renderSearchResults = (payload = state.adminSearchPayload) => {
|
|
909
|
+
if (!ui.adminSearchResults) return;
|
|
910
|
+
clearNode(ui.adminSearchResults);
|
|
911
|
+
|
|
912
|
+
if (!payload || !isObject(payload)) {
|
|
913
|
+
renderListPlaceholder(ui.adminSearchResults, 'Faça uma busca para ver usuários, grupos, packs e sessões.');
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const q = normalizeString(payload?.q);
|
|
918
|
+
const totals = isObject(payload?.totals) ? payload.totals : {};
|
|
919
|
+
const results = isObject(payload?.results) ? payload.results : {};
|
|
920
|
+
|
|
921
|
+
appendListItem({
|
|
922
|
+
container: ui.adminSearchResults,
|
|
923
|
+
title: `Resultado para "${q || 'consulta'}"`,
|
|
924
|
+
severity: 'low',
|
|
925
|
+
badgeLabel: 'BUSCA',
|
|
926
|
+
meta: [
|
|
927
|
+
`Usuários: ${formatIntegerOrNd(totals.users)}`,
|
|
928
|
+
`Sessões: ${formatIntegerOrNd(totals.sessions)}`,
|
|
929
|
+
`Grupos: ${formatIntegerOrNd(totals.groups)}`,
|
|
930
|
+
`Packs: ${formatIntegerOrNd(totals.packs)}`,
|
|
931
|
+
],
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
const users = Array.isArray(results.users) ? results.users : [];
|
|
935
|
+
for (const user of users) {
|
|
936
|
+
const identity = buildForceLogoutPayloadFromAny(user);
|
|
937
|
+
const actions = [];
|
|
938
|
+
if (Object.keys(identity).length) {
|
|
939
|
+
actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
|
|
940
|
+
}
|
|
941
|
+
appendListItem({
|
|
942
|
+
container: ui.adminSearchResults,
|
|
943
|
+
title: `[Usuário] ${normalizeString(user?.name || user?.email || user?.owner_jid || 'registro')}`,
|
|
944
|
+
severity: 'low',
|
|
945
|
+
badgeLabel: 'USER',
|
|
946
|
+
meta: [`Email: ${normalizeString(user?.email) || 'n/d'}`, `Owner: ${normalizeString(user?.owner_jid) || 'n/d'}`],
|
|
947
|
+
actions,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const sessions = Array.isArray(results.sessions) ? results.sessions : [];
|
|
952
|
+
for (const session of sessions) {
|
|
953
|
+
const identity = buildForceLogoutPayloadFromAny(session);
|
|
954
|
+
const actions = [];
|
|
955
|
+
if (Object.keys(identity).length) {
|
|
956
|
+
actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
|
|
957
|
+
}
|
|
958
|
+
appendListItem({
|
|
959
|
+
container: ui.adminSearchResults,
|
|
960
|
+
title: `[Sessão] ${normalizeString(session?.name || session?.email || session?.owner_jid || 'ativa')}`,
|
|
961
|
+
severity: 'low',
|
|
962
|
+
badgeLabel: 'SESSÃO',
|
|
963
|
+
meta: [`Email: ${normalizeString(session?.email) || 'n/d'}`, `Expira: ${formatDateTime(session?.expires_at)}`],
|
|
964
|
+
actions,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const groups = Array.isArray(results.groups) ? results.groups : [];
|
|
969
|
+
for (const group of groups) {
|
|
970
|
+
appendListItem({
|
|
971
|
+
container: ui.adminSearchResults,
|
|
972
|
+
title: `[Grupo] ${normalizeString(group?.subject || group?.id || 'grupo')}`,
|
|
973
|
+
severity: 'medium',
|
|
974
|
+
badgeLabel: 'GRUPO',
|
|
975
|
+
meta: [`ID: ${normalizeString(group?.id) || 'n/d'}`, `Atualizado: ${formatDateTime(group?.updated_at)}`],
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const packs = Array.isArray(results.packs) ? results.packs : [];
|
|
980
|
+
for (const pack of packs) {
|
|
981
|
+
const packUrl = normalizeString(pack?.web_url);
|
|
982
|
+
const actions = [];
|
|
983
|
+
if (packUrl) actions.push(createMiniLink('Abrir pack', packUrl));
|
|
984
|
+
appendListItem({
|
|
985
|
+
container: ui.adminSearchResults,
|
|
986
|
+
title: `[Pack] ${normalizeString(pack?.name || pack?.pack_key || 'pack')}`,
|
|
987
|
+
severity: 'low',
|
|
988
|
+
badgeLabel: normalizeString(pack?.visibility || 'pack').toUpperCase(),
|
|
989
|
+
meta: [`Owner: ${normalizeString(pack?.owner_jid) || 'n/d'}`, `Stickers: ${formatIntegerOrNd(pack?.stickers_count)}`],
|
|
990
|
+
actions,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (!users.length && !sessions.length && !groups.length && !packs.length) {
|
|
995
|
+
appendListItem({
|
|
996
|
+
container: ui.adminSearchResults,
|
|
997
|
+
title: 'Nenhum resultado encontrado.',
|
|
998
|
+
severity: 'low',
|
|
999
|
+
badgeLabel: 'VAZIO',
|
|
1000
|
+
meta: ['Tente outro termo de busca.'],
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const renderSystemHealth = (health) => {
|
|
1006
|
+
const statusMap = {
|
|
1007
|
+
ok: 'OK',
|
|
1008
|
+
degraded: 'Degradado',
|
|
1009
|
+
unknown: 'Indefinido',
|
|
1010
|
+
};
|
|
1011
|
+
setText(ui.adminHealthCpu, formatPercent(health?.cpu_percent));
|
|
1012
|
+
setText(ui.adminHealthRam, formatPercent(health?.ram_percent));
|
|
1013
|
+
setText(ui.adminHealthLatency, formatMilliseconds(health?.http_latency_p95_ms));
|
|
1014
|
+
setText(ui.adminHealthQueue, formatIntegerOrNd(health?.queue_pending));
|
|
1015
|
+
setText(ui.adminHealthDb, statusMap[normalizeString(health?.db_status)] || 'n/d');
|
|
248
1016
|
};
|
|
249
1017
|
|
|
250
1018
|
const renderAdminOverview = () => {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
setText(ui.
|
|
258
|
-
setText(ui.
|
|
259
|
-
setText(ui.
|
|
1019
|
+
const payload = state.adminOverviewPayload || {};
|
|
1020
|
+
const counters = isObject(payload?.counters) ? payload.counters : {};
|
|
1021
|
+
const dashboard = isObject(payload?.dashboard_quick) ? payload.dashboard_quick : {};
|
|
1022
|
+
const usersSessions = isObject(payload?.users_sessions) ? payload.users_sessions : {};
|
|
1023
|
+
const health = isObject(payload?.system_health) ? payload.system_health : {};
|
|
1024
|
+
|
|
1025
|
+
setText(ui.adminBotsOnline, formatIntegerOrNd(dashboard?.bots_online || 0));
|
|
1026
|
+
setText(ui.adminMessagesToday, formatIntegerOrNd(dashboard?.messages_today));
|
|
1027
|
+
setText(ui.adminSpamBlocked, formatIntegerOrNd(dashboard?.spam_blocked_today));
|
|
1028
|
+
setText(ui.adminUptime, normalizeString(dashboard?.uptime) || 'n/d');
|
|
1029
|
+
setText(ui.adminErrors5xx, formatIntegerOrNd(dashboard?.errors_5xx || 0));
|
|
1030
|
+
|
|
1031
|
+
setText(ui.adminTotalPacks, formatIntegerOrNd(counters?.total_packs_any_status || 0));
|
|
1032
|
+
setText(ui.adminTotalStickers, formatIntegerOrNd(counters?.total_stickers_any_status || 0));
|
|
1033
|
+
setText(ui.adminActiveBans, formatIntegerOrNd(counters?.active_bans || 0));
|
|
1034
|
+
setText(ui.adminKnownUsers, formatIntegerOrNd(counters?.known_google_users || 0));
|
|
1035
|
+
setText(ui.adminActiveSessions, formatIntegerOrNd(counters?.active_google_sessions || 0));
|
|
1036
|
+
setText(ui.adminVisits24h, formatIntegerOrNd(counters?.visit_events_24h || 0));
|
|
1037
|
+
setText(ui.adminVisits7d, formatIntegerOrNd(counters?.visit_events_7d || 0));
|
|
1038
|
+
setText(ui.adminUniqueVisitors7d, formatIntegerOrNd(counters?.unique_visitors_7d || 0));
|
|
1039
|
+
|
|
1040
|
+
renderSystemHealth(health);
|
|
1041
|
+
renderModerationQueue(payload?.moderation_queue);
|
|
1042
|
+
renderActiveSessions(usersSessions?.active_sessions);
|
|
1043
|
+
renderKnownUsers(usersSessions?.users);
|
|
1044
|
+
renderBlockedAccounts(usersSessions?.blocked_accounts);
|
|
1045
|
+
renderAuditLog(payload?.audit_log);
|
|
1046
|
+
renderFeatureFlags(payload?.feature_flags);
|
|
1047
|
+
renderAlerts(payload?.alerts);
|
|
1048
|
+
renderSearchResults();
|
|
1049
|
+
setText(ui.adminOpsStatus, state.adminOpsMessage || 'Ações operacionais disponíveis.');
|
|
260
1050
|
};
|
|
261
1051
|
|
|
262
1052
|
const renderAdminPanel = () => {
|
|
@@ -265,6 +1055,7 @@ if (root) {
|
|
|
265
1055
|
const enabled = state.adminStatusPayload?.enabled !== false;
|
|
266
1056
|
const authenticated = isAdminAuthenticated();
|
|
267
1057
|
const eligible = isAdminEligible();
|
|
1058
|
+
const role = resolveAdminRole();
|
|
268
1059
|
|
|
269
1060
|
if (!enabled || (!eligible && !authenticated)) {
|
|
270
1061
|
ui.adminPanel.hidden = true;
|
|
@@ -275,24 +1066,33 @@ if (root) {
|
|
|
275
1066
|
}
|
|
276
1067
|
|
|
277
1068
|
ui.adminPanel.hidden = false;
|
|
278
|
-
const role = resolveAdminRole();
|
|
279
1069
|
setText(ui.adminRole, formatAdminRole(role));
|
|
280
1070
|
|
|
281
1071
|
if (authenticated) {
|
|
282
|
-
setText(ui.adminStatus, `Sessão admin ativa como ${formatAdminRole(role)}.`);
|
|
1072
|
+
setText(ui.adminStatus, `Sessão admin ativa como ${formatAdminRole(role)}. Ferramentas operacionais liberadas abaixo.`);
|
|
283
1073
|
if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = true;
|
|
284
1074
|
if (ui.adminOverview) ui.adminOverview.hidden = false;
|
|
285
1075
|
renderAdminOverview();
|
|
286
1076
|
} else {
|
|
287
|
-
setText(ui.adminStatus, `Conta elegível para admin (${formatAdminRole(role)}). Informe a senha para liberar os dados.`);
|
|
1077
|
+
setText(ui.adminStatus, `Conta elegível para admin (${formatAdminRole(role)}). Informe a senha para liberar os dados sensíveis.`);
|
|
288
1078
|
if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = false;
|
|
289
1079
|
if (ui.adminOverview) ui.adminOverview.hidden = true;
|
|
290
|
-
|
|
1080
|
+
setAdminMetricsDefaults();
|
|
291
1081
|
}
|
|
292
1082
|
|
|
293
1083
|
setAdminBusy(state.adminBusy);
|
|
294
1084
|
};
|
|
295
1085
|
|
|
1086
|
+
const loadBotPhone = async () => {
|
|
1087
|
+
try {
|
|
1088
|
+
const payload = await fetchJson(botContactApiPath, { method: 'GET' });
|
|
1089
|
+
state.botPhone = normalizeDigits(payload?.data?.phone || '');
|
|
1090
|
+
} catch {
|
|
1091
|
+
state.botPhone = '';
|
|
1092
|
+
}
|
|
1093
|
+
if (ui.chatLink) ui.chatLink.href = buildWhatsAppMenuUrl(state.botPhone);
|
|
1094
|
+
};
|
|
1095
|
+
|
|
296
1096
|
const loadAdminStatus = async () => {
|
|
297
1097
|
const payload = await fetchJson(adminSessionApiPath, { method: 'GET' });
|
|
298
1098
|
state.adminStatusPayload = payload?.data || null;
|
|
@@ -324,7 +1124,7 @@ if (root) {
|
|
|
324
1124
|
};
|
|
325
1125
|
|
|
326
1126
|
const handleAdminUnlock = async () => {
|
|
327
|
-
const password =
|
|
1127
|
+
const password = normalizeString(ui.adminPassword?.value);
|
|
328
1128
|
if (!password) {
|
|
329
1129
|
showAdminError('Informe a senha do painel admin.');
|
|
330
1130
|
return;
|
|
@@ -340,6 +1140,8 @@ if (root) {
|
|
|
340
1140
|
body: JSON.stringify({ password }),
|
|
341
1141
|
});
|
|
342
1142
|
state.adminStatusPayload = payload?.data || null;
|
|
1143
|
+
state.adminOpsMessage = '';
|
|
1144
|
+
state.adminSearchPayload = null;
|
|
343
1145
|
if (ui.adminPassword) ui.adminPassword.value = '';
|
|
344
1146
|
await loadAdminOverview();
|
|
345
1147
|
} catch (error) {
|
|
@@ -359,9 +1161,11 @@ if (root) {
|
|
|
359
1161
|
try {
|
|
360
1162
|
await fetchJson(adminSessionApiPath, { method: 'DELETE' });
|
|
361
1163
|
} catch {
|
|
362
|
-
// no-op
|
|
1164
|
+
// no-op
|
|
363
1165
|
}
|
|
364
1166
|
state.adminOverviewPayload = null;
|
|
1167
|
+
state.adminSearchPayload = null;
|
|
1168
|
+
state.adminOpsMessage = '';
|
|
365
1169
|
await loadAdminStatus().catch(() => {
|
|
366
1170
|
state.adminStatusPayload = null;
|
|
367
1171
|
});
|
|
@@ -377,6 +1181,93 @@ if (root) {
|
|
|
377
1181
|
renderAdminPanel();
|
|
378
1182
|
};
|
|
379
1183
|
|
|
1184
|
+
const handleAdminSearchSubmit = async () => {
|
|
1185
|
+
if (state.adminBusy) return;
|
|
1186
|
+
|
|
1187
|
+
const q = normalizeString(ui.adminSearchInput?.value);
|
|
1188
|
+
if (!q) {
|
|
1189
|
+
state.adminSearchPayload = null;
|
|
1190
|
+
renderSearchResults();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
showAdminError('');
|
|
1195
|
+
setAdminBusy(true);
|
|
1196
|
+
try {
|
|
1197
|
+
const query = new URLSearchParams({ q, limit: '12' }).toString();
|
|
1198
|
+
const payload = await fetchJson(`${adminSearchApiPath}?${query}`, { method: 'GET' });
|
|
1199
|
+
state.adminSearchPayload = payload?.data || null;
|
|
1200
|
+
state.adminOpsMessage = `Busca concluída para "${q}".`;
|
|
1201
|
+
renderSearchResults();
|
|
1202
|
+
setText(ui.adminOpsStatus, state.adminOpsMessage);
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
showAdminError(error?.message || 'Falha ao buscar dados.');
|
|
1205
|
+
} finally {
|
|
1206
|
+
setAdminBusy(false);
|
|
1207
|
+
renderAdminPanel();
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
const downloadBlob = (blob, filename) => {
|
|
1212
|
+
const objectUrl = window.URL.createObjectURL(blob);
|
|
1213
|
+
const link = document.createElement('a');
|
|
1214
|
+
link.href = objectUrl;
|
|
1215
|
+
link.download = filename;
|
|
1216
|
+
document.body.appendChild(link);
|
|
1217
|
+
link.click();
|
|
1218
|
+
link.remove();
|
|
1219
|
+
window.setTimeout(() => {
|
|
1220
|
+
window.URL.revokeObjectURL(objectUrl);
|
|
1221
|
+
}, 250);
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const extractFilenameFromDisposition = (contentDisposition, fallbackName) => {
|
|
1225
|
+
const source = normalizeString(contentDisposition);
|
|
1226
|
+
if (!source) return fallbackName;
|
|
1227
|
+
const utf8Match = source.match(/filename\*=UTF-8''([^;]+)/i);
|
|
1228
|
+
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
|
|
1229
|
+
const plainMatch = source.match(/filename="?([^"]+)"?/i);
|
|
1230
|
+
if (plainMatch?.[1]) return plainMatch[1];
|
|
1231
|
+
return fallbackName;
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
const handleAdminExport = async ({ type = 'metrics', format = 'json' } = {}) => {
|
|
1235
|
+
if (state.adminBusy) return;
|
|
1236
|
+
|
|
1237
|
+
const normalizedType = normalizeString(type || 'metrics').toLowerCase();
|
|
1238
|
+
const normalizedFormat = normalizeString(format || 'json').toLowerCase();
|
|
1239
|
+
const fallbackName = `admin-${normalizedType}-${Date.now()}.${normalizedFormat === 'csv' ? 'csv' : 'json'}`;
|
|
1240
|
+
|
|
1241
|
+
showAdminError('');
|
|
1242
|
+
setAdminBusy(true);
|
|
1243
|
+
try {
|
|
1244
|
+
const query = new URLSearchParams({
|
|
1245
|
+
type: normalizedType,
|
|
1246
|
+
format: normalizedFormat,
|
|
1247
|
+
}).toString();
|
|
1248
|
+
const response = await fetchWithAuth(`${adminExportApiPath}?${query}`, { method: 'GET' });
|
|
1249
|
+
|
|
1250
|
+
if (normalizedFormat === 'csv') {
|
|
1251
|
+
const blob = await response.blob();
|
|
1252
|
+
const contentDisposition = response.headers.get('content-disposition');
|
|
1253
|
+
const filename = extractFilenameFromDisposition(contentDisposition, fallbackName);
|
|
1254
|
+
downloadBlob(blob, filename);
|
|
1255
|
+
} else {
|
|
1256
|
+
const payload = await response.json().catch(() => ({}));
|
|
1257
|
+
const blob = new Blob([JSON.stringify(payload?.data || payload || {}, null, 2)], { type: 'application/json; charset=utf-8' });
|
|
1258
|
+
downloadBlob(blob, fallbackName);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
state.adminOpsMessage = `Exportação ${normalizedType.toUpperCase()} (${normalizedFormat.toUpperCase()}) concluída.`;
|
|
1262
|
+
setText(ui.adminOpsStatus, state.adminOpsMessage);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
showAdminError(error?.message || 'Falha ao exportar dados.');
|
|
1265
|
+
} finally {
|
|
1266
|
+
setAdminBusy(false);
|
|
1267
|
+
renderAdminPanel();
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
|
|
380
1271
|
const handleLogout = async () => {
|
|
381
1272
|
if (!ui.logoutBtn) return;
|
|
382
1273
|
ui.logoutBtn.disabled = true;
|
|
@@ -397,6 +1288,7 @@ if (root) {
|
|
|
397
1288
|
|
|
398
1289
|
setText(ui.status, 'Validando sua sessão...');
|
|
399
1290
|
showError('');
|
|
1291
|
+
setAdminMetricsDefaults();
|
|
400
1292
|
|
|
401
1293
|
let sessionData = null;
|
|
402
1294
|
try {
|
|
@@ -456,5 +1348,43 @@ if (root) {
|
|
|
456
1348
|
});
|
|
457
1349
|
}
|
|
458
1350
|
|
|
1351
|
+
if (ui.adminSearchForm) {
|
|
1352
|
+
ui.adminSearchForm.addEventListener('submit', (event) => {
|
|
1353
|
+
event.preventDefault();
|
|
1354
|
+
void handleAdminSearchSubmit();
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (ui.adminOpButtons.length) {
|
|
1359
|
+
for (const button of ui.adminOpButtons) {
|
|
1360
|
+
button.addEventListener('click', () => {
|
|
1361
|
+
const action = normalizeString(button.dataset.adminOpAction);
|
|
1362
|
+
if (!action) return;
|
|
1363
|
+
void handleAdminOpsAction(action);
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (ui.adminExportMetricsJsonBtn) {
|
|
1369
|
+
ui.adminExportMetricsJsonBtn.addEventListener('click', () => {
|
|
1370
|
+
void handleAdminExport({ type: 'metrics', format: 'json' });
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
if (ui.adminExportMetricsCsvBtn) {
|
|
1374
|
+
ui.adminExportMetricsCsvBtn.addEventListener('click', () => {
|
|
1375
|
+
void handleAdminExport({ type: 'metrics', format: 'csv' });
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
if (ui.adminExportEventsJsonBtn) {
|
|
1379
|
+
ui.adminExportEventsJsonBtn.addEventListener('click', () => {
|
|
1380
|
+
void handleAdminExport({ type: 'events', format: 'json' });
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
if (ui.adminExportEventsCsvBtn) {
|
|
1384
|
+
ui.adminExportEventsCsvBtn.addEventListener('click', () => {
|
|
1385
|
+
void handleAdminExport({ type: 'events', format: 'csv' });
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
459
1389
|
void init();
|
|
460
1390
|
}
|