@kaikybrofc/omnizap-system 2.3.1 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -18
- package/app/controllers/messageController.js +473 -255
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
- package/app/observability/metrics.js +6 -3
- package/app/services/googleWebLinkService.js +77 -0
- package/database/index.js +2 -0
- package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
- package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
- package/package.json +1 -1
- package/public/index.html +12 -8
- package/public/js/apps/homeApp.js +75 -30
- package/public/js/apps/loginApp.js +184 -29
- package/public/js/apps/stickersAdminApp.js +3 -9
- package/public/js/apps/userApp.js +985 -55
- package/public/js/apps/userProfileApp.js +244 -0
- package/public/login/index.html +430 -100
- package/public/termos-de-uso/index.html +1 -1
- package/public/user/index.html +2 -180
- package/public/user/systemadm/index.html +774 -0
- package/server/controllers/stickerCatalog/nonCatalogHandlers.js +208 -0
- package/server/controllers/stickerCatalogController.js +1186 -363
- package/server/controllers/systemAdminController.js +141 -0
- package/server/controllers/userController.js +87 -0
- package/server/http/httpServer.js +72 -32
- package/server/middleware/cachePolicy.js +24 -0
- package/server/middleware/cachePolicyHelpers.js +2 -0
- package/server/middleware/rateLimit.js +82 -0
- package/server/middleware/requestLogger.js +16 -0
- package/server/middleware/requireAdminAuth.js +42 -0
- package/server/middleware/securityHeaders.js +6 -0
- package/server/routes/admin/systemAdminRouter.js +56 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +203 -0
- package/server/routes/metrics/metricsRouter.js +13 -0
- package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
- package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
- package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
- package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
- package/server/routes/user/userRouter.js +56 -0
- package/server/utils/safePath.js +26 -0
- package/server/routes/metricsRoute.js +0 -7
- package/server/routes/stickerCatalogRoute.js +0 -20
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { executeQuery, TABLES } from '../../../database/index.js';
|
|
2
|
+
|
|
3
|
+
const sanitizeText = (value, maxLength = 255) => {
|
|
4
|
+
const normalized = String(value || '')
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/\s+/g, ' ')
|
|
7
|
+
.slice(0, maxLength);
|
|
8
|
+
return normalized || null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const sanitizeCommandName = (value) => {
|
|
12
|
+
const normalized = String(value || '')
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9_-]/g, '')
|
|
16
|
+
.slice(0, 64);
|
|
17
|
+
return normalized || null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const sanitizeBool = (value) => (value ? 1 : 0);
|
|
21
|
+
|
|
22
|
+
const clampInt = (value, fallback, min, max) => {
|
|
23
|
+
const numeric = Number(value);
|
|
24
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
25
|
+
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const sanitizeMetadata = (value) => {
|
|
29
|
+
if (!value || typeof value !== 'object') return null;
|
|
30
|
+
try {
|
|
31
|
+
const asJson = JSON.stringify(value);
|
|
32
|
+
if (!asJson || asJson === '{}') return null;
|
|
33
|
+
return asJson;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export async function createMessageAnalysisEvent(payload = {}, connection = null) {
|
|
40
|
+
const messageId = sanitizeText(payload.messageId, 255);
|
|
41
|
+
const chatId = sanitizeText(payload.chatId, 255);
|
|
42
|
+
const senderId = sanitizeText(payload.senderId, 255);
|
|
43
|
+
const senderName = sanitizeText(payload.senderName, 120);
|
|
44
|
+
const upsertType = sanitizeText(payload.upsertType, 32);
|
|
45
|
+
const source = sanitizeText(payload.source, 32) || 'whatsapp';
|
|
46
|
+
const commandPrefix = sanitizeText(payload.commandPrefix, 8);
|
|
47
|
+
const commandName = sanitizeCommandName(payload.commandName);
|
|
48
|
+
const messageKind = sanitizeText(payload.messageKind, 48) || 'other';
|
|
49
|
+
const processingResult = sanitizeText(payload.processingResult, 64) || 'processed';
|
|
50
|
+
const errorCode = sanitizeText(payload.errorCode, 96);
|
|
51
|
+
const metadata = sanitizeMetadata(payload.metadata);
|
|
52
|
+
|
|
53
|
+
await executeQuery(
|
|
54
|
+
`INSERT INTO ${TABLES.MESSAGE_ANALYSIS_EVENT}
|
|
55
|
+
(
|
|
56
|
+
message_id,
|
|
57
|
+
chat_id,
|
|
58
|
+
sender_id,
|
|
59
|
+
sender_name,
|
|
60
|
+
upsert_type,
|
|
61
|
+
source,
|
|
62
|
+
is_group,
|
|
63
|
+
is_from_bot,
|
|
64
|
+
is_command,
|
|
65
|
+
command_name,
|
|
66
|
+
command_args_count,
|
|
67
|
+
command_known,
|
|
68
|
+
command_prefix,
|
|
69
|
+
message_kind,
|
|
70
|
+
has_media,
|
|
71
|
+
media_count,
|
|
72
|
+
text_length,
|
|
73
|
+
processing_result,
|
|
74
|
+
error_code,
|
|
75
|
+
metadata
|
|
76
|
+
)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
78
|
+
[messageId, chatId, senderId, senderName, upsertType, source, sanitizeBool(payload.isGroup), sanitizeBool(payload.isFromBot), sanitizeBool(payload.isCommand), commandName, clampInt(payload.commandArgsCount, 0, 0, 64), payload.commandKnown === null || payload.commandKnown === undefined ? null : sanitizeBool(payload.commandKnown), commandPrefix, messageKind, sanitizeBool(payload.hasMedia), clampInt(payload.mediaCount, 0, 0, 25), clampInt(payload.textLength, 0, 0, 16_000), processingResult, errorCode, metadata],
|
|
79
|
+
connection,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
@@ -108,9 +108,7 @@ const handleDomainEvent = async (event) => {
|
|
|
108
108
|
if (packId) {
|
|
109
109
|
enqueuePackScoreSnapshotRefresh([packId]);
|
|
110
110
|
}
|
|
111
|
-
const rebuildIdempotency = packId
|
|
112
|
-
? `evt:${eventType}:${packId}:${coalesceBucket}:rebuild_cycle`
|
|
113
|
-
: `evt:${eventType}:${coalesceBucket}:rebuild_cycle`;
|
|
111
|
+
const rebuildIdempotency = packId ? `evt:${eventType}:${packId}:${coalesceBucket}:rebuild_cycle` : `evt:${eventType}:${coalesceBucket}:rebuild_cycle`;
|
|
114
112
|
await enqueueTaskSafely({
|
|
115
113
|
taskType: 'rebuild_cycle',
|
|
116
114
|
payload: { reason: 'domain_event', event_type: eventType, aggregate_id: aggregateId, pack_id: packId || null, coalesced: true },
|
|
@@ -57,15 +57,17 @@ const toStatusClass = (statusCode) => {
|
|
|
57
57
|
return `${head}xx`;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null } = {}) => {
|
|
60
|
+
export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null, userConfig = null, systemAdminConfig = null } = {}) => {
|
|
61
61
|
if (pathname?.startsWith(metricsPath)) return 'metrics';
|
|
62
|
+
if (pathname === '/healthz' || pathname === '/readyz') return 'health';
|
|
62
63
|
if (pathname === '/sitemap.xml') return 'sitemap';
|
|
63
64
|
if (pathname === '/api/marketplace/stats') return 'marketplace_stats';
|
|
64
65
|
|
|
65
|
-
const apiBasePath = catalogConfig?.apiBasePath || '';
|
|
66
|
+
const apiBasePath = catalogConfig?.apiBasePath || userConfig?.apiBasePath || '';
|
|
66
67
|
const webPath = catalogConfig?.webPath || '';
|
|
67
68
|
const dataPublicPath = catalogConfig?.dataPublicPath || '';
|
|
68
|
-
const userProfilePath =
|
|
69
|
+
const userProfilePath = userConfig?.webPath || '';
|
|
70
|
+
const systemAdminPath = systemAdminConfig?.webPath || '';
|
|
69
71
|
|
|
70
72
|
if (apiBasePath && (pathname === apiBasePath || pathname?.startsWith(`${apiBasePath}/`))) {
|
|
71
73
|
if (pathname === `${apiBasePath}/auth/google/session` || pathname === `${apiBasePath}/me` || pathname === `${apiBasePath}/admin/session`) {
|
|
@@ -78,6 +80,7 @@ export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null
|
|
|
78
80
|
return 'catalog_api_public';
|
|
79
81
|
}
|
|
80
82
|
if (dataPublicPath && (pathname === dataPublicPath || pathname?.startsWith(`${dataPublicPath}/`))) return 'catalog_data_asset';
|
|
83
|
+
if (systemAdminPath && (pathname === systemAdminPath || pathname === `${systemAdminPath}/`)) return 'system_admin_web';
|
|
81
84
|
if (userProfilePath && (pathname === userProfilePath || pathname === `${userProfilePath}/`)) return 'catalog_user_profile';
|
|
82
85
|
if (webPath && (pathname === webPath || pathname?.startsWith(`${webPath}/`))) return 'catalog_web';
|
|
83
86
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { executeQuery, TABLES } from '../../database/index.js';
|
|
2
|
+
import { normalizeJid } from '../config/baileysConfig.js';
|
|
3
|
+
import { toWhatsAppPhoneDigits } from './whatsappLoginLinkService.js';
|
|
4
|
+
|
|
5
|
+
const parseEnvInt = (value, fallback, min, max) => {
|
|
6
|
+
const numeric = Number(value);
|
|
7
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
8
|
+
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const GOOGLE_LINK_CHECK_CACHE_TTL_MS = parseEnvInt(process.env.WHATSAPP_GOOGLE_LINK_CHECK_CACHE_TTL_MS, 60_000, 1_000, 10 * 60_000);
|
|
12
|
+
const googleLinkCheckCache = new Map();
|
|
13
|
+
let googleLinkTableMissingLogged = false;
|
|
14
|
+
|
|
15
|
+
const normalizeCacheKey = ({ ownerJid = '', ownerPhone = '' }) => {
|
|
16
|
+
const normalizedOwnerJid = normalizeJid(ownerJid) || '';
|
|
17
|
+
const normalizedOwnerPhone = toWhatsAppPhoneDigits(ownerPhone || ownerJid) || '';
|
|
18
|
+
return `${normalizedOwnerJid}|${normalizedOwnerPhone}`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getCachedGoogleLinkStatus = (cacheKey) => {
|
|
22
|
+
const cached = googleLinkCheckCache.get(cacheKey);
|
|
23
|
+
if (!cached) return null;
|
|
24
|
+
if (Number(cached.expiresAt || 0) <= Date.now()) {
|
|
25
|
+
googleLinkCheckCache.delete(cacheKey);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return Boolean(cached.linked);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const setCachedGoogleLinkStatus = (cacheKey, linked) => {
|
|
32
|
+
googleLinkCheckCache.set(cacheKey, {
|
|
33
|
+
linked: Boolean(linked),
|
|
34
|
+
expiresAt: Date.now() + GOOGLE_LINK_CHECK_CACHE_TTL_MS,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const isWhatsAppUserLinkedToGoogleWebAccount = async ({ ownerJid = '', ownerPhone = '' } = {}) => {
|
|
39
|
+
const normalizedOwnerJid = normalizeJid(ownerJid) || '';
|
|
40
|
+
const normalizedOwnerPhone = toWhatsAppPhoneDigits(ownerPhone || ownerJid) || '';
|
|
41
|
+
if (!normalizedOwnerJid && !normalizedOwnerPhone) return false;
|
|
42
|
+
|
|
43
|
+
const cacheKey = normalizeCacheKey({ ownerJid: normalizedOwnerJid, ownerPhone: normalizedOwnerPhone });
|
|
44
|
+
const cached = getCachedGoogleLinkStatus(cacheKey);
|
|
45
|
+
if (cached !== null) return cached;
|
|
46
|
+
|
|
47
|
+
const whereClauses = [];
|
|
48
|
+
const params = [];
|
|
49
|
+
if (normalizedOwnerJid) {
|
|
50
|
+
whereClauses.push('owner_jid = ?');
|
|
51
|
+
params.push(normalizedOwnerJid);
|
|
52
|
+
}
|
|
53
|
+
if (normalizedOwnerPhone) {
|
|
54
|
+
whereClauses.push('owner_phone = ?');
|
|
55
|
+
params.push(normalizedOwnerPhone);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rows = await executeQuery(
|
|
59
|
+
`SELECT google_sub
|
|
60
|
+
FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
|
|
61
|
+
WHERE ${whereClauses.join(' OR ')}
|
|
62
|
+
LIMIT 1`,
|
|
63
|
+
params,
|
|
64
|
+
).catch((error) => {
|
|
65
|
+
if (error?.code === 'ER_NO_SUCH_TABLE') {
|
|
66
|
+
if (!googleLinkTableMissingLogged) {
|
|
67
|
+
googleLinkTableMissingLogged = true;
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const linked = Array.isArray(rows) && rows.length > 0;
|
|
75
|
+
setCachedGoogleLinkStatus(cacheKey, linked);
|
|
76
|
+
return linked;
|
|
77
|
+
};
|
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
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(
|
|
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-
|
|
883
|
-
<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-
|
|
887
|
-
<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-
|
|
891
|
-
<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=
|
|
1139
|
+
<script type="module" src="/js/apps/homeApp.js?v=20260301-home-realtime-v11"></script>
|
|
1136
1140
|
</body>
|
|
1137
1141
|
</html>
|
|
@@ -1,16 +1,25 @@
|
|
|
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
|
|
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 =
|
|
9
|
-
.then((response) => {
|
|
10
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
11
|
-
return response.json();
|
|
12
|
-
})
|
|
13
|
-
.then((payload) => payload?.data || {})
|
|
22
|
+
homeBootstrapPayloadPromise = loadHomeBootstrapPayload()
|
|
14
23
|
.catch((error) => {
|
|
15
24
|
homeBootstrapPayloadPromise = null;
|
|
16
25
|
throw error;
|
|
@@ -454,17 +463,19 @@ const initAddBotCtas = () => {
|
|
|
454
463
|
};
|
|
455
464
|
|
|
456
465
|
const initSocialProof = () => {
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
const
|
|
466
|
+
const botsOnlineEl = document.getElementById('proof-bots-online');
|
|
467
|
+
const messagesTodayEl = document.getElementById('proof-messages-today');
|
|
468
|
+
const spamBlockedEl = document.getElementById('proof-spam-blocked');
|
|
469
|
+
const uptimeEl = document.getElementById('proof-uptime');
|
|
460
470
|
const statusEl = document.getElementById('proof-status');
|
|
461
471
|
|
|
462
|
-
if (!
|
|
472
|
+
if (!botsOnlineEl || !messagesTodayEl || !spamBlockedEl || !uptimeEl) return null;
|
|
463
473
|
|
|
464
474
|
const setFallback = () => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
475
|
+
botsOnlineEl.textContent = 'n/d';
|
|
476
|
+
messagesTodayEl.textContent = 'n/d';
|
|
477
|
+
spamBlockedEl.textContent = 'n/d';
|
|
478
|
+
uptimeEl.textContent = 'n/d';
|
|
468
479
|
if (statusEl) statusEl.textContent = 'bot pronto';
|
|
469
480
|
};
|
|
470
481
|
|
|
@@ -479,27 +490,61 @@ const initSocialProof = () => {
|
|
|
479
490
|
return 'pronto';
|
|
480
491
|
};
|
|
481
492
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
493
|
+
const setNumericMetric = (element, value, { animate = true } = {}) => {
|
|
494
|
+
const hasValue = value !== null && value !== undefined && value !== '';
|
|
495
|
+
const numeric = hasValue ? Number(value) : Number.NaN;
|
|
496
|
+
if (!hasValue || !Number.isFinite(numeric)) {
|
|
497
|
+
element.textContent = 'n/d';
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (!animate) {
|
|
501
|
+
element.textContent = shortNum(numeric);
|
|
502
|
+
element.dataset.value = String(Math.max(0, numeric));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
animateCountUp(element, numeric);
|
|
506
|
+
};
|
|
488
507
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
508
|
+
const refreshMetrics = async ({ forceRefresh = false, animate = false } = {}) => {
|
|
509
|
+
const bootstrapData = await fetchHomeBootstrapPayload({ forceRefresh });
|
|
510
|
+
const summary = bootstrapData?.system_summary || {};
|
|
511
|
+
const realtime = bootstrapData?.home_realtime || {};
|
|
492
512
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
513
|
+
const botsOnline = Number(realtime?.bots_online);
|
|
514
|
+
const messagesToday = Number(realtime?.messages_today);
|
|
515
|
+
const spamBlockedToday = Number(realtime?.spam_blocked_today);
|
|
516
|
+
const uptime = String(realtime?.uptime || summary?.process?.uptime || '').trim() || 'n/d';
|
|
517
|
+
|
|
518
|
+
setNumericMetric(botsOnlineEl, botsOnline, { animate });
|
|
519
|
+
setNumericMetric(messagesTodayEl, messagesToday, { animate });
|
|
520
|
+
setNumericMetric(spamBlockedEl, spamBlockedToday, { animate });
|
|
521
|
+
uptimeEl.textContent = uptime;
|
|
522
|
+
|
|
523
|
+
if (statusEl) {
|
|
524
|
+
statusEl.textContent = `bot ${normalizeStatus(summary?.system_status || summary?.bot?.connection_status)}`;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
let intervalId = null;
|
|
529
|
+
const stopBootstrap = runAfterLoadIdle(
|
|
530
|
+
() => {
|
|
531
|
+
void refreshMetrics({ forceRefresh: false, animate: true }).catch(() => {
|
|
532
|
+
setFallback();
|
|
533
|
+
});
|
|
500
534
|
},
|
|
501
535
|
{ delayMs: 620, timeoutMs: 1500 },
|
|
502
536
|
);
|
|
537
|
+
|
|
538
|
+
intervalId = window.setInterval(() => {
|
|
539
|
+
void refreshMetrics({ forceRefresh: true, animate: false }).catch(() => {});
|
|
540
|
+
}, SOCIAL_PROOF_REFRESH_MS);
|
|
541
|
+
|
|
542
|
+
return () => {
|
|
543
|
+
stopBootstrap();
|
|
544
|
+
if (intervalId !== null) {
|
|
545
|
+
window.clearInterval(intervalId);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
503
548
|
};
|
|
504
549
|
|
|
505
550
|
const registerCleanup = (cleanups, cleanup) => {
|