@kaikybrofc/omnizap-system 2.3.0 → 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 +13 -13
- package/app/controllers/messageController.js +473 -255
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +25 -5
- package/app/observability/metrics.js +6 -3
- package/app/services/googleWebLinkService.js +77 -0
- package/database/index.js +3 -0
- package/database/migrations/20260228_0027_web_visit_event.sql +15 -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 +53 -59
- package/public/js/apps/homeApp.js +259 -61
- package/public/js/apps/loginApp.js +184 -29
- package/public/js/apps/stickersAdminApp.js +3 -9
- package/public/js/apps/stickersApp.js +0 -28
- package/public/js/apps/userApp.js +1160 -14
- package/public/js/apps/userProfileApp.js +244 -0
- package/public/licenca/index.html +98 -2
- package/public/login/index.html +430 -100
- package/public/termos-de-uso/index.html +245 -25
- package/public/user/index.html +3 -1
- package/public/user/systemadm/index.html +774 -0
- package/server/auth/googleWebAuth/googleWebAuthService.js +614 -0
- package/server/controllers/stickerCatalog/nonCatalogHandlers.js +208 -0
- package/server/controllers/stickerCatalogController.js +1350 -924
- 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, 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,57 @@ 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
|
+
|
|
35
|
+
adminPanel: document.getElementById('user-admin-panel'),
|
|
36
|
+
adminRole: document.getElementById('user-admin-role'),
|
|
37
|
+
adminStatus: document.getElementById('user-admin-status'),
|
|
38
|
+
adminError: document.getElementById('user-admin-error'),
|
|
39
|
+
adminUnlockForm: document.getElementById('user-admin-unlock-form'),
|
|
40
|
+
adminPassword: document.getElementById('user-admin-password'),
|
|
41
|
+
adminUnlockBtn: document.getElementById('user-admin-unlock-btn'),
|
|
42
|
+
adminOverview: document.getElementById('user-admin-overview'),
|
|
43
|
+
adminRefreshBtn: document.getElementById('user-admin-refresh-btn'),
|
|
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'),
|
|
51
|
+
adminTotalPacks: document.getElementById('user-admin-total-packs'),
|
|
52
|
+
adminTotalStickers: document.getElementById('user-admin-total-stickers'),
|
|
53
|
+
adminActiveBans: document.getElementById('user-admin-active-bans'),
|
|
54
|
+
adminKnownUsers: document.getElementById('user-admin-known-users'),
|
|
55
|
+
adminActiveSessions: document.getElementById('user-admin-active-sessions'),
|
|
56
|
+
adminVisits24h: document.getElementById('user-admin-visits-24h'),
|
|
57
|
+
adminVisits7d: document.getElementById('user-admin-visits-7d'),
|
|
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]')),
|
|
34
85
|
};
|
|
35
86
|
|
|
36
87
|
const state = {
|
|
@@ -38,17 +89,35 @@ if (root) {
|
|
|
38
89
|
stickersPath: String(root.dataset.stickersPath || DEFAULT_STICKERS_PATH).trim() || DEFAULT_STICKERS_PATH,
|
|
39
90
|
loginPath: String(root.dataset.loginPath || DEFAULT_LOGIN_PATH).trim() || DEFAULT_LOGIN_PATH,
|
|
40
91
|
botPhone: '',
|
|
92
|
+
adminBusy: false,
|
|
93
|
+
adminStatusPayload: null,
|
|
94
|
+
adminOverviewPayload: null,
|
|
95
|
+
adminSearchPayload: null,
|
|
96
|
+
adminOpsMessage: '',
|
|
41
97
|
};
|
|
42
98
|
|
|
43
99
|
const sessionApiPath = `${state.apiBasePath}/auth/google/session`;
|
|
44
100
|
const myProfileApiPath = `${state.apiBasePath}/me`;
|
|
45
101
|
const botContactApiPath = `${state.apiBasePath}/bot-contact`;
|
|
102
|
+
const adminSessionApiPath = `${state.apiBasePath}/admin/session`;
|
|
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`;
|
|
46
110
|
|
|
47
111
|
const setText = (el, value) => {
|
|
48
112
|
if (!el) return;
|
|
49
113
|
el.textContent = String(value || '');
|
|
50
114
|
};
|
|
51
115
|
|
|
116
|
+
const clearNode = (el) => {
|
|
117
|
+
if (!el) return;
|
|
118
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
119
|
+
};
|
|
120
|
+
|
|
52
121
|
const showError = (message) => {
|
|
53
122
|
if (!ui.error) return;
|
|
54
123
|
const safeMessage = String(message || '').trim();
|
|
@@ -56,7 +125,16 @@ if (root) {
|
|
|
56
125
|
if (safeMessage) ui.error.textContent = safeMessage;
|
|
57
126
|
};
|
|
58
127
|
|
|
128
|
+
const showAdminError = (message) => {
|
|
129
|
+
if (!ui.adminError) return;
|
|
130
|
+
const safeMessage = String(message || '').trim();
|
|
131
|
+
ui.adminError.hidden = !safeMessage;
|
|
132
|
+
if (safeMessage) ui.adminError.textContent = safeMessage;
|
|
133
|
+
};
|
|
134
|
+
|
|
59
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));
|
|
60
138
|
|
|
61
139
|
const formatPhone = (digits) => {
|
|
62
140
|
const value = normalizeDigits(digits);
|
|
@@ -76,9 +154,28 @@ if (root) {
|
|
|
76
154
|
return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(ms));
|
|
77
155
|
};
|
|
78
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
|
+
|
|
79
175
|
const buildLoginRedirectUrl = () => {
|
|
80
176
|
const loginUrl = new URL(state.loginPath, window.location.origin);
|
|
81
|
-
|
|
177
|
+
const nextPath = `${window.location.pathname || '/user/systemadm/'}${window.location.search || ''}`;
|
|
178
|
+
loginUrl.searchParams.set('next', nextPath);
|
|
82
179
|
return `${loginUrl.pathname}${loginUrl.search}`;
|
|
83
180
|
};
|
|
84
181
|
|
|
@@ -98,6 +195,7 @@ if (root) {
|
|
|
98
195
|
credentials: 'include',
|
|
99
196
|
...init,
|
|
100
197
|
});
|
|
198
|
+
|
|
101
199
|
let payload = null;
|
|
102
200
|
try {
|
|
103
201
|
payload = await response.json();
|
|
@@ -113,24 +211,190 @@ if (root) {
|
|
|
113
211
|
return payload || {};
|
|
114
212
|
};
|
|
115
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
|
+
|
|
116
234
|
const redirectToLogin = () => {
|
|
117
235
|
window.location.assign(buildLoginRedirectUrl());
|
|
118
236
|
};
|
|
119
237
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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));
|
|
126
340
|
}
|
|
127
|
-
|
|
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();
|
|
128
392
|
};
|
|
129
393
|
|
|
130
394
|
const renderSession = (sessionData) => {
|
|
131
395
|
const user = sessionData?.user || {};
|
|
132
|
-
const ownerPhone =
|
|
133
|
-
const ownerJid =
|
|
396
|
+
const ownerPhone = normalizeString(sessionData?.owner_phone);
|
|
397
|
+
const ownerJid = normalizeString(sessionData?.owner_jid);
|
|
134
398
|
|
|
135
399
|
setText(ui.name, user?.name || 'Conta Google');
|
|
136
400
|
setText(ui.email, user?.email || 'Email não disponível');
|
|
@@ -143,7 +407,7 @@ if (root) {
|
|
|
143
407
|
}
|
|
144
408
|
|
|
145
409
|
if (ui.avatar) {
|
|
146
|
-
const picture =
|
|
410
|
+
const picture = normalizeString(user?.picture) || FALLBACK_AVATAR;
|
|
147
411
|
ui.avatar.src = picture;
|
|
148
412
|
ui.avatar.onerror = () => {
|
|
149
413
|
ui.avatar.src = FALLBACK_AVATAR;
|
|
@@ -151,7 +415,7 @@ if (root) {
|
|
|
151
415
|
}
|
|
152
416
|
|
|
153
417
|
setText(ui.ownerJid, ownerJid || 'n/d');
|
|
154
|
-
setText(ui.googleSub,
|
|
418
|
+
setText(ui.googleSub, normalizeString(user?.sub) || 'n/d');
|
|
155
419
|
setText(ui.expiresAt, formatDateTime(sessionData?.expires_at));
|
|
156
420
|
|
|
157
421
|
if (ui.profile) ui.profile.hidden = false;
|
|
@@ -162,7 +426,7 @@ if (root) {
|
|
|
162
426
|
const renderPackMetrics = (payload) => {
|
|
163
427
|
const data = payload?.data || {};
|
|
164
428
|
const packs = Array.isArray(data?.packs) ? data.packs : [];
|
|
165
|
-
const stats = data?.stats
|
|
429
|
+
const stats = isObject(data?.stats) ? data.stats : {};
|
|
166
430
|
|
|
167
431
|
let stickers = 0;
|
|
168
432
|
let downloads = 0;
|
|
@@ -181,6 +445,829 @@ if (root) {
|
|
|
181
445
|
if (ui.grid) ui.grid.hidden = false;
|
|
182
446
|
};
|
|
183
447
|
|
|
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');
|
|
454
|
+
setText(ui.adminTotalPacks, '0');
|
|
455
|
+
setText(ui.adminTotalStickers, '0');
|
|
456
|
+
setText(ui.adminActiveBans, '0');
|
|
457
|
+
setText(ui.adminKnownUsers, '0');
|
|
458
|
+
setText(ui.adminActiveSessions, '0');
|
|
459
|
+
setText(ui.adminVisits24h, '0');
|
|
460
|
+
setText(ui.adminVisits7d, '0');
|
|
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');
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const renderAdminOverview = () => {
|
|
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.');
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
const renderAdminPanel = () => {
|
|
1053
|
+
if (!ui.adminPanel) return;
|
|
1054
|
+
|
|
1055
|
+
const enabled = state.adminStatusPayload?.enabled !== false;
|
|
1056
|
+
const authenticated = isAdminAuthenticated();
|
|
1057
|
+
const eligible = isAdminEligible();
|
|
1058
|
+
const role = resolveAdminRole();
|
|
1059
|
+
|
|
1060
|
+
if (!enabled || (!eligible && !authenticated)) {
|
|
1061
|
+
ui.adminPanel.hidden = true;
|
|
1062
|
+
if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = true;
|
|
1063
|
+
if (ui.adminOverview) ui.adminOverview.hidden = true;
|
|
1064
|
+
showAdminError('');
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
ui.adminPanel.hidden = false;
|
|
1069
|
+
setText(ui.adminRole, formatAdminRole(role));
|
|
1070
|
+
|
|
1071
|
+
if (authenticated) {
|
|
1072
|
+
setText(ui.adminStatus, `Sessão admin ativa como ${formatAdminRole(role)}. Ferramentas operacionais liberadas abaixo.`);
|
|
1073
|
+
if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = true;
|
|
1074
|
+
if (ui.adminOverview) ui.adminOverview.hidden = false;
|
|
1075
|
+
renderAdminOverview();
|
|
1076
|
+
} else {
|
|
1077
|
+
setText(ui.adminStatus, `Conta elegível para admin (${formatAdminRole(role)}). Informe a senha para liberar os dados sensíveis.`);
|
|
1078
|
+
if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = false;
|
|
1079
|
+
if (ui.adminOverview) ui.adminOverview.hidden = true;
|
|
1080
|
+
setAdminMetricsDefaults();
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
setAdminBusy(state.adminBusy);
|
|
1084
|
+
};
|
|
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
|
+
|
|
1096
|
+
const loadAdminStatus = async () => {
|
|
1097
|
+
const payload = await fetchJson(adminSessionApiPath, { method: 'GET' });
|
|
1098
|
+
state.adminStatusPayload = payload?.data || null;
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
const loadAdminOverview = async () => {
|
|
1102
|
+
if (!isAdminAuthenticated()) {
|
|
1103
|
+
state.adminOverviewPayload = null;
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const payload = await fetchJson(adminOverviewApiPath, { method: 'GET' });
|
|
1107
|
+
state.adminOverviewPayload = payload?.data || null;
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const refreshAdminArea = async ({ keepCurrentError = false } = {}) => {
|
|
1111
|
+
if (!keepCurrentError) showAdminError('');
|
|
1112
|
+
try {
|
|
1113
|
+
await loadAdminStatus();
|
|
1114
|
+
await loadAdminOverview();
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
if (error?.statusCode === 404) {
|
|
1117
|
+
state.adminStatusPayload = { enabled: false };
|
|
1118
|
+
state.adminOverviewPayload = null;
|
|
1119
|
+
} else {
|
|
1120
|
+
showAdminError(error?.message || 'Falha ao carregar área admin.');
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
renderAdminPanel();
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
const handleAdminUnlock = async () => {
|
|
1127
|
+
const password = normalizeString(ui.adminPassword?.value);
|
|
1128
|
+
if (!password) {
|
|
1129
|
+
showAdminError('Informe a senha do painel admin.');
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (state.adminBusy) return;
|
|
1133
|
+
|
|
1134
|
+
showAdminError('');
|
|
1135
|
+
setAdminBusy(true);
|
|
1136
|
+
try {
|
|
1137
|
+
const payload = await fetchJson(adminSessionApiPath, {
|
|
1138
|
+
method: 'POST',
|
|
1139
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
1140
|
+
body: JSON.stringify({ password }),
|
|
1141
|
+
});
|
|
1142
|
+
state.adminStatusPayload = payload?.data || null;
|
|
1143
|
+
state.adminOpsMessage = '';
|
|
1144
|
+
state.adminSearchPayload = null;
|
|
1145
|
+
if (ui.adminPassword) ui.adminPassword.value = '';
|
|
1146
|
+
await loadAdminOverview();
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
showAdminError(error?.message || 'Falha ao desbloquear área admin.');
|
|
1149
|
+
await loadAdminStatus().catch(() => {});
|
|
1150
|
+
state.adminOverviewPayload = null;
|
|
1151
|
+
} finally {
|
|
1152
|
+
setAdminBusy(false);
|
|
1153
|
+
renderAdminPanel();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const handleAdminLogout = async () => {
|
|
1158
|
+
if (state.adminBusy) return;
|
|
1159
|
+
showAdminError('');
|
|
1160
|
+
setAdminBusy(true);
|
|
1161
|
+
try {
|
|
1162
|
+
await fetchJson(adminSessionApiPath, { method: 'DELETE' });
|
|
1163
|
+
} catch {
|
|
1164
|
+
// no-op
|
|
1165
|
+
}
|
|
1166
|
+
state.adminOverviewPayload = null;
|
|
1167
|
+
state.adminSearchPayload = null;
|
|
1168
|
+
state.adminOpsMessage = '';
|
|
1169
|
+
await loadAdminStatus().catch(() => {
|
|
1170
|
+
state.adminStatusPayload = null;
|
|
1171
|
+
});
|
|
1172
|
+
setAdminBusy(false);
|
|
1173
|
+
renderAdminPanel();
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
const handleAdminRefresh = async () => {
|
|
1177
|
+
if (state.adminBusy) return;
|
|
1178
|
+
setAdminBusy(true);
|
|
1179
|
+
await refreshAdminArea({ keepCurrentError: false });
|
|
1180
|
+
setAdminBusy(false);
|
|
1181
|
+
renderAdminPanel();
|
|
1182
|
+
};
|
|
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
|
+
|
|
184
1271
|
const handleLogout = async () => {
|
|
185
1272
|
if (!ui.logoutBtn) return;
|
|
186
1273
|
ui.logoutBtn.disabled = true;
|
|
@@ -201,6 +1288,7 @@ if (root) {
|
|
|
201
1288
|
|
|
202
1289
|
setText(ui.status, 'Validando sua sessão...');
|
|
203
1290
|
showError('');
|
|
1291
|
+
setAdminMetricsDefaults();
|
|
204
1292
|
|
|
205
1293
|
let sessionData = null;
|
|
206
1294
|
try {
|
|
@@ -218,6 +1306,7 @@ if (root) {
|
|
|
218
1306
|
|
|
219
1307
|
renderSession(sessionData);
|
|
220
1308
|
await loadBotPhone();
|
|
1309
|
+
await refreshAdminArea();
|
|
221
1310
|
|
|
222
1311
|
try {
|
|
223
1312
|
const myProfilePayload = await fetchJson(myProfileApiPath, { method: 'GET' });
|
|
@@ -240,5 +1329,62 @@ if (root) {
|
|
|
240
1329
|
});
|
|
241
1330
|
}
|
|
242
1331
|
|
|
1332
|
+
if (ui.adminUnlockForm) {
|
|
1333
|
+
ui.adminUnlockForm.addEventListener('submit', (event) => {
|
|
1334
|
+
event.preventDefault();
|
|
1335
|
+
void handleAdminUnlock();
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (ui.adminLogoutBtn) {
|
|
1340
|
+
ui.adminLogoutBtn.addEventListener('click', () => {
|
|
1341
|
+
void handleAdminLogout();
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (ui.adminRefreshBtn) {
|
|
1346
|
+
ui.adminRefreshBtn.addEventListener('click', () => {
|
|
1347
|
+
void handleAdminRefresh();
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
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
|
+
|
|
243
1389
|
void init();
|
|
244
1390
|
}
|