@kaikybrofc/omnizap-system 2.2.8 → 2.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +194 -0
  3. package/docs/seo/satellite-page-template.md +89 -0
  4. package/docs/seo/satellite-pages-phase1.json +486 -0
  5. package/package.json +3 -1
  6. package/public/api-docs/index.html +78 -22
  7. package/public/bot-whatsapp-para-grupo/index.html +276 -0
  8. package/public/bot-whatsapp-sem-programar/index.html +276 -0
  9. package/public/comandos/index.html +413 -0
  10. package/public/como-automatizar-avisos-no-whatsapp/index.html +276 -0
  11. package/public/como-criar-comandos-whatsapp/index.html +276 -0
  12. package/public/como-evitar-spam-no-whatsapp/index.html +276 -0
  13. package/public/como-moderar-grupo-whatsapp/index.html +276 -0
  14. package/public/como-organizar-comunidade-whatsapp/index.html +276 -0
  15. package/public/css/github-project-panel.css +8 -8
  16. package/public/css/stickers-admin.css +31 -31
  17. package/public/css/styles.css +17 -16
  18. package/public/index.html +701 -1181
  19. package/public/js/apps/apiDocsApp.js +39 -6
  20. package/public/js/apps/homeApp.js +157 -410
  21. package/public/js/apps/stickersApp.js +42 -0
  22. package/public/licenca/index.html +9 -9
  23. package/public/login/index.html +26 -22
  24. package/public/melhor-bot-whatsapp-para-grupos/index.html +276 -0
  25. package/public/sitemap.xml +45 -0
  26. package/public/stickers/create/index.html +7 -6
  27. package/public/stickers/index.html +72 -5
  28. package/public/termos-de-uso/index.html +10 -10
  29. package/public/user/index.html +25 -21
  30. package/scripts/generate-seo-satellite-pages.mjs +434 -0
  31. package/server/controllers/stickerCatalogController.js +341 -700
  32. package/kaikybrofc-omnizap-system-2.2.7.tgz +0 -0
  33. package/kaikybrofc-omnizap-system-2.2.8.tgz +0 -0
@@ -6,45 +6,48 @@ const shortNum = (value) =>
6
6
  maximumFractionDigits: Number(value) >= 1000 ? 1 : 0,
7
7
  }).format(Math.max(0, Number(value) || 0));
8
8
 
9
- const animateCountUp = (element, value, formatter = shortNum, durationMs = 850) => {
9
+ const animateCountUp = (element, value, durationMs = 780) => {
10
10
  if (!element) return;
11
+
11
12
  const target = Math.max(0, Number(value) || 0);
12
13
  if (!Number.isFinite(target)) {
13
- element.textContent = formatter(0);
14
+ element.textContent = shortNum(0);
14
15
  return;
15
16
  }
16
17
 
17
18
  if (typeof requestAnimationFrame !== 'function' || typeof performance === 'undefined') {
19
+ element.textContent = shortNum(target);
18
20
  element.dataset.value = String(target);
19
- element.textContent = formatter(target);
20
21
  return;
21
22
  }
22
23
 
23
24
  const previous = Number(element.dataset.value || 0);
24
25
  const start = Number.isFinite(previous) ? previous : 0;
25
26
  const delta = target - start;
26
- const startTime = performance.now();
27
- const easeOut = (t) => 1 - Math.pow(1 - t, 3);
27
+ const startAt = performance.now();
28
28
 
29
29
  const tick = (now) => {
30
- const progress = Math.min(1, (now - startTime) / durationMs);
31
- const eased = easeOut(progress);
32
- const currentValue = start + delta * eased;
33
- element.textContent = formatter(currentValue);
34
- if (progress < 1) requestAnimationFrame(tick);
30
+ const progress = Math.min(1, (now - startAt) / durationMs);
31
+ const eased = 1 - Math.pow(1 - progress, 3);
32
+ const current = start + delta * eased;
33
+ element.textContent = shortNum(current);
34
+ if (progress < 1) {
35
+ requestAnimationFrame(tick);
36
+ return;
37
+ }
38
+ element.dataset.value = String(target);
35
39
  };
36
40
 
37
- element.dataset.value = String(target);
38
41
  requestAnimationFrame(tick);
39
42
  };
40
43
 
41
- const runAfterLoadIdle = (callback, { delayMs = 0, timeoutMs = 2200 } = {}) => {
44
+ const runAfterLoadIdle = (callback, { delayMs = 0, timeoutMs = 1800 } = {}) => {
42
45
  let cancelled = false;
43
- let loadListener = null;
44
46
  let timeoutId = null;
45
47
  let idleId = null;
48
+ let loadHandler = null;
46
49
 
47
- const invoke = () => {
50
+ const run = () => {
48
51
  if (cancelled) return;
49
52
  callback();
50
53
  };
@@ -52,313 +55,120 @@ const runAfterLoadIdle = (callback, { delayMs = 0, timeoutMs = 2200 } = {}) => {
52
55
  const schedule = () => {
53
56
  if (cancelled) return;
54
57
 
55
- const queuedInvoke = () => {
58
+ const invoke = () => {
56
59
  if (cancelled) return;
57
60
  if (delayMs > 0) {
58
- timeoutId = window.setTimeout(invoke, delayMs);
61
+ timeoutId = window.setTimeout(run, delayMs);
59
62
  return;
60
63
  }
61
- invoke();
64
+ run();
62
65
  };
63
66
 
64
67
  if (typeof window.requestIdleCallback === 'function') {
65
- idleId = window.requestIdleCallback(queuedInvoke, { timeout: timeoutMs });
68
+ idleId = window.requestIdleCallback(invoke, { timeout: timeoutMs });
66
69
  return;
67
70
  }
68
71
 
69
- timeoutId = window.setTimeout(queuedInvoke, Math.min(350, delayMs || 120));
72
+ timeoutId = window.setTimeout(invoke, Math.min(240, delayMs || 120));
70
73
  };
71
74
 
72
75
  if (document.readyState === 'complete') {
73
76
  schedule();
74
77
  } else {
75
- loadListener = () => schedule();
76
- window.addEventListener('load', loadListener, { once: true });
78
+ loadHandler = () => schedule();
79
+ window.addEventListener('load', loadHandler, { once: true });
77
80
  }
78
81
 
79
82
  return () => {
80
83
  cancelled = true;
81
- if (loadListener) {
82
- window.removeEventListener('load', loadListener);
83
- }
84
- if (timeoutId !== null) {
85
- window.clearTimeout(timeoutId);
86
- }
84
+ if (timeoutId !== null) window.clearTimeout(timeoutId);
87
85
  if (idleId !== null && typeof window.cancelIdleCallback === 'function') {
88
86
  window.cancelIdleCallback(idleId);
89
87
  }
90
- };
91
- };
92
-
93
- const initMarketplacePreview = () => {
94
- const proofPacks = document.getElementById('proof-packs');
95
- const proofStickers = document.getElementById('proof-stickers');
96
- const proofDownloads = document.getElementById('proof-downloads');
97
- const proofUsers = document.getElementById('proof-users');
98
- const proofGroups = document.getElementById('proof-groups');
99
- const proofSystem = document.getElementById('proof-system');
100
- const previewStatus = document.getElementById('hero-preview-status');
101
- const previewGrid = document.getElementById('hero-pack-preview');
102
-
103
- if (
104
- !proofPacks
105
- || !proofStickers
106
- || !proofDownloads
107
- || !proofUsers
108
- || !proofGroups
109
- || !proofSystem
110
- || !previewStatus
111
- || !previewGrid
112
- ) {
113
- return null;
114
- }
115
-
116
- const isAutoPack = (pack) =>
117
- Number(pack?.is_auto_pack || pack?.auto_pack || 0) === 1 || /\[auto\]/i.test(String(pack?.name || ''));
118
-
119
- const renderPreviewSkeleton = (count = 6) => {
120
- previewGrid.innerHTML = Array.from({ length: count })
121
- .map(
122
- () =>
123
- '<article class="market-pack is-loading">' +
124
- '<div class="market-pack-skeleton-thumb"></div>' +
125
- '<div class="market-pack-skeleton-body">' +
126
- '<span class="market-pack-skeleton-line"></span>' +
127
- '<span class="market-pack-skeleton-line short"></span>' +
128
- '</div>' +
129
- '</article>',
130
- )
131
- .join('');
132
- };
133
-
134
- const renderPreview = (packs) => {
135
- previewGrid.innerHTML = '';
136
- if (!Array.isArray(packs) || !packs.length) {
137
- previewStatus.textContent = 'Sem packs em destaque no momento.';
138
- return;
139
- }
140
-
141
- previewStatus.textContent = `${packs.length} packs sugeridos agora`;
142
- packs.slice(0, 6).forEach((pack, index) => {
143
- const card = document.createElement('a');
144
- card.className = 'market-pack reveal';
145
- card.href = pack.web_url || `/stickers/${encodeURIComponent(pack.pack_key || '')}`;
146
- card.innerHTML =
147
- `<img class="market-pack-thumb" loading="lazy" decoding="async" fetchpriority="low" width="320" height="320" src="${pack.cover_url || FALLBACK_THUMB_URL}" alt="${String(
148
- pack.name || 'Pack',
149
- ).replace(/"/g, '&quot;')}">` +
150
- (isAutoPack(pack) ? '<span class="market-pack-tag">auto</span>' : '') +
151
- '<div class="market-pack-body">' +
152
- `<p class="market-pack-name">${pack.name || 'Pack sem nome'}</p>` +
153
- `<p class="market-pack-meta">${shortNum(pack.sticker_count || 0)} stickers · ${shortNum(
154
- pack?.engagement?.open_count || 0,
155
- )} aberturas</p>` +
156
- '</div>';
157
- card.style.transitionDelay = `${index * 40}ms`;
158
- previewGrid.appendChild(card);
159
- requestAnimationFrame(() => card.classList.add('in-view'));
160
- });
161
- };
162
-
163
- const loadMarketplaceData = async () => {
164
- try {
165
- const [statsResponse, intentsResponse] = await Promise.all([
166
- fetch('/api/sticker-packs/stats'),
167
- fetch('/api/sticker-packs/intents?limit=12'),
168
- ]);
169
-
170
- const statsPayload = statsResponse.ok ? await statsResponse.json() : null;
171
- const intentsPayload = intentsResponse.ok ? await intentsResponse.json() : null;
172
- const stats = statsPayload?.data || {};
173
- const intents = intentsPayload?.data || {};
174
- const trending = Array.isArray(intents?.em_alta) ? intents.em_alta : [];
175
-
176
- animateCountUp(proofPacks, stats.packs_total || 0);
177
- animateCountUp(proofStickers, stats.stickers_total || 0);
178
- animateCountUp(proofDownloads, stats.downloads_total || 0);
179
- renderPreview(trending);
180
- } catch {
181
- proofPacks.textContent = 'n/d';
182
- proofStickers.textContent = 'n/d';
183
- proofDownloads.textContent = 'n/d';
184
- proofUsers.textContent = 'n/d';
185
- proofGroups.textContent = 'n/d';
186
- proofSystem.textContent = 'n/d';
187
- previewGrid.innerHTML = '';
188
- previewStatus.textContent = 'Não foi possível carregar o preview agora.';
88
+ if (loadHandler) {
89
+ window.removeEventListener('load', loadHandler);
189
90
  }
190
91
  };
191
-
192
- renderPreviewSkeleton(6);
193
- return runAfterLoadIdle(() => {
194
- void loadMarketplaceData();
195
- }, { timeoutMs: 1200 });
196
92
  };
197
93
 
198
- const initRankingPanel = () => {
199
- const summaryEl = document.getElementById('rank-summary');
200
- const listEl = document.getElementById('rank-list');
201
- if (!summaryEl || !listEl) return null;
202
-
203
- const formatDate = (value) => {
204
- const time = Date.parse(String(value || ''));
205
- if (!Number.isFinite(time)) return 'n/d';
206
- return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(time));
207
- };
94
+ const initNavToggle = () => {
95
+ const toggle = document.getElementById('nav-toggle');
96
+ const nav = document.getElementById('main-nav');
97
+ if (!toggle || !nav) return null;
208
98
 
209
- const renderFallback = (message) => {
210
- summaryEl.textContent = message || 'Ranking indisponível no momento.';
211
- listEl.innerHTML = '<li class="rank-item"><span class="rank-name">Sem dados no momento</span><span class="rank-value">--</span></li>';
99
+ const closeMenu = () => {
100
+ nav.classList.remove('open');
101
+ toggle.setAttribute('aria-expanded', 'false');
212
102
  };
213
103
 
214
- const renderRanking = (data) => {
215
- const rows = Array.isArray(data?.rows) ? data.rows : [];
216
- const topType = data?.top_type ? `${data.top_type} (${data.top_type_count || 0})` : 'N/D';
217
- summaryEl.textContent =
218
- `Top recentes: ${Number(data?.top_share_percent || 0).toFixed(
219
- 2,
220
- )}% das ${Number(data?.total_messages || 0)} mensagens · Tipo mais usado: ${topType} · Janela: ${Number(
221
- data?.sample_limit || 0,
222
- )} msgs · Atualizado: ${formatDate(
223
- data?.updated_at,
224
- )}`;
225
-
226
- if (!rows.length) {
227
- renderFallback('Ainda não há mensagens suficientes para o ranking global.');
228
- return;
229
- }
230
-
231
- listEl.innerHTML = '';
232
- rows.forEach((row) => {
233
- const item = document.createElement('li');
234
- item.className = 'rank-item';
235
- const userWrap = document.createElement('span');
236
- userWrap.className = 'rank-user';
237
- const avatar = document.createElement('img');
238
- avatar.className = 'rank-avatar';
239
- avatar.alt = row.display_name || 'Usuário';
240
- avatar.loading = 'lazy';
241
- avatar.decoding = 'async';
242
- avatar.setAttribute('fetchpriority', 'low');
243
- avatar.width = 34;
244
- avatar.height = 34;
245
- avatar.src = row.avatar_url || FALLBACK_THUMB_URL;
246
- avatar.onerror = () => {
247
- avatar.src = FALLBACK_THUMB_URL;
248
- };
249
- const name = document.createElement('span');
250
- name.className = 'rank-name';
251
- name.textContent = `${row.position}. ${row.display_name || 'Desconhecido'}`;
252
- userWrap.append(avatar, name);
253
- const value = document.createElement('span');
254
- value.className = 'rank-value';
255
- value.textContent = `${Number(row.total_messages || 0)} msg · ${Number(row.percent_of_total || 0).toFixed(2)}%`;
256
- item.append(userWrap, value);
257
- listEl.appendChild(item);
258
- });
104
+ const onClick = () => {
105
+ const isOpen = nav.classList.toggle('open');
106
+ toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
259
107
  };
260
108
 
261
- const loadRanking = () =>
262
- fetch('/api/sticker-packs/global-ranking-summary')
263
- .then((response) => {
264
- if (!response.ok) throw new Error('Falha ao carregar ranking');
265
- return response.json();
266
- })
267
- .then((payload) => {
268
- renderRanking(payload?.data || {});
269
- })
270
- .catch(() => {
271
- renderFallback('Ranking indisponível no momento.');
272
- });
109
+ const onNavClick = (event) => {
110
+ const target = event.target;
111
+ if (!(target instanceof Element)) return;
273
112
 
274
- let intervalId = null;
275
- const cancelScheduledLoad = runAfterLoadIdle(() => {
276
- void loadRanking();
277
- intervalId = window.setInterval(() => {
278
- void loadRanking();
279
- }, 10 * 60 * 1000);
280
- }, { delayMs: 450, timeoutMs: 2200 });
113
+ const link = target.closest('a[href]');
114
+ if (!link) return;
281
115
 
282
- return () => {
283
- cancelScheduledLoad();
284
- if (intervalId !== null) {
285
- window.clearInterval(intervalId);
286
- }
116
+ closeMenu();
287
117
  };
288
- };
289
118
 
290
- const initNavToggle = () => {
291
- const toggle = document.getElementById('nav-toggle');
292
- const nav = document.getElementById('main-nav');
293
- if (!toggle || !nav) return null;
119
+ toggle.addEventListener('click', onClick);
120
+ nav.addEventListener('click', onNavClick);
294
121
 
295
- const handleClick = () => {
296
- const isOpen = nav.classList.toggle('open');
297
- toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
122
+ return () => {
123
+ toggle.removeEventListener('click', onClick);
124
+ nav.removeEventListener('click', onNavClick);
298
125
  };
299
-
300
- toggle.addEventListener('click', handleClick);
301
- return () => toggle.removeEventListener('click', handleClick);
302
126
  };
303
127
 
304
128
  const initAuthSession = () => {
305
129
  const authLink = document.getElementById('nav-auth-link');
306
- const schedulerLink = document.getElementById('nav-scheduler-link');
307
130
  const heroLoginCta = document.getElementById('hero-login-cta');
131
+ const finalLoginCta = document.getElementById('final-login-cta');
308
132
  if (!authLink) return null;
309
133
 
310
134
  const clearChildren = (node) => {
311
- while (node.firstChild) {
312
- node.removeChild(node.firstChild);
313
- }
135
+ while (node.firstChild) node.removeChild(node.firstChild);
314
136
  };
315
137
 
316
- const showLoginButton = () => {
317
- document.body.classList.remove('home-authenticated');
138
+ const setLoginState = () => {
318
139
  authLink.classList.remove('nav-user-chip');
319
140
  authLink.href = '/login/';
320
141
  authLink.removeAttribute('title');
321
142
  authLink.removeAttribute('aria-label');
322
143
  clearChildren(authLink);
323
144
 
324
- if (schedulerLink) {
325
- schedulerLink.hidden = false;
326
- schedulerLink.removeAttribute('aria-hidden');
327
- }
145
+ const icon = document.createElement('i');
146
+ icon.className = 'fa-solid fa-right-to-bracket';
147
+ icon.setAttribute('aria-hidden', 'true');
148
+
149
+ authLink.append(icon, document.createTextNode('Entrar'));
328
150
 
329
151
  if (heroLoginCta) {
330
152
  heroLoginCta.hidden = false;
331
153
  heroLoginCta.removeAttribute('aria-hidden');
332
154
  }
333
-
334
- const icon = document.createElement('i');
335
- icon.className = 'fa-solid fa-right-to-bracket icon-inline';
336
- icon.setAttribute('aria-hidden', 'true');
337
- authLink.append(icon, document.createTextNode('Login'));
155
+ if (finalLoginCta) {
156
+ finalLoginCta.hidden = false;
157
+ finalLoginCta.removeAttribute('aria-hidden');
158
+ }
338
159
  };
339
160
 
340
- const showLoggedUser = (sessionData) => {
161
+ const setLoggedState = (sessionData) => {
341
162
  const profile = sessionData?.user || {};
342
163
  const resolvedName = String(profile?.name || profile?.email || 'Conta Google').trim() || 'Conta Google';
343
164
  const resolvedPhoto = String(profile?.picture || '').trim() || FALLBACK_THUMB_URL;
344
165
 
345
- document.body.classList.add('home-authenticated');
346
166
  authLink.classList.add('nav-user-chip');
347
167
  authLink.href = '/user/';
348
168
  authLink.title = `${resolvedName} (sessão ativa)`;
349
169
  authLink.setAttribute('aria-label', `Sessão ativa de ${resolvedName}`);
350
170
  clearChildren(authLink);
351
171
 
352
- if (schedulerLink) {
353
- schedulerLink.hidden = true;
354
- schedulerLink.setAttribute('aria-hidden', 'true');
355
- }
356
-
357
- if (heroLoginCta) {
358
- heroLoginCta.hidden = true;
359
- heroLoginCta.setAttribute('aria-hidden', 'true');
360
- }
361
-
362
172
  const avatarBubble = document.createElement('span');
363
173
  avatarBubble.className = 'nav-user-avatar-bubble';
364
174
 
@@ -368,7 +178,6 @@ const initAuthSession = () => {
368
178
  photo.alt = `Foto de ${resolvedName}`;
369
179
  photo.loading = 'lazy';
370
180
  photo.decoding = 'async';
371
- photo.setAttribute('fetchpriority', 'low');
372
181
  photo.width = 34;
373
182
  photo.height = 34;
374
183
  photo.onerror = () => {
@@ -389,6 +198,15 @@ const initAuthSession = () => {
389
198
 
390
199
  nameBubble.append(icon, name);
391
200
  authLink.append(avatarBubble, nameBubble);
201
+
202
+ if (heroLoginCta) {
203
+ heroLoginCta.hidden = true;
204
+ heroLoginCta.setAttribute('aria-hidden', 'true');
205
+ }
206
+ if (finalLoginCta) {
207
+ finalLoginCta.hidden = true;
208
+ finalLoginCta.setAttribute('aria-hidden', 'true');
209
+ }
392
210
  };
393
211
 
394
212
  return runAfterLoadIdle(() => {
@@ -400,187 +218,119 @@ const initAuthSession = () => {
400
218
  .then((payload) => {
401
219
  const sessionData = payload?.data || {};
402
220
  if (!sessionData?.authenticated || !sessionData?.user?.sub) {
403
- showLoginButton();
221
+ setLoginState();
404
222
  return;
405
223
  }
406
- showLoggedUser(sessionData);
224
+ setLoggedState(sessionData);
407
225
  })
408
226
  .catch(() => {
409
- showLoginButton();
227
+ setLoginState();
410
228
  });
411
- }, { delayMs: 200, timeoutMs: 1400 });
229
+ }, { delayMs: 160, timeoutMs: 1200 });
412
230
  };
413
231
 
414
- const initWhatsappFloatingButton = () => {
415
- const wppButton = document.getElementById('wpp-float');
416
- if (!wppButton) return null;
417
-
418
- const command = 'iniciar';
419
- const normalizeDigits = (value) => String(value || '').replace(/\D+/g, '');
420
- const buildUrl = (phone) => `https://wa.me/${phone}?text=${encodeURIComponent(command)}`;
421
- const applyLink = (phone) => {
422
- const digits = normalizeDigits(phone);
423
- if (!digits) return false;
424
- wppButton.href = buildUrl(digits);
425
- wppButton.hidden = false;
232
+ const resolveSupportUrl = async () => {
233
+ try {
234
+ const response = await fetch('/api/sticker-packs/support');
235
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
236
+ const payload = await response.json();
237
+ const url = String(payload?.data?.url || '').trim();
238
+ return url || '';
239
+ } catch {
240
+ return '';
241
+ }
242
+ };
243
+
244
+ const initAddBotCtas = () => {
245
+ const ctas = Array.from(document.querySelectorAll('[data-add-bot-cta]'));
246
+ const floatButton = document.getElementById('wpp-float');
247
+ if (!ctas.length && !floatButton) return null;
248
+
249
+ const applyLink = (url) => {
250
+ const safeUrl = String(url || '').trim();
251
+ if (!safeUrl) return false;
252
+
253
+ ctas.forEach((element) => {
254
+ element.href = safeUrl;
255
+ element.target = '_blank';
256
+ element.rel = 'noreferrer noopener';
257
+ });
258
+
259
+ if (floatButton) {
260
+ floatButton.href = safeUrl;
261
+ floatButton.hidden = false;
262
+ }
426
263
  return true;
427
264
  };
428
265
 
429
266
  return runAfterLoadIdle(() => {
430
- fetch('/api/sticker-packs?visibility=public&limit=1')
431
- .then((response) => {
432
- if (!response.ok) throw new Error('Falha ao buscar bot');
433
- return response.json();
434
- })
435
- .then((payload) => {
436
- const firstPack = Array.isArray(payload?.data) ? payload.data[0] : null;
437
- const phone = firstPack?.whatsapp?.phone || '';
438
- applyLink(phone);
267
+ resolveSupportUrl()
268
+ .then((url) => {
269
+ const applied = applyLink(url);
270
+ if (!applied && floatButton) {
271
+ floatButton.hidden = true;
272
+ }
439
273
  })
440
274
  .catch(() => {
441
- wppButton.hidden = true;
275
+ if (floatButton) floatButton.hidden = true;
442
276
  });
443
- }, { delayMs: 500, timeoutMs: 2400 });
277
+ }, { delayMs: 120, timeoutMs: 1400 });
444
278
  };
445
279
 
446
- const initSystemSummary = () => {
447
- const cpuEl = document.getElementById('metric-host-cpu');
448
- const memEl = document.getElementById('metric-host-memory');
449
- const uptimeEl = document.getElementById('metric-process-uptime');
450
- const obsEl = document.getElementById('metric-observability');
451
- const proofUsers = document.getElementById('proof-users');
452
- const proofGroups = document.getElementById('proof-groups');
453
- const proofSystem = document.getElementById('proof-system');
454
- if (!cpuEl || !memEl || !uptimeEl || !obsEl) return null;
280
+ const initSocialProof = () => {
281
+ const packsEl = document.getElementById('proof-packs');
282
+ const stickersEl = document.getElementById('proof-stickers');
283
+ const groupsEl = document.getElementById('proof-groups');
284
+ const statusEl = document.getElementById('proof-status');
455
285
 
456
- const normalizeStatus = (value) => {
457
- const normalized = String(value || '')
458
- .trim()
459
- .toLowerCase();
460
- if (!normalized) return 'degraded';
461
- if (['online', 'healthy', 'ok'].includes(normalized)) return 'online';
462
- if (['offline', 'down', 'disconnected'].includes(normalized)) return 'offline';
463
- if (['connecting', 'opening', 'reconnecting'].includes(normalized)) return 'connecting';
464
- return 'degraded';
465
- };
466
-
467
- const formatSystemStatusLabel = (status) => {
468
- if (status === 'online') return 'online';
469
- if (status === 'offline') return 'offline';
470
- if (status === 'connecting') return 'conectando';
471
- return 'instável';
472
- };
286
+ if (!packsEl || !stickersEl || !groupsEl) return null;
473
287
 
474
288
  const setFallback = () => {
475
- cpuEl.textContent = 'CPU host: n/d';
476
- memEl.textContent = 'RAM host: n/d';
477
- uptimeEl.textContent = 'Uptime processo: n/d';
478
- obsEl.textContent = 'Observabilidade: API em /api/sticker-packs';
479
- if (proofUsers) proofUsers.textContent = 'n/d';
480
- if (proofGroups) proofGroups.textContent = 'n/d';
481
- if (proofSystem) {
482
- proofSystem.textContent = 'n/d';
483
- const card = proofSystem.closest('.proof-card');
484
- if (card) card.dataset.status = 'degraded';
485
- }
289
+ packsEl.textContent = 'n/d';
290
+ stickersEl.textContent = 'n/d';
291
+ groupsEl.textContent = 'n/d';
292
+ if (statusEl) statusEl.textContent = 'bot pronto';
486
293
  };
487
294
 
488
- const fmt = (value) => (Number.isFinite(value) ? value.toFixed(2) : 'n/d');
295
+ const normalizeStatus = (value) => {
296
+ const normalized = String(value || '').trim().toLowerCase();
297
+ if (!normalized) return 'pronto';
298
+ if (['online', 'ok', 'healthy'].includes(normalized)) return 'online';
299
+ if (['connecting', 'opening', 'reconnecting'].includes(normalized)) return 'conectando';
300
+ if (['offline', 'down'].includes(normalized)) return 'instável';
301
+ return 'pronto';
302
+ };
489
303
 
490
304
  return runAfterLoadIdle(() => {
491
- fetch('/api/sticker-packs/system-summary')
492
- .then((response) => {
493
- if (!response.ok) throw new Error('Falha ao carregar métricas');
494
- return response.json();
495
- })
496
- .then((payload) => {
497
- const data = payload && payload.data ? payload.data : {};
498
- const host = data.host || {};
499
- const process = data.process || {};
500
- const observability = data.observability || {};
501
- const platform = data.platform || {};
502
- const bot = data.bot || {};
503
- const systemStatus = normalizeStatus(data.system_status || bot.connection_status);
504
-
505
- cpuEl.textContent = 'CPU host: ' + fmt(Number(host.cpu_percent)) + '%';
506
- memEl.textContent =
507
- 'RAM host: ' +
508
- String(host.memory_used || 'n/d') +
509
- ' / ' +
510
- String(host.memory_total || 'n/d') +
511
- ' (' +
512
- fmt(Number(host.memory_percent)) +
513
- '%)';
514
- uptimeEl.textContent = 'Uptime processo: ' + String(process.uptime || 'n/d');
515
-
516
- const lag = Number(observability.lag_p99_ms);
517
- const dbTotal = observability.db_total;
518
- const dbSlow = observability.db_slow;
519
- obsEl.textContent =
520
- 'Lag p99: ' +
521
- (Number.isFinite(lag) ? lag.toFixed(2) + 'ms' : 'n/d') +
522
- ' | DB slow: ' +
523
- (Number.isFinite(Number(dbSlow)) && Number.isFinite(Number(dbTotal)) ? String(dbSlow) + '/' + String(dbTotal) : 'n/d');
524
-
525
- if (proofUsers) animateCountUp(proofUsers, platform.total_users || 0);
526
- if (proofGroups) animateCountUp(proofGroups, platform.total_groups || 0);
527
- if (proofSystem) {
528
- proofSystem.textContent = formatSystemStatusLabel(systemStatus);
529
- const card = proofSystem.closest('.proof-card');
530
- if (card) card.dataset.status = systemStatus;
305
+ Promise.all([
306
+ fetch('/api/sticker-packs/stats'),
307
+ fetch('/api/sticker-packs/system-summary'),
308
+ ])
309
+ .then(async ([statsRes, summaryRes]) => {
310
+ if (!statsRes.ok || !summaryRes.ok) throw new Error('Falha ao carregar prova social');
311
+
312
+ const statsPayload = await statsRes.json();
313
+ const summaryPayload = await summaryRes.json();
314
+
315
+ const stats = statsPayload?.data || {};
316
+ const summary = summaryPayload?.data || {};
317
+
318
+ animateCountUp(packsEl, Number(stats.packs_total || 0));
319
+ animateCountUp(stickersEl, Number(stats.stickers_total || 0));
320
+ animateCountUp(groupsEl, Number(summary?.platform?.total_groups || 0));
321
+
322
+ if (statusEl) {
323
+ statusEl.textContent = `bot ${normalizeStatus(summary?.system_status || summary?.bot?.connection_status)}`;
531
324
  }
532
325
  })
533
326
  .catch(() => {
534
327
  setFallback();
535
328
  });
536
- }, { delayMs: 650, timeoutMs: 2600 });
537
- };
538
-
539
- const initRevealAnimations = () => {
540
- const rawTargets = Array.from(
541
- document.querySelectorAll(
542
- '.market-preview, .section-title, .grid .card, .api, .rank-panel, .final-cta, .hero-stats .chip',
543
- ),
544
- );
545
- if (!rawTargets.length) return null;
546
-
547
- const viewportHeight = window.innerHeight || 0;
548
- const revealTargets = rawTargets.filter((element) => {
549
- const rect = element.getBoundingClientRect();
550
- return rect.top > viewportHeight * 0.7;
551
- });
552
- if (!revealTargets.length) return null;
553
-
554
- revealTargets.forEach((element) => element.classList.add('reveal'));
555
- if (typeof IntersectionObserver !== 'function') {
556
- revealTargets.forEach((element) => element.classList.add('in-view'));
557
- return null;
558
- }
559
-
560
- const observer = new IntersectionObserver(
561
- (entries) => {
562
- entries.forEach((entry) => {
563
- if (entry.isIntersecting) {
564
- entry.target.classList.add('in-view');
565
- observer.unobserve(entry.target);
566
- }
567
- });
568
- },
569
- {
570
- root: null,
571
- threshold: 0.14,
572
- rootMargin: '0px 0px -8% 0px',
573
- },
574
- );
575
-
576
- revealTargets.forEach((element) => observer.observe(element));
577
- return () => observer.disconnect();
329
+ }, { delayMs: 260, timeoutMs: 1500 });
578
330
  };
579
331
 
580
- const registerCleanup = (cleanups, cleanupFn) => {
581
- if (typeof cleanupFn === 'function') {
582
- cleanups.push(cleanupFn);
583
- }
332
+ const registerCleanup = (cleanups, cleanup) => {
333
+ if (typeof cleanup === 'function') cleanups.push(cleanup);
584
334
  };
585
335
 
586
336
  const initHomeApp = () => {
@@ -588,20 +338,17 @@ const initHomeApp = () => {
588
338
  window.__omnizapHomeAppReady = true;
589
339
 
590
340
  const cleanups = [];
591
- registerCleanup(cleanups, initMarketplacePreview());
592
- registerCleanup(cleanups, initRankingPanel());
593
341
  registerCleanup(cleanups, initNavToggle());
594
342
  registerCleanup(cleanups, initAuthSession());
595
- registerCleanup(cleanups, initWhatsappFloatingButton());
596
- registerCleanup(cleanups, initSystemSummary());
597
- registerCleanup(cleanups, initRevealAnimations());
343
+ registerCleanup(cleanups, initAddBotCtas());
344
+ registerCleanup(cleanups, initSocialProof());
598
345
 
599
346
  window.addEventListener(
600
347
  'pagehide',
601
348
  () => {
602
- cleanups.forEach((cleanupFn) => {
349
+ cleanups.forEach((cleanup) => {
603
350
  try {
604
- cleanupFn();
351
+ cleanup();
605
352
  } catch {
606
353
  // no-op
607
354
  }