@kaikybrofc/omnizap-system 2.3.1 → 2.3.3

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 (49) hide show
  1. package/README.md +82 -483
  2. package/app/controllers/messageController.js +473 -255
  3. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  4. package/app/modules/stickerModule/stickerCommand.js +7 -2
  5. package/app/modules/stickerModule/stickerTextCommand.js +7 -2
  6. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
  7. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +224 -53
  8. package/app/observability/metrics.js +6 -3
  9. package/app/services/googleWebLinkService.js +77 -0
  10. package/app/services/lidMapService.js +83 -4
  11. package/database/index.js +2 -0
  12. package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
  13. package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
  14. package/package.json +1 -1
  15. package/public/index.html +12 -8
  16. package/public/js/apps/createPackApp.js +4 -4
  17. package/public/js/apps/homeApp.js +78 -34
  18. package/public/js/apps/loginApp.js +245 -35
  19. package/public/js/apps/stickersAdminApp.js +4 -10
  20. package/public/js/apps/stickersApp.js +1 -1
  21. package/public/js/apps/userApp.js +956 -55
  22. package/public/js/apps/userProfileApp.js +244 -0
  23. package/public/login/index.html +437 -101
  24. package/public/termos-de-uso/index.html +1 -1
  25. package/public/user/index.html +2 -181
  26. package/public/user/systemadm/index.html +774 -0
  27. package/server/controllers/stickerCatalog/nonCatalogHandlers.js +183 -0
  28. package/server/controllers/stickerCatalogController.js +1289 -368
  29. package/server/controllers/systemAdminController.js +141 -0
  30. package/server/controllers/userController.js +87 -0
  31. package/server/http/httpServer.js +72 -32
  32. package/server/middleware/cachePolicy.js +24 -0
  33. package/server/middleware/cachePolicyHelpers.js +1 -0
  34. package/server/middleware/rateLimit.js +89 -0
  35. package/server/middleware/requestLogger.js +16 -0
  36. package/server/middleware/requireAdminAuth.js +42 -0
  37. package/server/middleware/securityHeaders.js +6 -0
  38. package/server/routes/admin/systemAdminRouter.js +56 -0
  39. package/server/routes/health/healthRouter.js +41 -0
  40. package/server/routes/indexRouter.js +197 -0
  41. package/server/routes/metrics/metricsRouter.js +13 -0
  42. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
  43. package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
  44. package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
  45. package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
  46. package/server/routes/user/userRouter.js +56 -0
  47. package/server/utils/safePath.js +26 -0
  48. package/server/routes/metricsRoute.js +0 -7
  49. package/server/routes/stickerCatalogRoute.js +0 -20
@@ -1,3 +1,6 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
1
4
  import logger from '../utils/logger/loggerModule.js';
2
5
  import { executeQuery, TABLES } from '../../database/index.js';
3
6
  import { getJidServer, normalizeJid, isGroupJid } from '../config/baileysConfig.js';
@@ -10,12 +13,16 @@ const STORE_COOLDOWN_MS = 10 * 60 * 1000;
10
13
  const BATCH_LIMIT = 800;
11
14
  const BACKFILL_DEFAULT_BATCH = 50000;
12
15
  const BACKFILL_SOURCE = 'backfill';
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const BAILEYS_AUTH_DIR = path.resolve(__dirname, '../connection/auth');
13
19
 
14
20
  const LID_SERVERS = new Set(['lid', 'hosted.lid']);
15
21
  const PN_SERVERS = new Set(['s.whatsapp.net', 'c.us', 'hosted']);
16
22
 
17
23
  const lidCache = new Map();
18
24
  const lidWriteBuffer = new Map();
25
+ const authReverseLidCache = new Map();
19
26
 
20
27
  let backfillPromise = null;
21
28
 
@@ -55,6 +62,54 @@ const normalizeWhatsAppJid = (jid) => {
55
62
  return normalized || null;
56
63
  };
57
64
 
65
+ const toDigits = (value) => String(value || '').replace(/\D+/g, '');
66
+
67
+ const parseReverseMappingPhoneDigits = (content) => {
68
+ const raw = String(content || '').trim();
69
+ if (!raw) return '';
70
+
71
+ let parsed = raw;
72
+ try {
73
+ parsed = JSON.parse(raw);
74
+ } catch {
75
+ parsed = raw;
76
+ }
77
+
78
+ const digits = toDigits(parsed);
79
+ return digits.length >= 10 && digits.length <= 15 ? digits : '';
80
+ };
81
+
82
+ const resolveAuthStoreJidByLid = async (lid) => {
83
+ const normalizedLid = normalizeLid(lid);
84
+ if (!normalizedLid) return null;
85
+
86
+ const [rawUser] = normalizedLid.split('@');
87
+ const rootUser = rawUser ? rawUser.split(':')[0] : '';
88
+ if (!rootUser || !/^\d+$/.test(rootUser)) return null;
89
+
90
+ if (authReverseLidCache.has(rootUser)) {
91
+ return authReverseLidCache.get(rootUser);
92
+ }
93
+
94
+ const reverseFilePath = path.join(BAILEYS_AUTH_DIR, `lid-mapping-${rootUser}_reverse.json`);
95
+ try {
96
+ const content = await readFile(reverseFilePath, 'utf8');
97
+ const phoneDigits = parseReverseMappingPhoneDigits(content);
98
+ const resolvedJid = phoneDigits ? normalizeWhatsAppJid(`${phoneDigits}@s.whatsapp.net`) : null;
99
+ authReverseLidCache.set(rootUser, resolvedJid);
100
+ return resolvedJid;
101
+ } catch (error) {
102
+ if (error?.code !== 'ENOENT') {
103
+ logger.warn('Falha ao resolver LID via auth store local.', {
104
+ lid: normalizedLid,
105
+ error: error?.message,
106
+ });
107
+ }
108
+ authReverseLidCache.set(rootUser, null);
109
+ return null;
110
+ }
111
+ };
112
+
58
113
  /**
59
114
  * Mascara um JID para logs.
60
115
  * @param {string|null|undefined} jid
@@ -191,8 +246,21 @@ export const primeLidCache = async (lids = []) => {
191
246
  const base = baseByLid.get(lid);
192
247
  const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
193
248
  const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
194
- const resolved = direct ?? baseValue ?? null;
195
- setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS);
249
+ let resolved = direct ?? baseValue ?? null;
250
+
251
+ if (!resolved) {
252
+ const authStoreResolved = await resolveAuthStoreJidByLid(lid);
253
+ if (authStoreResolved) {
254
+ resolved = authStoreResolved;
255
+ }
256
+ }
257
+
258
+ const directHasJid = typeof direct === 'string' && direct.length > 0;
259
+ const shouldSeed = Boolean(resolved && (!directHasJid || direct !== resolved));
260
+ setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS, shouldSeed ? 0 : undefined);
261
+ if (shouldSeed) {
262
+ queueLidUpdate(lid, resolved, 'prime');
263
+ }
196
264
  results.set(lid, resolved);
197
265
  }
198
266
 
@@ -379,6 +447,15 @@ const fetchJidByLid = async (lid) => {
379
447
  const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
380
448
  const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
381
449
  let resolved = direct ?? baseValue ?? null;
450
+ let resolveSource = 'db';
451
+
452
+ if (!resolved) {
453
+ const authStoreResolved = await resolveAuthStoreJidByLid(lid);
454
+ if (authStoreResolved) {
455
+ resolved = authStoreResolved;
456
+ resolveSource = 'auth-store';
457
+ }
458
+ }
382
459
 
383
460
  if (!resolved) {
384
461
  const normalized = base || lid;
@@ -404,16 +481,18 @@ const fetchJidByLid = async (lid) => {
404
481
  const derivedJid = derivedRows?.[0]?.jid;
405
482
  if (derivedJid && isWhatsAppJid(derivedJid)) {
406
483
  resolved = normalizeJid(derivedJid);
484
+ resolveSource = 'derived';
407
485
  }
408
486
  }
409
487
  }
410
488
 
411
- const shouldSeedDerived = Boolean(resolved && direct === undefined);
489
+ const directHasJid = typeof direct === 'string' && direct.length > 0;
490
+ const shouldSeedDerived = Boolean(resolved && (!directHasJid || direct !== resolved));
412
491
 
413
492
  setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS, shouldSeedDerived ? 0 : undefined);
414
493
 
415
494
  if (shouldSeedDerived) {
416
- queueLidUpdate(lid, resolved, 'derived');
495
+ queueLidUpdate(lid, resolved, resolveSource === 'auth-store' ? 'auth-store' : 'derived');
417
496
  }
418
497
 
419
498
  return resolved;
package/database/index.js CHANGED
@@ -91,6 +91,7 @@ logger.info(`Configuração de banco de dados carregada para o ambiente: ${envir
91
91
  */
92
92
  export const TABLES = {
93
93
  MESSAGES: 'messages',
94
+ MESSAGE_ANALYSIS_EVENT: 'message_analysis_event',
94
95
  CHATS: 'chats',
95
96
  GROUPS_METADATA: 'groups_metadata',
96
97
  GROUP_CONFIGS: 'group_configs',
@@ -116,6 +117,7 @@ export const TABLES = {
116
117
  STICKER_WEB_GOOGLE_SESSION: 'sticker_web_google_session',
117
118
  STICKER_WEB_ADMIN_BAN: 'sticker_web_admin_ban',
118
119
  STICKER_WEB_ADMIN_MODERATOR: 'sticker_web_admin_moderator',
120
+ ADMIN_ACTION_AUDIT: 'admin_action_audit',
119
121
  RPG_PLAYER: 'rpg_player',
120
122
  RPG_PLAYER_POKEMON: 'rpg_player_pokemon',
121
123
  RPG_BATTLE_STATE: 'rpg_battle_state',
@@ -0,0 +1,32 @@
1
+ CREATE TABLE IF NOT EXISTS message_analysis_event (
2
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
3
+ message_id VARCHAR(255) NULL,
4
+ chat_id VARCHAR(255) NULL,
5
+ sender_id VARCHAR(255) NULL,
6
+ sender_name VARCHAR(120) NULL,
7
+ upsert_type VARCHAR(32) NULL,
8
+ source VARCHAR(32) NOT NULL DEFAULT 'whatsapp',
9
+ is_group TINYINT(1) NOT NULL DEFAULT 0,
10
+ is_from_bot TINYINT(1) NOT NULL DEFAULT 0,
11
+ is_command TINYINT(1) NOT NULL DEFAULT 0,
12
+ command_name VARCHAR(64) NULL,
13
+ command_args_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
14
+ command_known TINYINT(1) NULL,
15
+ command_prefix VARCHAR(8) NULL,
16
+ message_kind VARCHAR(48) NOT NULL DEFAULT 'other',
17
+ has_media TINYINT(1) NOT NULL DEFAULT 0,
18
+ media_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
19
+ text_length INT UNSIGNED NOT NULL DEFAULT 0,
20
+ processing_result VARCHAR(64) NOT NULL DEFAULT 'processed',
21
+ error_code VARCHAR(96) NULL,
22
+ metadata JSON NULL,
23
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
24
+ INDEX idx_message_analysis_created (created_at),
25
+ INDEX idx_message_analysis_chat_created (chat_id, created_at),
26
+ INDEX idx_message_analysis_sender_created (sender_id, created_at),
27
+ INDEX idx_message_analysis_command_created (command_name, created_at),
28
+ INDEX idx_message_analysis_kind_created (message_kind, created_at),
29
+ INDEX idx_message_analysis_result_created (processing_result, created_at),
30
+ INDEX idx_message_analysis_is_command_created (is_command, created_at)
31
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
32
+
@@ -0,0 +1,16 @@
1
+ CREATE TABLE IF NOT EXISTS admin_action_audit (
2
+ id CHAR(36) PRIMARY KEY,
3
+ admin_role VARCHAR(32) NOT NULL DEFAULT 'owner',
4
+ admin_google_sub VARCHAR(255) NULL,
5
+ admin_email VARCHAR(255) NULL,
6
+ admin_owner_jid VARCHAR(255) NULL,
7
+ action VARCHAR(96) NOT NULL,
8
+ target_type VARCHAR(64) NULL,
9
+ target_id VARCHAR(255) NULL,
10
+ status VARCHAR(32) NOT NULL DEFAULT 'success',
11
+ details JSON NULL,
12
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
+ INDEX idx_admin_action_audit_created (created_at),
14
+ INDEX idx_admin_action_audit_action_created (action, created_at),
15
+ INDEX idx_admin_action_audit_admin_created (admin_google_sub, admin_email, created_at)
16
+ ) 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.1",
3
+ "version": "2.3.3",
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
@@ -395,7 +395,7 @@
395
395
  .proof-grid {
396
396
  margin-top: 16px;
397
397
  display: grid;
398
- grid-template-columns: repeat(3, minmax(0, 1fr));
398
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
399
399
  gap: 10px;
400
400
  }
401
401
 
@@ -879,16 +879,20 @@
879
879
 
880
880
  <div class="proof-grid" aria-label="Prova social">
881
881
  <article class="proof">
882
- <strong id="proof-packs">--</strong>
883
- <span>rotinas e respostas prontas</span>
882
+ <strong id="proof-bots-online">--</strong>
883
+ <span>bots online agora</span>
884
884
  </article>
885
885
  <article class="proof">
886
- <strong id="proof-stickers">--</strong>
887
- <span>stickers disponíveis</span>
886
+ <strong id="proof-messages-today">--</strong>
887
+ <span>mensagens hoje</span>
888
888
  </article>
889
889
  <article class="proof">
890
- <strong id="proof-groups">--</strong>
891
- <span>grupos ativos com o bot</span>
890
+ <strong id="proof-spam-blocked">--</strong>
891
+ <span>spam bloqueado hoje</span>
892
+ </article>
893
+ <article class="proof">
894
+ <strong id="proof-uptime">--</strong>
895
+ <span>uptime do bot</span>
892
896
  </article>
893
897
  </div>
894
898
  </div>
@@ -1132,6 +1136,6 @@
1132
1136
  ><svg viewBox="0 0 24 24"><use href="#icon-whatsapp"></use></svg></span
1133
1137
  ></a>
1134
1138
 
1135
- <script type="module" src="/js/apps/homeApp.js?v=20260228-home-bootstrap-v10"></script>
1139
+ <script type="module" src="/js/apps/homeApp.js?v=20260301-home-realtime-v11"></script>
1136
1140
  </body>
1137
1141
  </html>
@@ -141,7 +141,7 @@ const clearGoogleAuthCache = () => {
141
141
  };
142
142
 
143
143
  const fetchJson = async (url, options = {}) => {
144
- const response = await fetch(url, { credentials: 'same-origin', ...options });
144
+ const response = await fetch(url, { credentials: 'include', ...options });
145
145
  const payload = await response.json().catch(() => ({}));
146
146
  if (!response.ok) {
147
147
  throw new Error(payload?.error || 'Falha na requisição.');
@@ -730,7 +730,7 @@ function CreatePackApp() {
730
730
  try {
731
731
  const response = await fetch(`${apiBasePath}/${encodeURIComponent(activeSession.packKey)}/publish-state`, {
732
732
  method: 'POST',
733
- credentials: 'same-origin',
733
+ credentials: 'include',
734
734
  headers: { 'Content-Type': 'application/json; charset=utf-8' },
735
735
  body: JSON.stringify({ edit_token: activeSession.editToken }),
736
736
  });
@@ -958,7 +958,7 @@ function CreatePackApp() {
958
958
 
959
959
  const createResponse = await fetch(`${apiBasePath}/create`, {
960
960
  method: 'POST',
961
- credentials: 'same-origin',
961
+ credentials: 'include',
962
962
  headers: { 'Content-Type': 'application/json; charset=utf-8' },
963
963
  body: JSON.stringify({
964
964
  name: finalName,
@@ -1151,7 +1151,7 @@ function CreatePackApp() {
1151
1151
  setStatus('Publicando pack...');
1152
1152
  const finalizeResponse = await fetch(`${apiBasePath}/${encodeURIComponent(session.packKey)}/finalize`, {
1153
1153
  method: 'POST',
1154
- credentials: 'same-origin',
1154
+ credentials: 'include',
1155
1155
  headers: { 'Content-Type': 'application/json; charset=utf-8' },
1156
1156
  body: JSON.stringify({ edit_token: session.editToken }),
1157
1157
  });
@@ -1,20 +1,28 @@
1
1
  const FALLBACK_THUMB_URL = '/assets/images/brand-logo-128.webp';
2
2
  const HOME_BOOTSTRAP_ENDPOINT = '/api/sticker-packs/home-bootstrap';
3
3
  const SVG_NS = 'http://www.w3.org/2000/svg';
4
+ const SOCIAL_PROOF_REFRESH_MS = 15_000;
4
5
  let homeBootstrapPayloadPromise = null;
5
6
 
6
- const fetchHomeBootstrapPayload = async () => {
7
+ const loadHomeBootstrapPayload = async () => {
8
+ const response = await fetch(HOME_BOOTSTRAP_ENDPOINT, { credentials: 'include' });
9
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
10
+ const payload = await response.json();
11
+ return payload?.data || {};
12
+ };
13
+
14
+ const fetchHomeBootstrapPayload = async ({ forceRefresh = false } = {}) => {
15
+ if (forceRefresh) {
16
+ const freshData = await loadHomeBootstrapPayload();
17
+ homeBootstrapPayloadPromise = Promise.resolve(freshData);
18
+ return freshData;
19
+ }
20
+
7
21
  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
- });
22
+ homeBootstrapPayloadPromise = loadHomeBootstrapPayload().catch((error) => {
23
+ homeBootstrapPayloadPromise = null;
24
+ throw error;
25
+ });
18
26
  }
19
27
  return homeBootstrapPayloadPromise;
20
28
  };
@@ -454,17 +462,19 @@ const initAddBotCtas = () => {
454
462
  };
455
463
 
456
464
  const initSocialProof = () => {
457
- const packsEl = document.getElementById('proof-packs');
458
- const stickersEl = document.getElementById('proof-stickers');
459
- const groupsEl = document.getElementById('proof-groups');
465
+ const botsOnlineEl = document.getElementById('proof-bots-online');
466
+ const messagesTodayEl = document.getElementById('proof-messages-today');
467
+ const spamBlockedEl = document.getElementById('proof-spam-blocked');
468
+ const uptimeEl = document.getElementById('proof-uptime');
460
469
  const statusEl = document.getElementById('proof-status');
461
470
 
462
- if (!packsEl || !stickersEl || !groupsEl) return null;
471
+ if (!botsOnlineEl || !messagesTodayEl || !spamBlockedEl || !uptimeEl) return null;
463
472
 
464
473
  const setFallback = () => {
465
- packsEl.textContent = 'n/d';
466
- stickersEl.textContent = 'n/d';
467
- groupsEl.textContent = 'n/d';
474
+ botsOnlineEl.textContent = 'n/d';
475
+ messagesTodayEl.textContent = 'n/d';
476
+ spamBlockedEl.textContent = 'n/d';
477
+ uptimeEl.textContent = 'n/d';
468
478
  if (statusEl) statusEl.textContent = 'bot pronto';
469
479
  };
470
480
 
@@ -479,27 +489,61 @@ const initSocialProof = () => {
479
489
  return 'pronto';
480
490
  };
481
491
 
482
- return runAfterLoadIdle(
483
- () => {
484
- fetchHomeBootstrapPayload()
485
- .then((bootstrapData) => {
486
- const stats = bootstrapData?.stats || {};
487
- const summary = bootstrapData?.system_summary || {};
492
+ const setNumericMetric = (element, value, { animate = true } = {}) => {
493
+ const hasValue = value !== null && value !== undefined && value !== '';
494
+ const numeric = hasValue ? Number(value) : Number.NaN;
495
+ if (!hasValue || !Number.isFinite(numeric)) {
496
+ element.textContent = 'n/d';
497
+ return;
498
+ }
499
+ if (!animate) {
500
+ element.textContent = shortNum(numeric);
501
+ element.dataset.value = String(Math.max(0, numeric));
502
+ return;
503
+ }
504
+ animateCountUp(element, numeric);
505
+ };
488
506
 
489
- animateCountUp(packsEl, Number(stats.packs_total || 0));
490
- animateCountUp(stickersEl, Number(stats.stickers_total || 0));
491
- animateCountUp(groupsEl, Number(summary?.platform?.total_groups || 0));
507
+ const refreshMetrics = async ({ forceRefresh = false, animate = false } = {}) => {
508
+ const bootstrapData = await fetchHomeBootstrapPayload({ forceRefresh });
509
+ const summary = bootstrapData?.system_summary || {};
510
+ const realtime = bootstrapData?.home_realtime || {};
492
511
 
493
- if (statusEl) {
494
- statusEl.textContent = `bot ${normalizeStatus(summary?.system_status || summary?.bot?.connection_status)}`;
495
- }
496
- })
497
- .catch(() => {
498
- setFallback();
499
- });
512
+ const botsOnline = Number(realtime?.bots_online);
513
+ const messagesToday = Number(realtime?.messages_today);
514
+ const spamBlockedToday = Number(realtime?.spam_blocked_today);
515
+ const uptime = String(realtime?.uptime || summary?.process?.uptime || '').trim() || 'n/d';
516
+
517
+ setNumericMetric(botsOnlineEl, botsOnline, { animate });
518
+ setNumericMetric(messagesTodayEl, messagesToday, { animate });
519
+ setNumericMetric(spamBlockedEl, spamBlockedToday, { animate });
520
+ uptimeEl.textContent = uptime;
521
+
522
+ if (statusEl) {
523
+ statusEl.textContent = `bot ${normalizeStatus(summary?.system_status || summary?.bot?.connection_status)}`;
524
+ }
525
+ };
526
+
527
+ let intervalId = null;
528
+ const stopBootstrap = runAfterLoadIdle(
529
+ () => {
530
+ void refreshMetrics({ forceRefresh: false, animate: true }).catch(() => {
531
+ setFallback();
532
+ });
500
533
  },
501
534
  { delayMs: 620, timeoutMs: 1500 },
502
535
  );
536
+
537
+ intervalId = window.setInterval(() => {
538
+ void refreshMetrics({ forceRefresh: true, animate: false }).catch(() => {});
539
+ }, SOCIAL_PROOF_REFRESH_MS);
540
+
541
+ return () => {
542
+ stopBootstrap();
543
+ if (intervalId !== null) {
544
+ window.clearInterval(intervalId);
545
+ }
546
+ };
503
547
  };
504
548
 
505
549
  const registerCleanup = (cleanups, cleanup) => {