@kaikybrofc/omnizap-system 2.2.9 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +20 -18
  2. package/app/config/adminIdentity.js +1 -3
  3. package/app/connection/socketController.js +10 -20
  4. package/app/controllers/messageController.js +7 -28
  5. package/app/modules/aiModule/catCommand.js +29 -192
  6. package/app/modules/broadcastModule/noticeCommand.js +28 -97
  7. package/app/modules/gameModule/diceCommand.js +6 -32
  8. package/app/modules/playModule/playCommand.js +57 -258
  9. package/app/modules/quoteModule/quoteCommand.js +2 -4
  10. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1 -13
  11. package/app/modules/statsModule/noMessageCommand.js +16 -84
  12. package/app/modules/statsModule/rankingCommand.js +5 -25
  13. package/app/modules/statsModule/rankingCommon.js +1 -9
  14. package/app/modules/stickerModule/convertToWebp.js +4 -27
  15. package/app/modules/stickerModule/stickerCommand.js +13 -24
  16. package/app/modules/stickerModule/stickerTextCommand.js +13 -25
  17. package/app/modules/stickerPackModule/autoPackCollectorService.js +16 -7
  18. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +20 -36
  19. package/app/modules/stickerPackModule/domainEvents.js +2 -11
  20. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +13 -50
  21. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +2 -15
  22. package/app/modules/stickerPackModule/semanticThemeClusterService.js +14 -41
  23. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +25 -95
  24. package/app/modules/stickerPackModule/stickerAssetRepository.js +12 -31
  25. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +13 -18
  26. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +284 -709
  27. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +27 -106
  28. package/app/modules/stickerPackModule/stickerClassificationService.js +46 -77
  29. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +13 -53
  30. package/app/modules/stickerPackModule/stickerDomainEventBus.js +10 -16
  31. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +13 -34
  32. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +1 -4
  33. package/app/modules/stickerPackModule/stickerObjectStorageService.js +26 -26
  34. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +32 -187
  35. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +6 -15
  36. package/app/modules/stickerPackModule/stickerPackItemRepository.js +6 -32
  37. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +12 -36
  38. package/app/modules/stickerPackModule/stickerPackMessageService.js +12 -40
  39. package/app/modules/stickerPackModule/stickerPackRepository.js +23 -66
  40. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +9 -21
  41. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +10 -40
  42. package/app/modules/stickerPackModule/stickerPackService.js +50 -115
  43. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +2 -21
  44. package/app/modules/stickerPackModule/stickerPackUtils.js +13 -3
  45. package/app/modules/stickerPackModule/stickerStorageService.js +16 -65
  46. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +4 -22
  47. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +14 -29
  48. package/app/modules/systemMetricsModule/pingCommand.js +9 -39
  49. package/app/modules/tiktokModule/tiktokCommand.js +17 -109
  50. package/app/modules/userModule/userCommand.js +2 -88
  51. package/app/observability/metrics.js +5 -16
  52. package/app/services/captchaService.js +1 -6
  53. package/app/services/dbWriteQueue.js +3 -18
  54. package/app/services/featureFlagService.js +2 -8
  55. package/app/services/newsBroadcastService.js +0 -1
  56. package/app/services/queueUtils.js +2 -4
  57. package/app/services/whatsappLoginLinkService.js +7 -9
  58. package/app/store/premiumUserStore.js +1 -2
  59. package/app/utils/antiLink/antiLinkModule.js +3 -233
  60. package/app/utils/logger/loggerModule.js +9 -34
  61. package/app/utils/systemMetrics/systemMetricsModule.js +1 -4
  62. package/database/init.js +1 -8
  63. package/docker-compose.yml +27 -27
  64. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +220 -0
  65. package/docs/seo/satellite-page-template.md +91 -0
  66. package/docs/seo/satellite-pages-phase1.json +349 -0
  67. package/eslint.config.js +2 -15
  68. package/index.js +8 -36
  69. package/ml/clip_classifier/README.md +4 -6
  70. package/observability/alert-rules.yml +12 -12
  71. package/observability/grafana/provisioning/dashboards/dashboards.yml +1 -1
  72. package/package.json +8 -3
  73. package/public/api-docs/index.html +224 -141
  74. package/public/bot-whatsapp-para-grupo/index.html +306 -0
  75. package/public/bot-whatsapp-sem-programar/index.html +306 -0
  76. package/public/comandos/index.html +428 -0
  77. package/public/como-automatizar-avisos-no-whatsapp/index.html +306 -0
  78. package/public/como-criar-comandos-whatsapp/index.html +306 -0
  79. package/public/como-evitar-spam-no-whatsapp/index.html +306 -0
  80. package/public/como-moderar-grupo-whatsapp/index.html +306 -0
  81. package/public/como-organizar-comunidade-whatsapp/index.html +306 -0
  82. package/public/css/github-project-panel.css +20 -15
  83. package/public/css/stickers-admin.css +55 -39
  84. package/public/css/styles.css +37 -29
  85. package/public/index.html +1060 -1417
  86. package/public/js/apps/apiDocsApp.js +36 -153
  87. package/public/js/apps/createPackApp.js +69 -332
  88. package/public/js/apps/homeApp.js +201 -434
  89. package/public/js/apps/loginApp.js +3 -12
  90. package/public/js/apps/stickersAdminApp.js +190 -181
  91. package/public/js/apps/stickersApp.js +507 -1366
  92. package/public/js/catalog.js +11 -74
  93. package/public/js/github-panel/components/ErrorState.js +1 -8
  94. package/public/js/github-panel/components/GithubProjectPanel.js +2 -9
  95. package/public/js/github-panel/components/SkeletonPanel.js +1 -11
  96. package/public/js/github-panel/components/StatCard.js +1 -7
  97. package/public/js/github-panel/vendor/react.js +1 -9
  98. package/public/js/runtime/react-runtime.js +1 -9
  99. package/public/licenca/index.html +104 -86
  100. package/public/login/index.html +315 -321
  101. package/public/melhor-bot-whatsapp-para-grupos/index.html +306 -0
  102. package/public/sitemap.xml +45 -0
  103. package/public/stickers/admin/index.html +14 -19
  104. package/public/stickers/create/index.html +39 -43
  105. package/public/stickers/index.html +97 -41
  106. package/public/termos-de-uso/index.html +142 -115
  107. package/public/user/index.html +347 -346
  108. package/scripts/cache-bust.mjs +5 -24
  109. package/scripts/generate-seo-satellite-pages.mjs +431 -0
  110. package/scripts/run-prettier-all.mjs +25 -0
  111. package/scripts/sticker-catalog-loadtest.mjs +13 -11
  112. package/scripts/sticker-worker-task.mjs +1 -4
  113. package/scripts/sync-readme-snapshot.mjs +3 -2
  114. package/server/controllers/stickerCatalogController.js +407 -704
  115. package/server/http/httpServer.js +2 -10
  116. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +1 -8
  117. package/server/routes/stickerCatalog/catalogHandlers/catalogAuthHttp.js +1 -9
  118. package/server/routes/stickerCatalog/catalogHandlers/catalogPublicHttp.js +10 -11
  119. package/server/routes/stickerCatalog/catalogHandlers/catalogUploadHttp.js +1 -10
  120. package/server/routes/stickerCatalog/catalogRouter.js +11 -13
  121. package/kaikybrofc-omnizap-system-2.2.9.tgz +0 -0
@@ -1,4 +1,40 @@
1
1
  const FALLBACK_THUMB_URL = '/assets/images/brand-logo-128.webp';
2
+ const HOME_BOOTSTRAP_ENDPOINT = '/api/sticker-packs/home-bootstrap';
3
+ const SVG_NS = 'http://www.w3.org/2000/svg';
4
+ let homeBootstrapPayloadPromise = null;
5
+
6
+ const fetchHomeBootstrapPayload = async () => {
7
+ if (!homeBootstrapPayloadPromise) {
8
+ homeBootstrapPayloadPromise = fetch(HOME_BOOTSTRAP_ENDPOINT, { credentials: 'include' })
9
+ .then((response) => {
10
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
11
+ return response.json();
12
+ })
13
+ .then((payload) => payload?.data || {})
14
+ .catch((error) => {
15
+ homeBootstrapPayloadPromise = null;
16
+ throw error;
17
+ });
18
+ }
19
+ return homeBootstrapPayloadPromise;
20
+ };
21
+
22
+ const createIcon = (iconId, className = 'icon') => {
23
+ const wrapper = document.createElement('span');
24
+ wrapper.className = className;
25
+ wrapper.setAttribute('aria-hidden', 'true');
26
+
27
+ const svg = document.createElementNS(SVG_NS, 'svg');
28
+ svg.setAttribute('viewBox', '0 0 24 24');
29
+ svg.setAttribute('focusable', 'false');
30
+
31
+ const use = document.createElementNS(SVG_NS, 'use');
32
+ use.setAttribute('href', `#${iconId}`);
33
+
34
+ svg.appendChild(use);
35
+ wrapper.appendChild(svg);
36
+ return wrapper;
37
+ };
2
38
 
3
39
  const shortNum = (value) =>
4
40
  new Intl.NumberFormat('pt-BR', {
@@ -6,45 +42,48 @@ const shortNum = (value) =>
6
42
  maximumFractionDigits: Number(value) >= 1000 ? 1 : 0,
7
43
  }).format(Math.max(0, Number(value) || 0));
8
44
 
9
- const animateCountUp = (element, value, formatter = shortNum, durationMs = 850) => {
45
+ const animateCountUp = (element, value, durationMs = 780) => {
10
46
  if (!element) return;
47
+
11
48
  const target = Math.max(0, Number(value) || 0);
12
49
  if (!Number.isFinite(target)) {
13
- element.textContent = formatter(0);
50
+ element.textContent = shortNum(0);
14
51
  return;
15
52
  }
16
53
 
17
54
  if (typeof requestAnimationFrame !== 'function' || typeof performance === 'undefined') {
55
+ element.textContent = shortNum(target);
18
56
  element.dataset.value = String(target);
19
- element.textContent = formatter(target);
20
57
  return;
21
58
  }
22
59
 
23
60
  const previous = Number(element.dataset.value || 0);
24
61
  const start = Number.isFinite(previous) ? previous : 0;
25
62
  const delta = target - start;
26
- const startTime = performance.now();
27
- const easeOut = (t) => 1 - Math.pow(1 - t, 3);
63
+ const startAt = performance.now();
28
64
 
29
65
  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);
66
+ const progress = Math.min(1, (now - startAt) / durationMs);
67
+ const eased = 1 - Math.pow(1 - progress, 3);
68
+ const current = start + delta * eased;
69
+ element.textContent = shortNum(current);
70
+ if (progress < 1) {
71
+ requestAnimationFrame(tick);
72
+ return;
73
+ }
74
+ element.dataset.value = String(target);
35
75
  };
36
76
 
37
- element.dataset.value = String(target);
38
77
  requestAnimationFrame(tick);
39
78
  };
40
79
 
41
- const runAfterLoadIdle = (callback, { delayMs = 0, timeoutMs = 2200 } = {}) => {
80
+ const runAfterLoadIdle = (callback, { delayMs = 0, timeoutMs = 1800 } = {}) => {
42
81
  let cancelled = false;
43
- let loadListener = null;
44
82
  let timeoutId = null;
45
83
  let idleId = null;
84
+ let loadHandler = null;
46
85
 
47
- const invoke = () => {
86
+ const run = () => {
48
87
  if (cancelled) return;
49
88
  callback();
50
89
  };
@@ -52,313 +91,118 @@ const runAfterLoadIdle = (callback, { delayMs = 0, timeoutMs = 2200 } = {}) => {
52
91
  const schedule = () => {
53
92
  if (cancelled) return;
54
93
 
55
- const queuedInvoke = () => {
94
+ const invoke = () => {
56
95
  if (cancelled) return;
57
96
  if (delayMs > 0) {
58
- timeoutId = window.setTimeout(invoke, delayMs);
97
+ timeoutId = window.setTimeout(run, delayMs);
59
98
  return;
60
99
  }
61
- invoke();
100
+ run();
62
101
  };
63
102
 
64
103
  if (typeof window.requestIdleCallback === 'function') {
65
- idleId = window.requestIdleCallback(queuedInvoke, { timeout: timeoutMs });
104
+ idleId = window.requestIdleCallback(invoke, { timeout: timeoutMs });
66
105
  return;
67
106
  }
68
107
 
69
- timeoutId = window.setTimeout(queuedInvoke, Math.min(350, delayMs || 120));
108
+ timeoutId = window.setTimeout(invoke, Math.min(240, delayMs || 120));
70
109
  };
71
110
 
72
111
  if (document.readyState === 'complete') {
73
112
  schedule();
74
113
  } else {
75
- loadListener = () => schedule();
76
- window.addEventListener('load', loadListener, { once: true });
114
+ loadHandler = () => schedule();
115
+ window.addEventListener('load', loadHandler, { once: true });
77
116
  }
78
117
 
79
118
  return () => {
80
119
  cancelled = true;
81
- if (loadListener) {
82
- window.removeEventListener('load', loadListener);
83
- }
84
- if (timeoutId !== null) {
85
- window.clearTimeout(timeoutId);
86
- }
120
+ if (timeoutId !== null) window.clearTimeout(timeoutId);
87
121
  if (idleId !== null && typeof window.cancelIdleCallback === 'function') {
88
122
  window.cancelIdleCallback(idleId);
89
123
  }
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.';
124
+ if (loadHandler) {
125
+ window.removeEventListener('load', loadHandler);
189
126
  }
190
127
  };
191
-
192
- renderPreviewSkeleton(6);
193
- return runAfterLoadIdle(() => {
194
- void loadMarketplaceData();
195
- }, { timeoutMs: 1200 });
196
128
  };
197
129
 
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
- };
130
+ const initNavToggle = () => {
131
+ const toggle = document.getElementById('nav-toggle');
132
+ const nav = document.getElementById('main-nav');
133
+ if (!toggle || !nav) return null;
208
134
 
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>';
135
+ const closeMenu = () => {
136
+ nav.classList.remove('open');
137
+ toggle.setAttribute('aria-expanded', 'false');
212
138
  };
213
139
 
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
- });
140
+ const onClick = () => {
141
+ const isOpen = nav.classList.toggle('open');
142
+ toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
259
143
  };
260
144
 
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
- });
145
+ const onNavClick = (event) => {
146
+ const target = event.target;
147
+ if (!(target instanceof Element)) return;
273
148
 
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 });
149
+ const link = target.closest('a[href]');
150
+ if (!link) return;
281
151
 
282
- return () => {
283
- cancelScheduledLoad();
284
- if (intervalId !== null) {
285
- window.clearInterval(intervalId);
286
- }
152
+ closeMenu();
287
153
  };
288
- };
289
154
 
290
- const initNavToggle = () => {
291
- const toggle = document.getElementById('nav-toggle');
292
- const nav = document.getElementById('main-nav');
293
- if (!toggle || !nav) return null;
155
+ toggle.addEventListener('click', onClick);
156
+ nav.addEventListener('click', onNavClick);
294
157
 
295
- const handleClick = () => {
296
- const isOpen = nav.classList.toggle('open');
297
- toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
158
+ return () => {
159
+ toggle.removeEventListener('click', onClick);
160
+ nav.removeEventListener('click', onNavClick);
298
161
  };
299
-
300
- toggle.addEventListener('click', handleClick);
301
- return () => toggle.removeEventListener('click', handleClick);
302
162
  };
303
163
 
304
164
  const initAuthSession = () => {
305
165
  const authLink = document.getElementById('nav-auth-link');
306
- const schedulerLink = document.getElementById('nav-scheduler-link');
307
166
  const heroLoginCta = document.getElementById('hero-login-cta');
167
+ const finalLoginCta = document.getElementById('final-login-cta');
308
168
  if (!authLink) return null;
309
169
 
310
170
  const clearChildren = (node) => {
311
- while (node.firstChild) {
312
- node.removeChild(node.firstChild);
313
- }
171
+ while (node.firstChild) node.removeChild(node.firstChild);
314
172
  };
315
173
 
316
- const showLoginButton = () => {
317
- document.body.classList.remove('home-authenticated');
174
+ const setLoginState = () => {
318
175
  authLink.classList.remove('nav-user-chip');
319
176
  authLink.href = '/login/';
320
177
  authLink.removeAttribute('title');
321
178
  authLink.removeAttribute('aria-label');
322
179
  clearChildren(authLink);
323
180
 
324
- if (schedulerLink) {
325
- schedulerLink.hidden = false;
326
- schedulerLink.removeAttribute('aria-hidden');
327
- }
181
+ const icon = createIcon('icon-login');
182
+
183
+ authLink.append(icon, document.createTextNode('Entrar'));
328
184
 
329
185
  if (heroLoginCta) {
330
186
  heroLoginCta.hidden = false;
331
187
  heroLoginCta.removeAttribute('aria-hidden');
332
188
  }
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'));
189
+ if (finalLoginCta) {
190
+ finalLoginCta.hidden = false;
191
+ finalLoginCta.removeAttribute('aria-hidden');
192
+ }
338
193
  };
339
194
 
340
- const showLoggedUser = (sessionData) => {
195
+ const setLoggedState = (sessionData) => {
341
196
  const profile = sessionData?.user || {};
342
197
  const resolvedName = String(profile?.name || profile?.email || 'Conta Google').trim() || 'Conta Google';
343
198
  const resolvedPhoto = String(profile?.picture || '').trim() || FALLBACK_THUMB_URL;
344
199
 
345
- document.body.classList.add('home-authenticated');
346
200
  authLink.classList.add('nav-user-chip');
347
201
  authLink.href = '/user/';
348
202
  authLink.title = `${resolvedName} (sessão ativa)`;
349
203
  authLink.setAttribute('aria-label', `Sessão ativa de ${resolvedName}`);
350
204
  clearChildren(authLink);
351
205
 
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
206
  const avatarBubble = document.createElement('span');
363
207
  avatarBubble.className = 'nav-user-avatar-bubble';
364
208
 
@@ -368,7 +212,6 @@ const initAuthSession = () => {
368
212
  photo.alt = `Foto de ${resolvedName}`;
369
213
  photo.loading = 'lazy';
370
214
  photo.decoding = 'async';
371
- photo.setAttribute('fetchpriority', 'low');
372
215
  photo.width = 34;
373
216
  photo.height = 34;
374
217
  photo.onerror = () => {
@@ -379,9 +222,7 @@ const initAuthSession = () => {
379
222
  const nameBubble = document.createElement('span');
380
223
  nameBubble.className = 'nav-user-name-bubble';
381
224
 
382
- const icon = document.createElement('i');
383
- icon.className = 'fa-solid fa-user nav-user-icon';
384
- icon.setAttribute('aria-hidden', 'true');
225
+ const icon = createIcon('icon-user', 'icon nav-user-icon');
385
226
 
386
227
  const name = document.createElement('span');
387
228
  name.className = 'nav-user-name';
@@ -389,198 +230,127 @@ const initAuthSession = () => {
389
230
 
390
231
  nameBubble.append(icon, name);
391
232
  authLink.append(avatarBubble, nameBubble);
233
+
234
+ if (heroLoginCta) {
235
+ heroLoginCta.hidden = true;
236
+ heroLoginCta.setAttribute('aria-hidden', 'true');
237
+ }
238
+ if (finalLoginCta) {
239
+ finalLoginCta.hidden = true;
240
+ finalLoginCta.setAttribute('aria-hidden', 'true');
241
+ }
392
242
  };
393
243
 
394
- return runAfterLoadIdle(() => {
395
- fetch('/api/sticker-packs/auth/google/session', { credentials: 'include' })
396
- .then((response) => {
397
- if (!response.ok) throw new Error('Sessão indisponível');
398
- return response.json();
399
- })
400
- .then((payload) => {
401
- const sessionData = payload?.data || {};
402
- if (!sessionData?.authenticated || !sessionData?.user?.sub) {
403
- showLoginButton();
404
- return;
405
- }
406
- showLoggedUser(sessionData);
407
- })
408
- .catch(() => {
409
- showLoginButton();
410
- });
411
- }, { delayMs: 200, timeoutMs: 1400 });
244
+ return runAfterLoadIdle(
245
+ () => {
246
+ fetchHomeBootstrapPayload()
247
+ .then((bootstrapData) => {
248
+ const sessionData = bootstrapData?.session || {};
249
+ if (!sessionData?.authenticated || !sessionData?.user?.sub) {
250
+ setLoginState();
251
+ return;
252
+ }
253
+ setLoggedState(sessionData);
254
+ })
255
+ .catch(() => {
256
+ setLoginState();
257
+ });
258
+ },
259
+ { delayMs: 520, timeoutMs: 1200 },
260
+ );
412
261
  };
413
262
 
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;
263
+ const initAddBotCtas = () => {
264
+ const ctas = Array.from(document.querySelectorAll('[data-add-bot-cta]'));
265
+ const floatButton = document.getElementById('wpp-float');
266
+ if (!ctas.length && !floatButton) return null;
267
+
268
+ const applyLink = (url) => {
269
+ const safeUrl = String(url || '').trim();
270
+ if (!safeUrl) return false;
271
+
272
+ ctas.forEach((element) => {
273
+ element.href = safeUrl;
274
+ element.target = '_blank';
275
+ element.rel = 'noreferrer noopener';
276
+ });
277
+
278
+ if (floatButton) {
279
+ floatButton.href = safeUrl;
280
+ floatButton.hidden = false;
281
+ }
426
282
  return true;
427
283
  };
428
284
 
429
- 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);
439
- })
440
- .catch(() => {
441
- wppButton.hidden = true;
442
- });
443
- }, { delayMs: 500, timeoutMs: 2400 });
285
+ return runAfterLoadIdle(
286
+ () => {
287
+ fetchHomeBootstrapPayload()
288
+ .then((bootstrapData) => {
289
+ const url = String(bootstrapData?.support?.url || '').trim();
290
+ const applied = applyLink(url);
291
+ if (!applied && floatButton) {
292
+ floatButton.hidden = true;
293
+ }
294
+ })
295
+ .catch(() => {
296
+ if (floatButton) floatButton.hidden = true;
297
+ });
298
+ },
299
+ { delayMs: 480, timeoutMs: 1400 },
300
+ );
444
301
  };
445
302
 
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;
303
+ const initSocialProof = () => {
304
+ const packsEl = document.getElementById('proof-packs');
305
+ const stickersEl = document.getElementById('proof-stickers');
306
+ const groupsEl = document.getElementById('proof-groups');
307
+ const statusEl = document.getElementById('proof-status');
308
+
309
+ if (!packsEl || !stickersEl || !groupsEl) return null;
310
+
311
+ const setFallback = () => {
312
+ packsEl.textContent = 'n/d';
313
+ stickersEl.textContent = 'n/d';
314
+ groupsEl.textContent = 'n/d';
315
+ if (statusEl) statusEl.textContent = 'bot pronto';
316
+ };
455
317
 
456
318
  const normalizeStatus = (value) => {
457
319
  const normalized = String(value || '')
458
320
  .trim()
459
321
  .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';
322
+ if (!normalized) return 'pronto';
323
+ if (['online', 'ok', 'healthy'].includes(normalized)) return 'online';
324
+ if (['connecting', 'opening', 'reconnecting'].includes(normalized)) return 'conectando';
325
+ if (['offline', 'down'].includes(normalized)) return 'instável';
326
+ return 'pronto';
465
327
  };
466
328
 
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
- };
473
-
474
- 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
- }
486
- };
487
-
488
- const fmt = (value) => (Number.isFinite(value) ? value.toFixed(2) : 'n/d');
489
-
490
- 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;
531
- }
532
- })
533
- .catch(() => {
534
- setFallback();
535
- });
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',
329
+ return runAfterLoadIdle(
330
+ () => {
331
+ fetchHomeBootstrapPayload()
332
+ .then((bootstrapData) => {
333
+ const stats = bootstrapData?.stats || {};
334
+ const summary = bootstrapData?.system_summary || {};
335
+
336
+ animateCountUp(packsEl, Number(stats.packs_total || 0));
337
+ animateCountUp(stickersEl, Number(stats.stickers_total || 0));
338
+ animateCountUp(groupsEl, Number(summary?.platform?.total_groups || 0));
339
+
340
+ if (statusEl) {
341
+ statusEl.textContent = `bot ${normalizeStatus(summary?.system_status || summary?.bot?.connection_status)}`;
342
+ }
343
+ })
344
+ .catch(() => {
345
+ setFallback();
346
+ });
573
347
  },
348
+ { delayMs: 620, timeoutMs: 1500 },
574
349
  );
575
-
576
- revealTargets.forEach((element) => observer.observe(element));
577
- return () => observer.disconnect();
578
350
  };
579
351
 
580
- const registerCleanup = (cleanups, cleanupFn) => {
581
- if (typeof cleanupFn === 'function') {
582
- cleanups.push(cleanupFn);
583
- }
352
+ const registerCleanup = (cleanups, cleanup) => {
353
+ if (typeof cleanup === 'function') cleanups.push(cleanup);
584
354
  };
585
355
 
586
356
  const initHomeApp = () => {
@@ -588,20 +358,17 @@ const initHomeApp = () => {
588
358
  window.__omnizapHomeAppReady = true;
589
359
 
590
360
  const cleanups = [];
591
- registerCleanup(cleanups, initMarketplacePreview());
592
- registerCleanup(cleanups, initRankingPanel());
593
361
  registerCleanup(cleanups, initNavToggle());
594
362
  registerCleanup(cleanups, initAuthSession());
595
- registerCleanup(cleanups, initWhatsappFloatingButton());
596
- registerCleanup(cleanups, initSystemSummary());
597
- registerCleanup(cleanups, initRevealAnimations());
363
+ registerCleanup(cleanups, initAddBotCtas());
364
+ registerCleanup(cleanups, initSocialProof());
598
365
 
599
366
  window.addEventListener(
600
367
  'pagehide',
601
368
  () => {
602
- cleanups.forEach((cleanupFn) => {
369
+ cleanups.forEach((cleanup) => {
603
370
  try {
604
- cleanupFn();
371
+ cleanup();
605
372
  } catch {
606
373
  // no-op
607
374
  }