@kaikybrofc/omnizap-system 2.3.0 → 2.3.1

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 CHANGED
@@ -51,31 +51,29 @@ Conteúdo incluído no snapshot:
51
51
  Atualização em cache: **30 minutos** por padrão (`README_SUMMARY_CACHE_SECONDS=1800`).
52
52
 
53
53
  <!-- README_SNAPSHOT:START -->
54
-
55
54
  ### Snapshot do Sistema
56
55
 
57
- > Atualizado em `2026-02-28T21:19:25.225Z` | cache `1800s`
56
+ > Atualizado em `2026-02-28T21:56:21.021Z` | cache `1800s`
58
57
 
59
- | Métrica | Valor |
60
- | --------------------- | ------: |
61
- | Usuários (lid_map) | 5.515 |
62
- | Grupos | 116 |
63
- | Packs | 301 |
64
- | Stickers | 7.169 |
65
- | Mensagens registradas | 444.374 |
58
+ | Métrica | Valor |
59
+ | --- | ---: |
60
+ | Usuários (lid_map) | 5.515 |
61
+ | Grupos | 116 |
62
+ | Packs | 301 |
63
+ | Stickers | 7.170 |
64
+ | Mensagens registradas | 444.453 |
66
65
 
67
66
  #### Tipos de mensagem mais usados (amostra: 25.000)
68
-
69
- | Tipo | Total |
70
- | ----------- | -----: |
71
- | `texto` | 16.084 |
72
- | `figurinha` | 4.777 |
73
- | `reacao` | 1.546 |
74
- | `imagem` | 1.339 |
75
- | `outros` | 849 |
76
- | `video` | 215 |
77
- | `audio` | 184 |
78
- | `documento` | 6 |
67
+ | Tipo | Total |
68
+ | --- | ---: |
69
+ | `texto` | 16.074 |
70
+ | `figurinha` | 4.780 |
71
+ | `reacao` | 1.546 |
72
+ | `imagem` | 1.339 |
73
+ | `outros` | 850 |
74
+ | `video` | 215 |
75
+ | `audio` | 190 |
76
+ | `documento` | 6 |
79
77
 
80
78
  <details><summary>Comandos disponíveis (62)</summary>
81
79
 
@@ -19,6 +19,9 @@ const CONSUMER_ENABLED = parseEnvBool(process.env.STICKER_DOMAIN_EVENT_CONSUMER_
19
19
  const STARTUP_DELAY_MS = Math.max(1_000, Number(process.env.STICKER_DOMAIN_EVENT_CONSUMER_STARTUP_DELAY_MS) || 8_000);
20
20
  const POLLER_INTERVAL_MS = Math.max(1_000, Number(process.env.STICKER_DOMAIN_EVENT_CONSUMER_POLLER_INTERVAL_MS) || 2_000);
21
21
  const RETRY_DELAY_SECONDS = Math.max(5, Math.min(3600, Number(process.env.STICKER_DOMAIN_EVENT_CONSUMER_RETRY_DELAY_SECONDS) || 45));
22
+ const CLASSIFICATION_COALESCE_WINDOW_SECONDS = Math.max(30, Math.min(3600, Number(process.env.STICKER_DOMAIN_EVENT_CLASSIFICATION_COALESCE_WINDOW_SECONDS) || 60));
23
+ const CURATION_COALESCE_WINDOW_SECONDS = Math.max(30, Math.min(3600, Number(process.env.STICKER_DOMAIN_EVENT_CURATION_COALESCE_WINDOW_SECONDS) || 60));
24
+ const REBUILD_COALESCE_WINDOW_SECONDS = Math.max(30, Math.min(3600, Number(process.env.STICKER_DOMAIN_EVENT_REBUILD_COALESCE_WINDOW_SECONDS) || 120));
22
25
  const CONSUMER_COHORT_KEY = String(process.env.STICKER_DOMAIN_EVENT_CONSUMER_COHORT_KEY || process.env.HOSTNAME || process.pid).trim() || 'consumer';
23
26
 
24
27
  let startupHandle = null;
@@ -47,6 +50,18 @@ const enqueueTaskSafely = async ({ taskType, payload, priority, idempotencyKey }
47
50
  });
48
51
  };
49
52
 
53
+ const toUnixSeconds = (value) => {
54
+ if (!value) return Math.floor(Date.now() / 1000);
55
+ const numeric = Date.parse(value);
56
+ if (!Number.isFinite(numeric)) return Math.floor(Date.now() / 1000);
57
+ return Math.floor(numeric / 1000);
58
+ };
59
+
60
+ const toWindowBucket = (value, windowSeconds) => {
61
+ const safeWindow = Math.max(1, Math.floor(windowSeconds || 1));
62
+ return Math.floor(toUnixSeconds(value) / safeWindow);
63
+ };
64
+
50
65
  const handleDomainEvent = async (event) => {
51
66
  const eventType = String(event?.event_type || '')
52
67
  .trim()
@@ -55,16 +70,18 @@ const handleDomainEvent = async (event) => {
55
70
  const payload = event?.payload && typeof event.payload === 'object' ? event.payload : {};
56
71
 
57
72
  if (eventType === STICKER_DOMAIN_EVENTS.STICKER_ASSET_CREATED) {
73
+ const coalesceBucket = toWindowBucket(event?.created_at, CLASSIFICATION_COALESCE_WINDOW_SECONDS);
58
74
  await enqueueTaskSafely({
59
75
  taskType: 'classification_cycle',
60
- payload: { reason: 'domain_event', event_type: eventType, aggregate_id: aggregateId },
76
+ payload: { reason: 'domain_event', event_type: eventType, aggregate_id: aggregateId, coalesced: true },
61
77
  priority: 80,
62
- idempotencyKey: `evt:${event.id}:classification_cycle`,
78
+ idempotencyKey: `evt:${eventType}:${coalesceBucket}:classification_cycle`,
63
79
  });
64
80
  return;
65
81
  }
66
82
 
67
83
  if (eventType === STICKER_DOMAIN_EVENTS.STICKER_CLASSIFIED) {
84
+ const coalesceBucket = toWindowBucket(event?.created_at, CURATION_COALESCE_WINDOW_SECONDS);
68
85
  const assetId = String(payload?.asset_id || aggregateId || '').trim();
69
86
  const relatedPackIds = assetId ? await listPackIdsByStickerId(assetId).catch(() => []) : [];
70
87
  if (relatedPackIds.length) {
@@ -77,23 +94,28 @@ const handleDomainEvent = async (event) => {
77
94
  event_type: eventType,
78
95
  aggregate_id: aggregateId,
79
96
  related_pack_ids: relatedPackIds,
97
+ coalesced: true,
80
98
  },
81
99
  priority: 65,
82
- idempotencyKey: `evt:${event.id}:curation_cycle`,
100
+ idempotencyKey: `evt:${eventType}:${coalesceBucket}:curation_cycle`,
83
101
  });
84
102
  return;
85
103
  }
86
104
 
87
105
  if (eventType === STICKER_DOMAIN_EVENTS.PACK_UPDATED) {
106
+ const coalesceBucket = toWindowBucket(event?.created_at, REBUILD_COALESCE_WINDOW_SECONDS);
88
107
  const packId = String(payload?.pack_id || aggregateId || '').trim();
89
108
  if (packId) {
90
109
  enqueuePackScoreSnapshotRefresh([packId]);
91
110
  }
111
+ const rebuildIdempotency = packId
112
+ ? `evt:${eventType}:${packId}:${coalesceBucket}:rebuild_cycle`
113
+ : `evt:${eventType}:${coalesceBucket}:rebuild_cycle`;
92
114
  await enqueueTaskSafely({
93
115
  taskType: 'rebuild_cycle',
94
- payload: { reason: 'domain_event', event_type: eventType, aggregate_id: aggregateId, pack_id: packId || null },
116
+ payload: { reason: 'domain_event', event_type: eventType, aggregate_id: aggregateId, pack_id: packId || null, coalesced: true },
95
117
  priority: 60,
96
- idempotencyKey: `evt:${event.id}:rebuild_cycle`,
118
+ idempotencyKey: rebuildIdempotency,
97
119
  });
98
120
  return;
99
121
  }
package/database/index.js CHANGED
@@ -104,6 +104,7 @@ export const TABLES = {
104
104
  SEMANTIC_THEME_SUGGESTION_CACHE: 'semantic_theme_suggestion_cache',
105
105
  STICKER_PACK_ENGAGEMENT: 'sticker_pack_engagement',
106
106
  STICKER_PACK_INTERACTION_EVENT: 'sticker_pack_interaction_event',
107
+ WEB_VISIT_EVENT: 'web_visit_event',
107
108
  STICKER_PACK_SCORE_SNAPSHOT: 'sticker_pack_score_snapshot',
108
109
  STICKER_ASSET_REPROCESS_QUEUE: 'sticker_asset_reprocess_queue',
109
110
  STICKER_WORKER_TASK_QUEUE: 'sticker_worker_task_queue',
@@ -0,0 +1,15 @@
1
+ CREATE TABLE IF NOT EXISTS web_visit_event (
2
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
3
+ visitor_key VARCHAR(80) NOT NULL,
4
+ session_key VARCHAR(80) NOT NULL,
5
+ page_path VARCHAR(255) NOT NULL,
6
+ referrer VARCHAR(1024) NULL,
7
+ user_agent VARCHAR(512) NULL,
8
+ source VARCHAR(32) NOT NULL DEFAULT 'web',
9
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
10
+ INDEX idx_web_visit_created_at (created_at),
11
+ INDEX idx_web_visit_page_created (page_path, created_at),
12
+ INDEX idx_web_visit_visitor_created (visitor_key, created_at),
13
+ INDEX idx_web_visit_session_created (session_key, created_at),
14
+ INDEX idx_web_visit_source_created (source, created_at)
15
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaikybrofc/omnizap-system",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Sistema profissional de automação WhatsApp com tecnologia Baileys",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/public/index.html CHANGED
@@ -184,6 +184,32 @@
184
184
  cursor: pointer;
185
185
  }
186
186
 
187
+ .nav-toggle.nav-toggle-user {
188
+ padding: 0;
189
+ border-color: #3f5f8f;
190
+ background: linear-gradient(120deg, #132544d6, #10203ad6);
191
+ box-shadow: 0 0 0 2px #1a2e4b;
192
+ overflow: hidden;
193
+ }
194
+
195
+ .nav-toggle.nav-toggle-login {
196
+ width: auto;
197
+ min-width: 92px;
198
+ padding: 0 14px;
199
+ border-color: #38598b;
200
+ background: linear-gradient(120deg, #132544d6, #10203ad6);
201
+ box-shadow: 0 0 0 2px #1a2e4b;
202
+ font-size: 14px;
203
+ font-weight: 700;
204
+ }
205
+
206
+ .nav-toggle-photo {
207
+ width: 100%;
208
+ height: 100%;
209
+ object-fit: cover;
210
+ display: block;
211
+ }
212
+
187
213
  .btn {
188
214
  text-decoration: none;
189
215
  color: var(--text);
@@ -672,26 +698,25 @@
672
698
  background: #101a2f;
673
699
  }
674
700
 
675
- .nav-user-chip {
676
- display: inline-flex;
677
- align-items: center;
678
- gap: 8px;
679
- padding: 6px 10px 6px 6px;
680
- max-width: 270px;
701
+ .btn.nav-user-chip {
702
+ width: 40px;
703
+ height: 40px;
704
+ padding: 0;
705
+ border-radius: 999px;
681
706
  border-color: #3f5f8f;
682
707
  background: linear-gradient(120deg, #132544d6, #10203ad6);
708
+ box-shadow: 0 0 0 2px #1a2e4b;
709
+ justify-content: center;
710
+ overflow: hidden;
683
711
  }
684
712
 
685
713
  .nav-user-avatar-bubble {
686
714
  display: inline-flex;
687
- width: 30px;
688
- height: 30px;
715
+ width: 100%;
716
+ height: 100%;
689
717
  border-radius: 999px;
690
- border: 1px solid #4a6fa4;
718
+ border: 0;
691
719
  background: #132340;
692
- box-shadow:
693
- 0 0 0 2px #1a2e4b,
694
- 0 0 16px #6aaaf236;
695
720
  overflow: hidden;
696
721
  flex: 0 0 auto;
697
722
  }
@@ -703,32 +728,6 @@
703
728
  display: block;
704
729
  }
705
730
 
706
- .nav-user-name-bubble {
707
- display: inline-flex;
708
- align-items: center;
709
- gap: 6px;
710
- min-width: 0;
711
- padding: 4px 8px;
712
- border-radius: 999px;
713
- border: 1px solid #34547f;
714
- background: #12213ac9;
715
- }
716
-
717
- .nav-user-icon {
718
- font-size: 12px;
719
- color: #9bc6f5;
720
- flex: 0 0 auto;
721
- }
722
-
723
- .nav-user-name {
724
- font-size: 12px;
725
- font-weight: 700;
726
- color: #dbe8fb;
727
- white-space: nowrap;
728
- overflow: hidden;
729
- text-overflow: ellipsis;
730
- }
731
-
732
731
  .wpp-float {
733
732
  position: fixed;
734
733
  right: 16px;
@@ -805,6 +804,10 @@
805
804
  .btn {
806
805
  width: 100%;
807
806
  }
807
+
808
+ .btn.nav-mobile-hidden {
809
+ display: none;
810
+ }
808
811
  .proof-grid {
809
812
  grid-template-columns: 1fr;
810
813
  }
@@ -817,9 +820,6 @@
817
820
  <path d="M3 4.5A2.5 2.5 0 0 1 5.5 2h8A2.5 2.5 0 0 1 16 4.5V8a1 1 0 1 1-2 0V4.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v15a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V16a1 1 0 1 1 2 0v3.5a2.5 2.5 0 0 1-2.5 2.5h-8A2.5 2.5 0 0 1 3 19.5z" />
818
821
  <path d="M19.293 8.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L20.586 13H9a1 1 0 1 1 0-2h11.586l-1.293-1.293a1 1 0 0 1 0-1.414z" />
819
822
  </symbol>
820
- <symbol id="icon-user" viewBox="0 0 24 24">
821
- <path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM4 20a8 8 0 1 1 16 0z" />
822
- </symbol>
823
823
  <symbol id="icon-sparkles" viewBox="0 0 24 24">
824
824
  <path d="m11.35 2.23 1.3.01 1.58 4.23 4.24 1.59v1.29l-4.24 1.59-1.58 4.23h-1.3l-1.58-4.23-4.24-1.59V8.06l4.24-1.59zM18.17 10l.83 2.03L21 12.86l-2 .83-.83 2.03-.83-2.03-2-.83 2-.83zM5 13.2l1.02 2.43 2.42 1.03-2.42 1.03L5 20.12l-1.02-2.43-2.42-1.03 2.42-1.03z" />
825
825
  </symbol>
@@ -875,11 +875,6 @@
875
875
  ><svg viewBox="0 0 24 24"><use href="#icon-sparkles"></use></svg></span
876
876
  >Ver Recursos</a
877
877
  >
878
- <a id="hero-login-cta" class="btn" href="/login/"
879
- ><span class="icon" aria-hidden="true"
880
- ><svg viewBox="0 0 24 24"><use href="#icon-user"></use></svg></span
881
- >Entrar</a
882
- >
883
878
  </div>
884
879
 
885
880
  <div class="proof-grid" aria-label="Prova social">
@@ -1114,11 +1109,6 @@
1114
1109
  ><svg viewBox="0 0 24 24"><use href="#icon-whatsapp"></use></svg></span
1115
1110
  >Adicionar Bot Agora</a
1116
1111
  >
1117
- <a id="final-login-cta" class="btn" href="/login/"
1118
- ><span class="icon" aria-hidden="true"
1119
- ><svg viewBox="0 0 24 24"><use href="#icon-user"></use></svg></span
1120
- >Entrar</a
1121
- >
1122
1112
  </div>
1123
1113
  </section>
1124
1114
  </main>
@@ -1142,6 +1132,6 @@
1142
1132
  ><svg viewBox="0 0 24 24"><use href="#icon-whatsapp"></use></svg></span
1143
1133
  ></a>
1144
1134
 
1145
- <script type="module" src="/js/apps/homeApp.js?v=20260228-home-bootstrap-v4"></script>
1135
+ <script type="module" src="/js/apps/homeApp.js?v=20260228-home-bootstrap-v10"></script>
1146
1136
  </body>
1147
1137
  </html>
@@ -137,7 +137,21 @@ const initNavToggle = () => {
137
137
  toggle.setAttribute('aria-expanded', 'false');
138
138
  };
139
139
 
140
- const onClick = () => {
140
+ const onClick = (event) => {
141
+ if (toggle.classList.contains('nav-toggle-login')) {
142
+ if (event) event.preventDefault();
143
+ closeMenu();
144
+ const loginUrl = String(toggle.dataset.loginUrl || '/login/').trim() || '/login/';
145
+ window.location.assign(loginUrl);
146
+ return;
147
+ }
148
+ if (toggle.classList.contains('nav-toggle-user')) {
149
+ if (event) event.preventDefault();
150
+ closeMenu();
151
+ const profileUrl = String(toggle.dataset.profileUrl || '/user/').trim() || '/user/';
152
+ window.location.assign(profileUrl);
153
+ return;
154
+ }
141
155
  const isOpen = nav.classList.toggle('open');
142
156
  toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
143
157
  };
@@ -163,16 +177,138 @@ const initNavToggle = () => {
163
177
 
164
178
  const initAuthSession = () => {
165
179
  const authLink = document.getElementById('nav-auth-link');
166
- const heroLoginCta = document.getElementById('hero-login-cta');
167
- const finalLoginCta = document.getElementById('final-login-cta');
180
+ const schedulerLink = document.getElementById('nav-scheduler-link');
181
+ const navToggle = document.getElementById('nav-toggle');
168
182
  if (!authLink) return null;
169
183
 
184
+ const mobileQuery = typeof window.matchMedia === 'function' ? window.matchMedia('(max-width: 920px)') : null;
185
+ let currentSessionData = null;
186
+ let isAuthenticated = false;
187
+ const isMobileViewport = () => {
188
+ const byMedia = Boolean(mobileQuery?.matches);
189
+ const viewportWidth = Math.max(Number(window.innerWidth || 0), Number(document.documentElement?.clientWidth || 0));
190
+ return byMedia || (viewportWidth > 0 && viewportWidth <= 920);
191
+ };
192
+ const schedulerDefaultHref = String(schedulerLink?.getAttribute('href') || '#beneficios').trim() || '#beneficios';
193
+ const schedulerDefaultLabel = String(schedulerLink?.textContent || 'Benefícios').trim() || 'Benefícios';
194
+ const navToggleDefaultLabel = String(navToggle?.textContent || '☰').trim() || '☰';
195
+
170
196
  const clearChildren = (node) => {
171
197
  while (node.firstChild) node.removeChild(node.firstChild);
172
198
  };
173
199
 
200
+ const setSchedulerDefaultState = () => {
201
+ if (!schedulerLink) return;
202
+ schedulerLink.classList.remove('nav-user-chip');
203
+ schedulerLink.href = schedulerDefaultHref;
204
+ schedulerLink.removeAttribute('title');
205
+ schedulerLink.removeAttribute('aria-label');
206
+ clearChildren(schedulerLink);
207
+ schedulerLink.append(document.createTextNode(schedulerDefaultLabel));
208
+ };
209
+
210
+ const setToggleDefaultState = () => {
211
+ if (!navToggle) return;
212
+ navToggle.classList.remove('nav-toggle-user', 'nav-toggle-login');
213
+ delete navToggle.dataset.profileUrl;
214
+ delete navToggle.dataset.loginUrl;
215
+ navToggle.setAttribute('aria-label', 'Abrir menu');
216
+ navToggle.setAttribute('aria-controls', 'main-nav');
217
+ navToggle.setAttribute('aria-expanded', 'false');
218
+ clearChildren(navToggle);
219
+ navToggle.append(document.createTextNode(navToggleDefaultLabel));
220
+ };
221
+
222
+ const setToggleLoginState = () => {
223
+ if (!navToggle) return;
224
+ navToggle.classList.remove('nav-toggle-user');
225
+ navToggle.classList.add('nav-toggle-login');
226
+ delete navToggle.dataset.profileUrl;
227
+ navToggle.dataset.loginUrl = '/login/';
228
+ navToggle.setAttribute('aria-label', 'Entrar');
229
+ navToggle.removeAttribute('aria-controls');
230
+ navToggle.removeAttribute('aria-expanded');
231
+ clearChildren(navToggle);
232
+ navToggle.append(document.createTextNode('Entrar'));
233
+ };
234
+
235
+ const setSchedulerAsUserBubble = (sessionData) => {
236
+ if (!schedulerLink) return;
237
+
238
+ const profile = sessionData?.user || {};
239
+ const resolvedName = String(profile?.name || profile?.email || 'Conta Google').trim() || 'Conta Google';
240
+ const resolvedPhoto = String(profile?.picture || '').trim() || FALLBACK_THUMB_URL;
241
+
242
+ schedulerLink.classList.add('nav-user-chip');
243
+ schedulerLink.href = '/user/';
244
+ schedulerLink.title = `${resolvedName} (sessão ativa)`;
245
+ schedulerLink.setAttribute('aria-label', `Sessão ativa de ${resolvedName}`);
246
+ clearChildren(schedulerLink);
247
+
248
+ const avatarBubble = document.createElement('span');
249
+ avatarBubble.className = 'nav-user-avatar-bubble';
250
+
251
+ const photo = document.createElement('img');
252
+ photo.className = 'nav-user-photo';
253
+ photo.src = resolvedPhoto;
254
+ photo.alt = `Foto de ${resolvedName}`;
255
+ photo.loading = 'lazy';
256
+ photo.decoding = 'async';
257
+ photo.width = 34;
258
+ photo.height = 34;
259
+ photo.onerror = () => {
260
+ photo.src = FALLBACK_THUMB_URL;
261
+ };
262
+ avatarBubble.appendChild(photo);
263
+ schedulerLink.append(avatarBubble);
264
+ };
265
+
266
+ const setToggleAsUserBubble = (sessionData) => {
267
+ if (!navToggle) return;
268
+
269
+ const profile = sessionData?.user || {};
270
+ const resolvedName = String(profile?.name || profile?.email || 'Conta Google').trim() || 'Conta Google';
271
+ const resolvedPhoto = String(profile?.picture || '').trim() || FALLBACK_THUMB_URL;
272
+
273
+ navToggle.classList.remove('nav-toggle-login');
274
+ navToggle.classList.add('nav-toggle-user');
275
+ navToggle.dataset.profileUrl = '/user/';
276
+ delete navToggle.dataset.loginUrl;
277
+ navToggle.setAttribute('aria-label', `Abrir perfil de ${resolvedName}`);
278
+ navToggle.removeAttribute('aria-controls');
279
+ navToggle.removeAttribute('aria-expanded');
280
+ clearChildren(navToggle);
281
+
282
+ const photo = document.createElement('img');
283
+ photo.className = 'nav-toggle-photo';
284
+ photo.src = resolvedPhoto;
285
+ photo.alt = `Foto de ${resolvedName}`;
286
+ photo.loading = 'lazy';
287
+ photo.decoding = 'async';
288
+ photo.width = 40;
289
+ photo.height = 40;
290
+ photo.onerror = () => {
291
+ photo.src = FALLBACK_THUMB_URL;
292
+ };
293
+ navToggle.append(photo);
294
+ };
295
+
296
+ const applyLoggedOutLayout = () => {
297
+ setSchedulerDefaultState();
298
+ if (isMobileViewport()) {
299
+ authLink.classList.add('nav-mobile-hidden');
300
+ setToggleLoginState();
301
+ return;
302
+ }
303
+ authLink.classList.remove('nav-mobile-hidden');
304
+ setToggleDefaultState();
305
+ };
306
+
174
307
  const setLoginState = () => {
308
+ currentSessionData = null;
309
+ isAuthenticated = false;
175
310
  authLink.classList.remove('nav-user-chip');
311
+ authLink.classList.remove('nav-mobile-hidden');
176
312
  authLink.href = '/login/';
177
313
  authLink.removeAttribute('title');
178
314
  authLink.removeAttribute('aria-label');
@@ -181,18 +317,13 @@ const initAuthSession = () => {
181
317
  const icon = createIcon('icon-login');
182
318
 
183
319
  authLink.append(icon, document.createTextNode('Entrar'));
184
-
185
- if (heroLoginCta) {
186
- heroLoginCta.hidden = false;
187
- heroLoginCta.removeAttribute('aria-hidden');
188
- }
189
- if (finalLoginCta) {
190
- finalLoginCta.hidden = false;
191
- finalLoginCta.removeAttribute('aria-hidden');
192
- }
320
+ applyLoggedOutLayout();
193
321
  };
194
322
 
195
- const setLoggedState = (sessionData) => {
323
+ const applyLoggedLayout = () => {
324
+ if (!currentSessionData) return;
325
+
326
+ const sessionData = currentSessionData;
196
327
  const profile = sessionData?.user || {};
197
328
  const resolvedName = String(profile?.name || profile?.email || 'Conta Google').trim() || 'Conta Google';
198
329
  const resolvedPhoto = String(profile?.picture || '').trim() || FALLBACK_THUMB_URL;
@@ -218,30 +349,42 @@ const initAuthSession = () => {
218
349
  photo.src = FALLBACK_THUMB_URL;
219
350
  };
220
351
  avatarBubble.appendChild(photo);
352
+ authLink.append(avatarBubble);
221
353
 
222
- const nameBubble = document.createElement('span');
223
- nameBubble.className = 'nav-user-name-bubble';
224
-
225
- const icon = createIcon('icon-user', 'icon nav-user-icon');
354
+ if (isMobileViewport()) {
355
+ setSchedulerAsUserBubble(sessionData);
356
+ setToggleAsUserBubble(sessionData);
357
+ authLink.classList.add('nav-mobile-hidden');
358
+ return;
359
+ }
226
360
 
227
- const name = document.createElement('span');
228
- name.className = 'nav-user-name';
229
- name.textContent = resolvedName;
361
+ authLink.classList.remove('nav-mobile-hidden');
362
+ setSchedulerDefaultState();
363
+ setToggleDefaultState();
364
+ };
230
365
 
231
- nameBubble.append(icon, name);
232
- authLink.append(avatarBubble, nameBubble);
366
+ const setLoggedState = (sessionData) => {
367
+ currentSessionData = sessionData || null;
368
+ isAuthenticated = true;
369
+ applyLoggedLayout();
370
+ };
233
371
 
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');
372
+ const onViewportChange = () => {
373
+ if (isAuthenticated) {
374
+ applyLoggedLayout();
375
+ return;
241
376
  }
377
+ applyLoggedOutLayout();
242
378
  };
243
379
 
244
- return runAfterLoadIdle(
380
+ if (mobileQuery && typeof mobileQuery.addEventListener === 'function') {
381
+ mobileQuery.addEventListener('change', onViewportChange);
382
+ } else if (mobileQuery && typeof mobileQuery.addListener === 'function') {
383
+ mobileQuery.addListener(onViewportChange);
384
+ }
385
+ window.addEventListener('resize', onViewportChange);
386
+
387
+ const stopBootstrap = runAfterLoadIdle(
245
388
  () => {
246
389
  fetchHomeBootstrapPayload()
247
390
  .then((bootstrapData) => {
@@ -258,6 +401,16 @@ const initAuthSession = () => {
258
401
  },
259
402
  { delayMs: 520, timeoutMs: 1200 },
260
403
  );
404
+
405
+ return () => {
406
+ stopBootstrap();
407
+ window.removeEventListener('resize', onViewportChange);
408
+ if (mobileQuery && typeof mobileQuery.removeEventListener === 'function') {
409
+ mobileQuery.removeEventListener('change', onViewportChange);
410
+ } else if (mobileQuery && typeof mobileQuery.removeListener === 'function') {
411
+ mobileQuery.removeListener(onViewportChange);
412
+ }
413
+ };
261
414
  };
262
415
 
263
416
  const initAddBotCtas = () => {
@@ -286,7 +439,7 @@ const initAddBotCtas = () => {
286
439
  () => {
287
440
  fetchHomeBootstrapPayload()
288
441
  .then((bootstrapData) => {
289
- const url = String(bootstrapData?.support?.url || '').trim();
442
+ const url = String(bootstrapData?.bot_contact?.urls?.menu || '').trim();
290
443
  const applied = applyLink(url);
291
444
  if (!applied && floatButton) {
292
445
  floatButton.hidden = true;
@@ -1729,7 +1729,6 @@ function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike
1729
1729
  const packLockedByNsfw = isPackMarkedNsfw(pack) && !hasNsfwAccess;
1730
1730
  const cover = packLockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack?.cover_url || items?.[0]?.asset_url || DEFAULT_STICKER_PLACEHOLDER_URL;
1731
1731
  const whatsappUrl = String(pack?.whatsapp?.url || '').trim();
1732
- const packApiPath = `/api/sticker-packs/${encodeURIComponent(String(pack?.pack_key || '').trim())}`;
1733
1732
  const engagement = getPackEngagement(pack);
1734
1733
  const hasReactionRequest = Boolean(reactionLoading);
1735
1734
  const [previewIndex, setPreviewIndex] = useState(-1);
@@ -1779,23 +1778,6 @@ function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike
1779
1778
  </div>
1780
1779
 
1781
1780
  ${pack?.description ? html`<p className="text-sm leading-6 text-slate-300">${pack.description}</p>` : null}
1782
-
1783
- <section className="rounded-xl border border-cyan-500/25 bg-cyan-500/5 p-3">
1784
- <p className="text-[11px] uppercase tracking-wide text-cyan-200">Use este pack no seu bot</p>
1785
- <p className="mt-1 text-sm text-slate-200">Este pack faz parte do módulo de stickers da plataforma OmniZap. Você pode consumir por API e conectar ao seu fluxo de automação WhatsApp.</p>
1786
- <pre className="mt-2 overflow-auto rounded-lg border border-slate-800 bg-slate-950/60 p-2 text-[11px] text-slate-300">
1787
- GET ${packApiPath}
1788
- POST ${packApiPath}/open
1789
- POST ${packApiPath}/like
1790
- </pre
1791
- >
1792
- <div className="mt-2 flex flex-wrap gap-2">
1793
- <a href="/api-docs/" className="inline-flex h-8 items-center rounded-lg border border-cyan-500/35 bg-cyan-500/10 px-3 text-[11px] font-semibold text-cyan-100 hover:bg-cyan-500/20"> Ver API e exemplos </a>
1794
- <a href="/" className="inline-flex h-8 items-center rounded-lg border border-slate-700 bg-slate-900/70 px-3 text-[11px] text-slate-200 hover:bg-slate-800"> Plataforma OmniZap </a>
1795
- <a href="/stickers/" className="inline-flex h-8 items-center rounded-lg border border-slate-700 bg-slate-900/70 px-3 text-[11px] text-slate-200 hover:bg-slate-800"> Voltar ao catálogo </a>
1796
- </div>
1797
- </section>
1798
-
1799
1781
  ${tags.length
1800
1782
  ? html`
1801
1783
  <div className="space-y-1">
@@ -3765,16 +3747,6 @@ function StickersApp() {
3765
3747
  : currentPackKey
3766
3748
  ? html` ${packLoading ? html`<${PackPageSkeleton} />` : html`<${PackPage} pack=${currentPack} relatedPacks=${relatedPacks} onBack=${goCatalog} onOpenRelated=${openPack} onLike=${handleLike} onDislike=${handleDislike} onTagClick=${openCatalogTagFilter} reactionLoading=${reactionLoading} reactionNotice=${reactionNotice} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`} `
3767
3749
  : html`
3768
- <section className="rounded-2xl border border-slate-800 bg-slate-900/80 p-3 sm:p-4">
3769
- <p className="text-[11px] uppercase tracking-wide text-slate-400">Módulo de stickers da plataforma OmniZap</p>
3770
- <h1 className="mt-1 text-lg sm:text-xl font-extrabold tracking-tight text-slate-100">Catálogo de stickers integrável via API</h1>
3771
- <p className="mt-1 text-sm text-slate-300">Os stickers são uma feature do OmniZap para bots e automação WhatsApp. Explore packs públicos e integre no seu sistema com endpoints documentados.</p>
3772
- <div className="mt-2 flex flex-wrap gap-2">
3773
- <a href="/api-docs/" className="inline-flex h-8 items-center rounded-lg border border-cyan-500/35 bg-cyan-500/10 px-3 text-[11px] font-semibold text-cyan-100 hover:bg-cyan-500/20"> Área de Desenvolvedor </a>
3774
- <a href="/" className="inline-flex h-8 items-center rounded-lg border border-slate-700 bg-slate-900/70 px-3 text-[11px] text-slate-200 hover:bg-slate-800"> Página principal OmniZap </a>
3775
- </div>
3776
- </section>
3777
-
3778
3750
  <div className="lg:grid lg:grid-cols-[220px_minmax(0,1fr)] lg:gap-4">
3779
3751
  <aside className="hidden lg:block">
3780
3752
  <div className="sticky top-[72px] space-y-2.5 rounded-2xl border border-slate-800 bg-slate-900/80 p-2.5">