@omnizap-system/omnizap 2.5.12

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 (425) hide show
  1. package/.clusterfuzzlite/Dockerfile +10 -0
  2. package/.env.example +907 -0
  3. package/.github/codeql/codeql-config.yml +10 -0
  4. package/.github/dependabot.yml +35 -0
  5. package/.github/workflows/ci.yml +73 -0
  6. package/.github/workflows/codeql.yml +106 -0
  7. package/.github/workflows/db-migration-check.yml +98 -0
  8. package/.github/workflows/dependency-review.yml +22 -0
  9. package/.github/workflows/deploy.yml +95 -0
  10. package/.github/workflows/release.yml +106 -0
  11. package/.github/workflows/security-attest-provenance.yml +51 -0
  12. package/.github/workflows/security-gitleaks.yml +34 -0
  13. package/.github/workflows/security-runner-hardening.yml +31 -0
  14. package/.github/workflows/security-scorecard.yml +44 -0
  15. package/.github/workflows/security-zap-baseline.yml +44 -0
  16. package/.github/workflows/security-zap-full-scan.yml +43 -0
  17. package/.github/workflows/security-zizmor.yml +36 -0
  18. package/.github/workflows/wiki-sync.yml +44 -0
  19. package/.gitleaks.toml +15 -0
  20. package/.prettierrc +34 -0
  21. package/CODE_OF_CONDUCT.md +114 -0
  22. package/LICENSE +56 -0
  23. package/README.md +110 -0
  24. package/SECURITY.md +110 -0
  25. package/app/config/index.js +4 -0
  26. package/app/configParts/adminIdentity.js +92 -0
  27. package/app/configParts/baileysConfig.js +1818 -0
  28. package/app/configParts/groupUtils.js +692 -0
  29. package/app/configParts/loggerConfig.js +394 -0
  30. package/app/configParts/messagePersistenceService.js +305 -0
  31. package/app/connection/baileysCompatibility.test.js +40 -0
  32. package/app/connection/baileysDbAuthState.js +344 -0
  33. package/app/connection/socketController.js +2243 -0
  34. package/app/controllers/messageController.js +7 -0
  35. package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
  36. package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
  37. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
  38. package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
  39. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
  40. package/app/controllers/messageProcessingPipeline.js +699 -0
  41. package/app/modules/adminModule/AGENT.md +4056 -0
  42. package/app/modules/adminModule/adminAiHelpService.js +56 -0
  43. package/app/modules/adminModule/adminConfigRuntime.js +177 -0
  44. package/app/modules/adminModule/commandConfig.json +7122 -0
  45. package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
  46. package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
  47. package/app/modules/adminModule/groupEventHandlers.js +399 -0
  48. package/app/modules/aiModule/AGENT.md +547 -0
  49. package/app/modules/aiModule/aiAiHelpService.js +14 -0
  50. package/app/modules/aiModule/aiConfigRuntime.js +135 -0
  51. package/app/modules/aiModule/catCommand.js +967 -0
  52. package/app/modules/aiModule/commandConfig.json +981 -0
  53. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  54. package/app/modules/gameModule/AGENT.md +196 -0
  55. package/app/modules/gameModule/commandConfig.json +366 -0
  56. package/app/modules/gameModule/diceCommand.js +42 -0
  57. package/app/modules/gameModule/gameAiHelpService.js +14 -0
  58. package/app/modules/gameModule/gameConfigRuntime.js +68 -0
  59. package/app/modules/menuModule/AGENT.md +205 -0
  60. package/app/modules/menuModule/commandConfig.json +366 -0
  61. package/app/modules/menuModule/common.js +316 -0
  62. package/app/modules/menuModule/menuAiHelpService.js +14 -0
  63. package/app/modules/menuModule/menuConfigRuntime.js +68 -0
  64. package/app/modules/menuModule/menus.js +66 -0
  65. package/app/modules/playModule/AGENT.md +321 -0
  66. package/app/modules/playModule/commandConfig.json +584 -0
  67. package/app/modules/playModule/playAiHelpService.js +14 -0
  68. package/app/modules/playModule/playCommand.js +1417 -0
  69. package/app/modules/playModule/playConfigRuntime.js +68 -0
  70. package/app/modules/quoteModule/AGENT.md +199 -0
  71. package/app/modules/quoteModule/commandConfig.json +366 -0
  72. package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
  73. package/app/modules/quoteModule/quoteCommand.js +842 -0
  74. package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
  75. package/app/modules/rpgPokemonModule/AGENT.md +229 -0
  76. package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
  77. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
  78. package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
  79. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
  80. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  81. package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
  82. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
  83. package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
  84. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  85. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  86. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  87. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  88. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
  89. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
  90. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  91. package/app/modules/statsModule/AGENT.md +320 -0
  92. package/app/modules/statsModule/commandConfig.json +540 -0
  93. package/app/modules/statsModule/globalRankingCommand.js +64 -0
  94. package/app/modules/statsModule/rankingCommand.js +41 -0
  95. package/app/modules/statsModule/rankingCommon.js +1305 -0
  96. package/app/modules/statsModule/statsAiHelpService.js +14 -0
  97. package/app/modules/statsModule/statsConfigRuntime.js +68 -0
  98. package/app/modules/stickerModule/AGENT.md +692 -0
  99. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  100. package/app/modules/stickerModule/commandConfig.json +1216 -0
  101. package/app/modules/stickerModule/convertToWebp.js +367 -0
  102. package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
  103. package/app/modules/stickerModule/stickerCommand.js +446 -0
  104. package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
  105. package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
  106. package/app/modules/stickerModule/stickerTextCommand.js +653 -0
  107. package/app/modules/stickerPackModule/AGENT.md +215 -0
  108. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  109. package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
  110. package/app/modules/stickerPackModule/commandConfig.json +387 -0
  111. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
  112. package/app/modules/stickerPackModule/domainEvents.js +52 -0
  113. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
  114. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
  115. package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
  116. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
  117. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  118. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
  119. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
  120. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
  121. package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
  122. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
  123. package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
  124. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
  125. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
  126. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  127. package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
  128. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
  129. package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
  130. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
  131. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  132. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
  133. package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
  134. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
  135. package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
  136. package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
  137. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
  138. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
  139. package/app/modules/stickerPackModule/stickerPackService.js +733 -0
  140. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
  141. package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
  142. package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
  143. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
  144. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
  145. package/app/modules/systemMetricsModule/AGENT.md +193 -0
  146. package/app/modules/systemMetricsModule/commandConfig.json +344 -0
  147. package/app/modules/systemMetricsModule/pingCommand.js +399 -0
  148. package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
  149. package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
  150. package/app/modules/tiktokModule/AGENT.md +196 -0
  151. package/app/modules/tiktokModule/commandConfig.json +366 -0
  152. package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
  153. package/app/modules/tiktokModule/tiktokCommand.js +716 -0
  154. package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
  155. package/app/modules/userModule/AGENT.md +200 -0
  156. package/app/modules/userModule/commandConfig.json +386 -0
  157. package/app/modules/userModule/userAiHelpService.js +14 -0
  158. package/app/modules/userModule/userCommand.js +1155 -0
  159. package/app/modules/userModule/userConfigRuntime.js +68 -0
  160. package/app/modules/waifuPicsModule/AGENT.md +431 -0
  161. package/app/modules/waifuPicsModule/commandConfig.json +780 -0
  162. package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
  163. package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
  164. package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
  165. package/app/observability/metrics.js +766 -0
  166. package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
  167. package/app/services/ai/aiLearningRepository.js +400 -0
  168. package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
  169. package/app/services/ai/commandConfigEnrichmentService.js +452 -0
  170. package/app/services/ai/commandConfigValidationService.js +443 -0
  171. package/app/services/ai/commandToolBuilderService.js +192 -0
  172. package/app/services/ai/conversationRouterService.js +516 -0
  173. package/app/services/ai/geminiService.js +115 -0
  174. package/app/services/ai/geminiService.test.js +87 -0
  175. package/app/services/ai/globalModuleAiHelpService.js +1412 -0
  176. package/app/services/ai/globalToolCallingService.js +203 -0
  177. package/app/services/ai/messageCommandExecutionService.js +391 -0
  178. package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
  179. package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
  180. package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
  181. package/app/services/ai/moduleToolExecutorService.js +464 -0
  182. package/app/services/ai/moduleToolRegistryService.js +178 -0
  183. package/app/services/ai/toolCandidateSelectorService.js +781 -0
  184. package/app/services/auth/googleWebLinkService.js +80 -0
  185. package/app/services/auth/whatsappLoginLinkService.js +230 -0
  186. package/app/services/external/pokeApiService.js +398 -0
  187. package/app/services/group/groupMetadataService.js +311 -0
  188. package/app/services/infra/dbWriteQueue.js +874 -0
  189. package/app/services/infra/featureFlagService.js +131 -0
  190. package/app/services/infra/queueUtils.js +55 -0
  191. package/app/services/messaging/captchaService.js +491 -0
  192. package/app/services/messaging/messagePersistenceService.js +1 -0
  193. package/app/services/messaging/newsBroadcastService.js +347 -0
  194. package/app/services/sticker/stickerFocusService.js +347 -0
  195. package/app/services/sticker/stickerFocusService.test.js +43 -0
  196. package/app/store/aiPromptStore.js +38 -0
  197. package/app/store/conversationSessionStore.js +131 -0
  198. package/app/store/groupConfigStore.js +58 -0
  199. package/app/store/premiumUserStore.js +54 -0
  200. package/app/utils/antiLink/antiLinkModule.js +700 -0
  201. package/app/utils/http/getImageBufferModule.js +18 -0
  202. package/app/utils/json/jsonSanitizer.js +113 -0
  203. package/app/utils/json/jsonSanitizer.test.js +40 -0
  204. package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
  205. package/app/workers/aiLearningWorker.js +605 -0
  206. package/app/workers/commandConfigEnrichmentWorker.js +242 -0
  207. package/database/index.js +2075 -0
  208. package/database/init.js +151 -0
  209. package/database/migrations/.gitkeep +0 -0
  210. package/database/migrations/20260307_d0_hardening_down.sql +64 -0
  211. package/database/migrations/20260307_d0_hardening_up.sql +79 -0
  212. package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
  213. package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
  214. package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
  215. package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
  216. package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
  217. package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
  218. package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
  219. package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
  220. package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
  221. package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
  222. package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
  223. package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
  224. package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
  225. package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
  226. package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
  227. package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
  228. package/database/schema.sql +1186 -0
  229. package/docker-compose.yml +104 -0
  230. package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
  231. package/docs/audits/stickerCatalogController-symbols.md +58 -0
  232. package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
  233. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
  234. package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
  235. package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
  236. package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
  237. package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
  238. package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
  239. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
  240. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
  241. package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
  242. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
  243. package/docs/seo/satellite-page-template.md +116 -0
  244. package/docs/seo/satellite-pages-phase1.json +364 -0
  245. package/docs/wiki/Home.md +120 -0
  246. package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
  247. package/docs/wiki/recent-changes-2026-03-08.md +47 -0
  248. package/ecosystem.prod.config.cjs +135 -0
  249. package/eslint.config.js +89 -0
  250. package/index.js +488 -0
  251. package/ml/clip_classifier/Dockerfile +18 -0
  252. package/ml/clip_classifier/README.md +118 -0
  253. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  254. package/ml/clip_classifier/classifier.py +654 -0
  255. package/ml/clip_classifier/embedding_store.py +481 -0
  256. package/ml/clip_classifier/env_loader.py +15 -0
  257. package/ml/clip_classifier/llm_label_expander.py +144 -0
  258. package/ml/clip_classifier/main.py +213 -0
  259. package/ml/clip_classifier/requirements.txt +10 -0
  260. package/ml/clip_classifier/similarity_engine.py +74 -0
  261. package/new-logo.png +0 -0
  262. package/observability/alert-rules.yml +60 -0
  263. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  264. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  265. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  266. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  267. package/observability/loki-config.yml +38 -0
  268. package/observability/mysql-setup.sql +46 -0
  269. package/observability/prometheus.yml +35 -0
  270. package/observability/promtail-config.yml +84 -0
  271. package/observability/sticker-catalog-slo.md +83 -0
  272. package/observability/sticker-scale-hardening-rollout.md +128 -0
  273. package/package.json +144 -0
  274. package/public/apple-touch-icon.png +0 -0
  275. package/public/assets/css/commands-react.input.css +71 -0
  276. package/public/assets/css/create-pack-react.input.css +31 -0
  277. package/public/assets/css/home-react.input.css +106 -0
  278. package/public/assets/css/login-react.input.css +58 -0
  279. package/public/assets/css/stickers-react.input.css +18 -0
  280. package/public/assets/css/terms-react.input.css +115 -0
  281. package/public/assets/css/user-react.input.css +57 -0
  282. package/public/assets/images/brand-icon-192.png +0 -0
  283. package/public/assets/images/brand-logo-128.webp +0 -0
  284. package/public/assets/images/hero-banner-1280.jpg +0 -0
  285. package/public/comandos/commands-catalog.json +4517 -0
  286. package/public/css/api-docs.css +161 -0
  287. package/public/css/stickers-admin.css +1288 -0
  288. package/public/css/styles.css +679 -0
  289. package/public/css/systemadm/admin.css +474 -0
  290. package/public/css/systemadm/base.css +73 -0
  291. package/public/css/systemadm/components.css +662 -0
  292. package/public/css/systemadm/layout.css +229 -0
  293. package/public/css/systemadm/tokens.css +56 -0
  294. package/public/favicon-16x16.png +0 -0
  295. package/public/favicon-32x32.png +0 -0
  296. package/public/favicon.ico +0 -0
  297. package/public/js/apps/apiDocsApp.js +235 -0
  298. package/public/js/apps/commandsReactApp.js +528 -0
  299. package/public/js/apps/createPackApp.js +1646 -0
  300. package/public/js/apps/homeReactApp.js +942 -0
  301. package/public/js/apps/loginReactApp.js +496 -0
  302. package/public/js/apps/stickersAdminApp.js +1753 -0
  303. package/public/js/apps/stickersApp.js +3797 -0
  304. package/public/js/apps/termsReactApp.js +528 -0
  305. package/public/js/apps/userApp.js +2540 -0
  306. package/public/js/apps/userProfile/actions.js +66 -0
  307. package/public/js/apps/userReactApp.js +547 -0
  308. package/public/js/catalog.js +950 -0
  309. package/public/pages/api-docs.html +40 -0
  310. package/public/pages/aup.html +158 -0
  311. package/public/pages/comandos.html +41 -0
  312. package/public/pages/dpa.html +227 -0
  313. package/public/pages/home.html +45 -0
  314. package/public/pages/licenca.html +182 -0
  315. package/public/pages/login.html +40 -0
  316. package/public/pages/notice-and-takedown.html +234 -0
  317. package/public/pages/politica-de-privacidade.html +251 -0
  318. package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
  319. package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
  320. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
  321. package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
  322. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
  323. package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
  324. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
  325. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
  326. package/public/pages/stickers-admin.html +31 -0
  327. package/public/pages/stickers-create.html +41 -0
  328. package/public/pages/stickers.html +45 -0
  329. package/public/pages/suboperadores.html +237 -0
  330. package/public/pages/termos-de-uso-texto-integral.html +241 -0
  331. package/public/pages/termos-de-uso.html +41 -0
  332. package/public/pages/user-password-reset.html +32 -0
  333. package/public/pages/user-systemadm.html +508 -0
  334. package/public/pages/user.html +39 -0
  335. package/public/robots.txt +9 -0
  336. package/public/site.webmanifest +24 -0
  337. package/public/sitemap.xml +98 -0
  338. package/schemas/command-config.schema.json +582 -0
  339. package/scripts/baileys-compat-smoke.mjs +12 -0
  340. package/scripts/cache-bust.mjs +142 -0
  341. package/scripts/deploy.sh +916 -0
  342. package/scripts/email-broadcast-terms-update.mjs +170 -0
  343. package/scripts/enrich-command-discovery-fields.mjs +286 -0
  344. package/scripts/generate-command-config-schema.mjs +273 -0
  345. package/scripts/generate-commands-catalog.mjs +308 -0
  346. package/scripts/generate-module-agents.mjs +631 -0
  347. package/scripts/generate-seo-satellite-pages.mjs +400 -0
  348. package/scripts/github-deploy-notify.mjs +174 -0
  349. package/scripts/github-release-notify.mjs +219 -0
  350. package/scripts/release.sh +599 -0
  351. package/scripts/run-codeql-local.sh +116 -0
  352. package/scripts/run-prettier-all.mjs +25 -0
  353. package/scripts/security-smoketest.mjs +581 -0
  354. package/scripts/sticker-catalog-loadtest.mjs +210 -0
  355. package/scripts/sticker-worker-task.mjs +119 -0
  356. package/scripts/sync-readme-snapshot.mjs +133 -0
  357. package/scripts/validate-command-config-schema.mjs +130 -0
  358. package/scripts/validate-command-configs.mjs +15 -0
  359. package/scripts/wiki-sync.sh +191 -0
  360. package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
  361. package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
  362. package/server/auth/jwt/webJwtService.js +147 -0
  363. package/server/auth/stickerCatalogAuthContext.js +165 -0
  364. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
  365. package/server/auth/userPassword/index.js +14 -0
  366. package/server/auth/userPassword/userPasswordAuthService.js +422 -0
  367. package/server/auth/userPassword/userPasswordCrypto.js +199 -0
  368. package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
  369. package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
  370. package/server/auth/validation/authSchemas.js +236 -0
  371. package/server/auth/webAccount/webAccountHandlers.js +1434 -0
  372. package/server/controllers/admin/adminBanService.js +138 -0
  373. package/server/controllers/admin/adminPanelHandlers.js +2083 -0
  374. package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
  375. package/server/controllers/admin/systemAdminController.js +201 -0
  376. package/server/controllers/email/emailAutomationController.js +239 -0
  377. package/server/controllers/metricsController.js +21 -0
  378. package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
  379. package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
  380. package/server/controllers/sticker/stickerCatalogController.js +4700 -0
  381. package/server/controllers/system/contactController.js +115 -0
  382. package/server/controllers/system/githubController.js +137 -0
  383. package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
  384. package/server/controllers/system/storageController.js +154 -0
  385. package/server/controllers/system/systemController.js +135 -0
  386. package/server/controllers/system/systemMetricsController.js +156 -0
  387. package/server/controllers/system/visitController.js +90 -0
  388. package/server/controllers/userController.js +145 -0
  389. package/server/email/emailAutomationRuntime.js +225 -0
  390. package/server/email/emailAutomationService.js +125 -0
  391. package/server/email/emailOutboxRepository.js +282 -0
  392. package/server/email/emailTemplateService.js +480 -0
  393. package/server/email/emailTransportService.js +156 -0
  394. package/server/http/clientIp.js +95 -0
  395. package/server/http/httpRequestUtils.js +262 -0
  396. package/server/http/httpRequestUtils.test.js +80 -0
  397. package/server/http/httpServer.js +180 -0
  398. package/server/http/requestContext.js +20 -0
  399. package/server/http/siteRoutingUtils.js +87 -0
  400. package/server/index.js +1 -0
  401. package/server/middleware/cachePolicy.js +26 -0
  402. package/server/middleware/cachePolicyHelpers.js +1 -0
  403. package/server/middleware/endpointRateLimit.js +181 -0
  404. package/server/middleware/rateLimit.js +70 -0
  405. package/server/middleware/requireAdminAuth.js +48 -0
  406. package/server/middleware/securityHeaders.js +97 -0
  407. package/server/routes/admin/systemAdminRouter.js +64 -0
  408. package/server/routes/email/emailAutomationRouter.js +46 -0
  409. package/server/routes/health/healthRouter.js +41 -0
  410. package/server/routes/indexRouter.js +234 -0
  411. package/server/routes/metrics/metricsRouter.js +58 -0
  412. package/server/routes/static/staticPageRouter.js +134 -0
  413. package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
  414. package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
  415. package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
  416. package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
  417. package/server/routes/sticker/catalogRouter.js +77 -0
  418. package/server/routes/sticker/stickerApiRouter.js +84 -0
  419. package/server/routes/sticker/stickerDataRouter.js +145 -0
  420. package/server/routes/sticker/stickerSiteRouter.js +43 -0
  421. package/server/routes/user/userApiPaths.js +66 -0
  422. package/server/routes/user/userRouter.js +65 -0
  423. package/server/utils/safePath.js +26 -0
  424. package/utils/logger/loggerModule.js +35 -0
  425. package/vite.config.mjs +38 -0
@@ -0,0 +1,2540 @@
1
+ const DEFAULT_API_BASE_PATH = '/api';
2
+ const DEFAULT_STICKERS_PATH = '/stickers';
3
+ const DEFAULT_LOGIN_PATH = '/login';
4
+ const FALLBACK_AVATAR = 'https://iili.io/FC3FABe.jpg';
5
+
6
+ const root = document.getElementById('user-app-root');
7
+
8
+ if (root) {
9
+ const ui = {
10
+ status: document.getElementById('user-status'),
11
+ error: document.getElementById('user-error'),
12
+ profile: document.getElementById('user-profile'),
13
+ avatar: document.getElementById('user-avatar'),
14
+ topAvatar: document.getElementById('user-top-avatar'),
15
+ topAdminName: document.getElementById('user-top-admin-name'),
16
+ envBadge: document.getElementById('user-env-badge'),
17
+ globalStatus: document.getElementById('user-global-status'),
18
+ globalStatusText: document.getElementById('user-global-status-text'),
19
+ compactToggle: document.getElementById('user-compact-toggle'),
20
+ navLinks: Array.from(document.querySelectorAll('#user-admin-nav .nav-link')),
21
+ viewport: document.querySelector('.viewport'),
22
+ toastStack: document.getElementById('user-admin-toast-stack'),
23
+ name: document.getElementById('user-name'),
24
+ email: document.getElementById('user-email'),
25
+ whatsapp: document.getElementById('user-whatsapp'),
26
+ grid: document.getElementById('user-grid'),
27
+ metricPacks: document.getElementById('metric-packs'),
28
+ metricStickers: document.getElementById('metric-stickers'),
29
+ metricDownloads: document.getElementById('metric-downloads'),
30
+ metricLikes: document.getElementById('metric-likes'),
31
+ summary: document.getElementById('user-summary'),
32
+ ownerJid: document.getElementById('user-owner-jid'),
33
+ googleSub: document.getElementById('user-google-sub'),
34
+ expiresAt: document.getElementById('user-expires-at'),
35
+ actions: document.getElementById('user-actions'),
36
+ chatLink: document.getElementById('user-chat-link'),
37
+ logoutBtn: document.getElementById('user-logout-btn'),
38
+ manageHeadLink: document.getElementById('user-manage-head-link'),
39
+ manageMainLink: document.getElementById('user-manage-main-link'),
40
+ currentYear: document.getElementById('user-current-year'),
41
+
42
+ adminPanel: document.getElementById('user-admin-panel'),
43
+ adminRole: document.getElementById('user-admin-role'),
44
+ adminStatus: document.getElementById('user-admin-status'),
45
+ adminError: document.getElementById('user-admin-error'),
46
+ adminUnlockForm: document.getElementById('user-admin-unlock-form'),
47
+ adminPassword: document.getElementById('user-admin-password'),
48
+ adminUnlockBtn: document.getElementById('user-admin-unlock-btn'),
49
+ adminOverview: document.getElementById('user-admin-overview'),
50
+ adminLayout: document.getElementById('user-admin-layout'),
51
+ adminRefreshBtn: document.getElementById('user-admin-refresh-btn'),
52
+ adminLogoutBtn: document.getElementById('user-admin-logout-btn'),
53
+ adminCarouselNav: document.getElementById('user-admin-carousel-nav'),
54
+ adminCarouselPrevBtn: document.getElementById('user-admin-carousel-prev'),
55
+ adminCarouselNextBtn: document.getElementById('user-admin-carousel-next'),
56
+ adminCarouselCounter: document.getElementById('user-admin-carousel-counter'),
57
+
58
+ adminBotsOnline: document.getElementById('user-admin-bots-online'),
59
+ adminMessagesToday: document.getElementById('user-admin-messages-today'),
60
+ adminSpamBlocked: document.getElementById('user-admin-spam-blocked'),
61
+ adminUptime: document.getElementById('user-admin-uptime'),
62
+ adminErrors5xx: document.getElementById('user-admin-errors-5xx'),
63
+ adminTotalPacks: document.getElementById('user-admin-total-packs'),
64
+ adminTotalStickers: document.getElementById('user-admin-total-stickers'),
65
+ adminActiveBans: document.getElementById('user-admin-active-bans'),
66
+ adminKnownUsers: document.getElementById('user-admin-known-users'),
67
+ adminActiveSessions: document.getElementById('user-admin-active-sessions'),
68
+ adminVisits24h: document.getElementById('user-admin-visits-24h'),
69
+ adminVisits7d: document.getElementById('user-admin-visits-7d'),
70
+ adminUniqueVisitors7d: document.getElementById('user-admin-unique-visitors-7d'),
71
+ adminBotsOnlineContext: document.getElementById('user-admin-bots-online-context'),
72
+ adminMessagesTodayContext: document.getElementById('user-admin-messages-today-context'),
73
+ adminUptimeContext: document.getElementById('user-admin-uptime-context'),
74
+ adminErrors5xxContext: document.getElementById('user-admin-errors-5xx-context'),
75
+ adminTotalPacksContext: document.getElementById('user-admin-total-packs-context'),
76
+ adminTotalStickersContext: document.getElementById('user-admin-total-stickers-context'),
77
+ adminSpamBlockedContext: document.getElementById('user-admin-spam-blocked-context'),
78
+ adminActiveBansContext: document.getElementById('user-admin-active-bans-context'),
79
+ adminKnownUsersContext: document.getElementById('user-admin-known-users-context'),
80
+ adminActiveSessionsContext: document.getElementById('user-admin-active-sessions-context'),
81
+ adminVisits24hContext: document.getElementById('user-admin-visits-24h-context'),
82
+ adminVisits7dContext: document.getElementById('user-admin-visits-7d-context'),
83
+ adminUniqueVisitors7dContext: document.getElementById('user-admin-unique-visitors-7d-context'),
84
+ adminLastUpdated: document.getElementById('user-admin-last-updated'),
85
+ securitySession: document.getElementById('user-security-session'),
86
+ securityEncryption: document.getElementById('user-security-encryption'),
87
+ securityIp: document.getElementById('user-security-ip'),
88
+ security2fa: document.getElementById('user-security-2fa'),
89
+
90
+ adminHealthCpu: document.getElementById('user-admin-health-cpu'),
91
+ adminHealthRam: document.getElementById('user-admin-health-ram'),
92
+ adminHealthLatency: document.getElementById('user-admin-health-latency'),
93
+ adminHealthQueue: document.getElementById('user-admin-health-queue'),
94
+ adminHealthDb: document.getElementById('user-admin-health-db'),
95
+ adminHealthCpuMeta: document.getElementById('user-admin-health-cpu-meta'),
96
+ adminHealthRamMeta: document.getElementById('user-admin-health-ram-meta'),
97
+ adminHealthLatencyMeta: document.getElementById('user-admin-health-latency-meta'),
98
+ adminHealthQueueMeta: document.getElementById('user-admin-health-queue-meta'),
99
+ adminHealthDbMeta: document.getElementById('user-admin-health-db-meta'),
100
+ adminHealthCpuBar: document.getElementById('user-admin-health-cpu-bar'),
101
+ adminHealthRamBar: document.getElementById('user-admin-health-ram-bar'),
102
+ adminHealthLatencyBar: document.getElementById('user-admin-health-latency-bar'),
103
+ adminHealthQueueBar: document.getElementById('user-admin-health-queue-bar'),
104
+ adminHealthDbBadge: document.getElementById('user-admin-health-db-badge'),
105
+
106
+ adminModerationList: document.getElementById('user-admin-moderation-list'),
107
+ adminModerationFilterSeverity: document.getElementById('user-admin-moderation-filter-severity'),
108
+ adminModerationFilterType: document.getElementById('user-admin-moderation-filter-type'),
109
+ adminModerationPagination: document.getElementById('user-admin-moderation-pagination'),
110
+ adminModerationPageMeta: document.getElementById('user-admin-moderation-page-meta'),
111
+ adminModerationPageCounter: document.getElementById('user-admin-moderation-page-counter'),
112
+ adminModerationPrevBtn: document.getElementById('user-admin-moderation-prev'),
113
+ adminModerationNextBtn: document.getElementById('user-admin-moderation-next'),
114
+ adminSessionsList: document.getElementById('user-admin-sessions-list'),
115
+ adminSessionsPagination: document.getElementById('user-admin-sessions-pagination'),
116
+ adminSessionsPageMeta: document.getElementById('user-admin-sessions-page-meta'),
117
+ adminSessionsPageCounter: document.getElementById('user-admin-sessions-page-counter'),
118
+ adminSessionsPrevBtn: document.getElementById('user-admin-sessions-prev'),
119
+ adminSessionsNextBtn: document.getElementById('user-admin-sessions-next'),
120
+ adminUsersList: document.getElementById('user-admin-users-list'),
121
+ adminUsersPagination: document.getElementById('user-admin-users-pagination'),
122
+ adminUsersPageMeta: document.getElementById('user-admin-users-page-meta'),
123
+ adminUsersPageCounter: document.getElementById('user-admin-users-page-counter'),
124
+ adminUsersPrevBtn: document.getElementById('user-admin-users-prev'),
125
+ adminUsersNextBtn: document.getElementById('user-admin-users-next'),
126
+ adminBansList: document.getElementById('user-admin-bans-list'),
127
+ adminAuditList: document.getElementById('user-admin-audit-list'),
128
+ adminAuditFilterStatus: document.getElementById('user-admin-audit-filter-status'),
129
+ adminAuditSearch: document.getElementById('user-admin-audit-search'),
130
+ adminAuditPagination: document.getElementById('user-admin-audit-pagination'),
131
+ adminAuditPageMeta: document.getElementById('user-admin-audit-page-meta'),
132
+ adminAuditPageCounter: document.getElementById('user-admin-audit-page-counter'),
133
+ adminAuditPrevBtn: document.getElementById('user-admin-audit-prev'),
134
+ adminAuditNextBtn: document.getElementById('user-admin-audit-next'),
135
+ adminFlagsList: document.getElementById('user-admin-flags-list'),
136
+ adminAlertsList: document.getElementById('user-admin-alerts-list'),
137
+ adminAlertsPagination: document.getElementById('user-admin-alerts-pagination'),
138
+ adminAlertsPageMeta: document.getElementById('user-admin-alerts-page-meta'),
139
+ adminAlertsPageCounter: document.getElementById('user-admin-alerts-page-counter'),
140
+ adminAlertsPrevBtn: document.getElementById('user-admin-alerts-prev'),
141
+ adminAlertsNextBtn: document.getElementById('user-admin-alerts-next'),
142
+ adminOpsStatus: document.getElementById('user-admin-ops-status'),
143
+ riskCpu: document.getElementById('user-risk-cpu'),
144
+ riskSpam: document.getElementById('user-risk-spam'),
145
+ riskBans: document.getElementById('user-risk-bans'),
146
+ riskErrors: document.getElementById('user-risk-errors'),
147
+
148
+ adminSearchForm: document.getElementById('user-admin-search-form'),
149
+ adminSearchInput: document.getElementById('user-admin-search-input'),
150
+ adminSearchBtn: document.getElementById('user-admin-search-btn'),
151
+ adminSearchResults: document.getElementById('user-admin-search-results'),
152
+
153
+ adminExportMetricsJsonBtn: document.getElementById('user-admin-export-metrics-json'),
154
+ adminExportMetricsCsvBtn: document.getElementById('user-admin-export-metrics-csv'),
155
+ adminExportEventsJsonBtn: document.getElementById('user-admin-export-events-json'),
156
+ adminExportEventsCsvBtn: document.getElementById('user-admin-export-events-csv'),
157
+ adminOpButtons: Array.from(document.querySelectorAll('[data-admin-op-action]')),
158
+ };
159
+
160
+ const state = {
161
+ apiBasePath: String(root.dataset.apiBasePath || DEFAULT_API_BASE_PATH).trim() || DEFAULT_API_BASE_PATH,
162
+ stickersPath: String(root.dataset.stickersPath || DEFAULT_STICKERS_PATH).trim() || DEFAULT_STICKERS_PATH,
163
+ loginPath: String(root.dataset.loginPath || DEFAULT_LOGIN_PATH).trim() || DEFAULT_LOGIN_PATH,
164
+ botPhone: '',
165
+ adminBusy: false,
166
+ adminStatusPayload: null,
167
+ adminOverviewPayload: null,
168
+ previousAdminOverviewPayload: null,
169
+ adminSearchPayload: null,
170
+ adminOpsMessage: '',
171
+ compactMode: false,
172
+ moderationFilterSeverity: 'all',
173
+ moderationFilterType: 'all',
174
+ moderationPage: 1,
175
+ moderationPageSize: 6,
176
+ usersPage: 1,
177
+ usersPageSize: 6,
178
+ sessionsPage: 1,
179
+ sessionsPageSize: 6,
180
+ auditFilterStatus: 'all',
181
+ auditSearchQuery: '',
182
+ auditPage: 1,
183
+ auditPageSize: 6,
184
+ alertsPage: 1,
185
+ alertsPageSize: 6,
186
+ adminCarouselIndex: 0,
187
+ };
188
+
189
+ const sessionApiPath = `${state.apiBasePath}/auth/google/session`;
190
+ const myProfileApiPath = `${state.apiBasePath}/me`;
191
+ const botContactApiPath = `${state.apiBasePath}/bot-contact`;
192
+ const adminSessionApiPath = `${state.apiBasePath}/admin/session`;
193
+ const adminOverviewApiPath = `${state.apiBasePath}/admin/overview`;
194
+ const adminForceLogoutApiPath = `${state.apiBasePath}/admin/users/force-logout`;
195
+ const adminFeatureFlagsApiPath = `${state.apiBasePath}/admin/feature-flags`;
196
+ const adminOpsApiPath = `${state.apiBasePath}/admin/ops`;
197
+ const adminSearchApiPath = `${state.apiBasePath}/admin/search`;
198
+ const adminExportApiPath = `${state.apiBasePath}/admin/export`;
199
+ const adminBansApiPath = `${state.apiBasePath}/admin/bans`;
200
+ const COMPACT_MODE_STORAGE_KEY = 'omnizap_admin_compact_mode_v1';
201
+ const CRITICAL_ADMIN_ACTIONS = new Set(['restart_worker', 'clear_cache']);
202
+
203
+ const setText = (el, value) => {
204
+ if (!el) return;
205
+ el.textContent = String(value || '');
206
+ };
207
+
208
+ const clearNode = (el) => {
209
+ if (!el) return;
210
+ while (el.firstChild) el.removeChild(el.firstChild);
211
+ };
212
+
213
+ const showError = (message) => {
214
+ if (!ui.error) return;
215
+ const safeMessage = String(message || '').trim();
216
+ ui.error.hidden = !safeMessage;
217
+ if (safeMessage) ui.error.textContent = safeMessage;
218
+ };
219
+
220
+ const showAdminError = (message) => {
221
+ if (!ui.adminError) return;
222
+ const safeMessage = String(message || '').trim();
223
+ ui.adminError.hidden = !safeMessage;
224
+ if (safeMessage) {
225
+ ui.adminError.textContent = safeMessage;
226
+ showToast({ kind: 'error', title: 'Erro', message: safeMessage });
227
+ }
228
+ };
229
+
230
+ const normalizeSeverity = (value, fallback = 'low') => {
231
+ const normalized = String(value || '')
232
+ .trim()
233
+ .toLowerCase();
234
+ if (['critical', 'high', 'medium', 'low'].includes(normalized)) return normalized;
235
+ if (normalized === 'error') return 'high';
236
+ if (normalized === 'warn' || normalized === 'warning') return 'medium';
237
+ return fallback;
238
+ };
239
+
240
+ const setRiskPill = (el, text, tone = 'normal') => {
241
+ if (!el) return;
242
+ el.textContent = text;
243
+ el.title = text;
244
+ el.classList.remove('warn', 'danger');
245
+ if (tone === 'warn') el.classList.add('warn');
246
+ if (tone === 'danger') el.classList.add('danger');
247
+ };
248
+
249
+ const setHealthMeter = (el, value, { max = 100, warnAt = 70, dangerAt = 90 } = {}) => {
250
+ if (!el) return;
251
+ const numeric = Number(value);
252
+ const percent = Number.isFinite(numeric) ? Math.max(0, Math.min(100, (numeric / max) * 100)) : 0;
253
+ el.style.width = `${percent.toFixed(1)}%`;
254
+ el.classList.remove('warn', 'danger');
255
+ if (percent >= dangerAt) {
256
+ el.classList.add('danger');
257
+ } else if (percent >= warnAt) {
258
+ el.classList.add('warn');
259
+ }
260
+ };
261
+
262
+ const showToast = ({ kind = 'success', title = 'Status', message = '' } = {}) => {
263
+ if (!ui.toastStack) return;
264
+ const text = String(message || '').trim();
265
+ if (!text) return;
266
+
267
+ const toast = document.createElement('article');
268
+ toast.className = `toast ${kind}`;
269
+
270
+ const headline = document.createElement('strong');
271
+ headline.textContent = String(title || 'Status').trim() || 'Status';
272
+ toast.appendChild(headline);
273
+
274
+ const body = document.createElement('p');
275
+ body.textContent = text;
276
+ toast.appendChild(body);
277
+
278
+ ui.toastStack.appendChild(toast);
279
+ window.setTimeout(() => {
280
+ toast.remove();
281
+ }, 3800);
282
+ };
283
+
284
+ const normalizeDigits = (value) => String(value || '').replace(/\D+/g, '');
285
+ const normalizeString = (value) => String(value || '').trim();
286
+ const isObject = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
287
+ const toPositiveInt = (value, fallback = 1) => {
288
+ const numeric = Number(value);
289
+ if (!Number.isFinite(numeric) || numeric < 1) return fallback;
290
+ return Math.floor(numeric);
291
+ };
292
+
293
+ const setActiveNavLink = (targetId) => {
294
+ if (!ui.navLinks.length) return;
295
+ const normalizedTarget = normalizeString(targetId).replace(/^#/, '');
296
+ for (const link of ui.navLinks) {
297
+ const href = normalizeString(link.getAttribute('href'));
298
+ const linkTarget = href.startsWith('#') ? href.slice(1) : '';
299
+ link.classList.toggle('active', Boolean(linkTarget) && linkTarget === normalizedTarget);
300
+ }
301
+ };
302
+
303
+ const getAdminSubpageSections = () => {
304
+ if (!ui.adminLayout) return [];
305
+ return Array.from(ui.adminLayout.querySelectorAll('.section[data-admin-page]'));
306
+ };
307
+
308
+ const resolveAdminSubpageTarget = (value) => {
309
+ const sections = getAdminSubpageSections();
310
+ if (!sections.length) return '';
311
+ const requested = normalizeString(value).replace(/^#/, '');
312
+ if (requested) {
313
+ const hasMatch = sections.some((section) => {
314
+ const pageKey = normalizeString(section.dataset.adminPage || section.id).replace(/^#/, '');
315
+ return pageKey === requested || normalizeString(section.id).replace(/^#/, '') === requested;
316
+ });
317
+ if (hasMatch) return requested;
318
+ }
319
+ const first = sections[0];
320
+ return normalizeString(first?.dataset?.adminPage || first?.id).replace(/^#/, '');
321
+ };
322
+
323
+ const activateAdminSubpage = (value, { syncHash = false, resetScroll = true } = {}) => {
324
+ const sections = getAdminSubpageSections();
325
+ if (!sections.length) return false;
326
+
327
+ const target = resolveAdminSubpageTarget(value);
328
+ if (!target) return false;
329
+
330
+ for (const section of sections) {
331
+ const pageKey = normalizeString(section.dataset.adminPage || section.id).replace(/^#/, '');
332
+ const sectionId = normalizeString(section.id).replace(/^#/, '');
333
+ const visible = pageKey === target || sectionId === target;
334
+ section.hidden = !visible;
335
+ section.setAttribute('aria-hidden', visible ? 'false' : 'true');
336
+ }
337
+
338
+ setActiveNavLink(target);
339
+ if (syncHash) {
340
+ const nextHash = `#${target}`;
341
+ if (window.location.hash !== nextHash) {
342
+ window.history.replaceState(null, '', nextHash);
343
+ }
344
+ }
345
+ if (resetScroll && ui.viewport) {
346
+ ui.viewport.scrollTo({ top: 0, behavior: 'auto' });
347
+ }
348
+ return true;
349
+ };
350
+
351
+ const getAdminCarouselSlides = () => {
352
+ if (!ui.adminLayout) return [];
353
+ return Array.from(ui.adminLayout.children).filter((node) => node instanceof Element && node.classList.contains('section'));
354
+ };
355
+
356
+ const isCarouselMode = () => Boolean(ui.adminLayout && ui.adminLayout.dataset.carouselEnabled === 'true' && document.body.classList.contains('compact'));
357
+
358
+ const updateAdminCarouselControls = (slides = getAdminCarouselSlides()) => {
359
+ const carouselMode = isCarouselMode();
360
+ const total = slides.length;
361
+ if (ui.adminCarouselNav) ui.adminCarouselNav.hidden = !carouselMode || total <= 1;
362
+ if (!carouselMode) {
363
+ if (ui.adminLayout) ui.adminLayout.style.blockSize = '';
364
+ if (ui.adminCarouselPrevBtn) ui.adminCarouselPrevBtn.disabled = true;
365
+ if (ui.adminCarouselNextBtn) ui.adminCarouselNextBtn.disabled = true;
366
+ if (ui.adminCarouselCounter) ui.adminCarouselCounter.textContent = `${Math.min(state.adminCarouselIndex + 1, Math.max(total, 1))} / ${Math.max(total, 1)}`;
367
+ return;
368
+ }
369
+
370
+ if (!total) {
371
+ if (ui.adminCarouselCounter) ui.adminCarouselCounter.textContent = '0 / 0';
372
+ if (ui.adminCarouselPrevBtn) ui.adminCarouselPrevBtn.disabled = true;
373
+ if (ui.adminCarouselNextBtn) ui.adminCarouselNextBtn.disabled = true;
374
+ state.adminCarouselIndex = 0;
375
+ return;
376
+ }
377
+
378
+ const nextIndex = Math.max(0, Math.min(state.adminCarouselIndex, total - 1));
379
+ state.adminCarouselIndex = nextIndex;
380
+ const activeSlide = slides[nextIndex];
381
+ if (ui.adminLayout && activeSlide) {
382
+ const viewportRatio = state.compactMode ? 0.62 : 0.68;
383
+ const maxHeight = Math.max(300, Math.round(window.innerHeight * viewportRatio));
384
+ const targetHeight = Math.min(Math.max(activeSlide.scrollHeight, 300), maxHeight);
385
+ ui.adminLayout.style.blockSize = `${targetHeight}px`;
386
+ }
387
+
388
+ if (ui.adminCarouselCounter) {
389
+ ui.adminCarouselCounter.textContent = `${nextIndex + 1} / ${total}`;
390
+ }
391
+ if (ui.adminCarouselPrevBtn) ui.adminCarouselPrevBtn.disabled = nextIndex <= 0;
392
+ if (ui.adminCarouselNextBtn) ui.adminCarouselNextBtn.disabled = nextIndex >= total - 1;
393
+ };
394
+
395
+ const scrollAdminCarouselToIndex = (index, { behavior = 'smooth' } = {}) => {
396
+ if (!isCarouselMode()) return false;
397
+ const slides = getAdminCarouselSlides();
398
+ if (!slides.length || !ui.adminLayout) return false;
399
+
400
+ const boundedIndex = Math.max(0, Math.min(Number(index) || 0, slides.length - 1));
401
+ state.adminCarouselIndex = boundedIndex;
402
+ const targetSlide = slides[boundedIndex];
403
+ ui.adminLayout.scrollTo({
404
+ left: targetSlide.offsetLeft,
405
+ behavior,
406
+ });
407
+ updateAdminCarouselControls(slides);
408
+ if (targetSlide.id) setActiveNavLink(targetSlide.id);
409
+ return true;
410
+ };
411
+
412
+ const scrollAdminCarouselToId = (targetId, options = {}) => {
413
+ if (!isCarouselMode()) return false;
414
+ const normalizedTarget = normalizeString(targetId).replace(/^#/, '');
415
+ if (!normalizedTarget) return false;
416
+ const slides = getAdminCarouselSlides();
417
+ const nextIndex = slides.findIndex((slide) => slide.id === normalizedTarget);
418
+ if (nextIndex < 0) return false;
419
+ return scrollAdminCarouselToIndex(nextIndex, options);
420
+ };
421
+
422
+ const bindAdminCarousel = () => {
423
+ if (!ui.adminLayout) return;
424
+ const slides = getAdminCarouselSlides();
425
+ if (!slides.length) return;
426
+
427
+ let rafToken = 0;
428
+ const syncFromScroll = () => {
429
+ if (!isCarouselMode()) return;
430
+ if (!ui.adminLayout) return;
431
+ const currentLeft = ui.adminLayout.scrollLeft;
432
+ let closestIndex = state.adminCarouselIndex;
433
+ let closestDistance = Number.POSITIVE_INFINITY;
434
+
435
+ for (let index = 0; index < slides.length; index += 1) {
436
+ const distance = Math.abs(slides[index].offsetLeft - currentLeft);
437
+ if (distance < closestDistance) {
438
+ closestDistance = distance;
439
+ closestIndex = index;
440
+ }
441
+ }
442
+
443
+ if (closestIndex !== state.adminCarouselIndex) {
444
+ state.adminCarouselIndex = closestIndex;
445
+ if (slides[closestIndex]?.id) setActiveNavLink(slides[closestIndex].id);
446
+ }
447
+ updateAdminCarouselControls(slides);
448
+ };
449
+
450
+ ui.adminLayout.addEventListener(
451
+ 'scroll',
452
+ () => {
453
+ if (rafToken) return;
454
+ rafToken = window.requestAnimationFrame(() => {
455
+ rafToken = 0;
456
+ syncFromScroll();
457
+ });
458
+ },
459
+ { passive: true },
460
+ );
461
+
462
+ if (ui.adminCarouselPrevBtn) {
463
+ ui.adminCarouselPrevBtn.addEventListener('click', () => {
464
+ if (!isCarouselMode()) return;
465
+ scrollAdminCarouselToIndex(state.adminCarouselIndex - 1);
466
+ });
467
+ }
468
+
469
+ if (ui.adminCarouselNextBtn) {
470
+ ui.adminCarouselNextBtn.addEventListener('click', () => {
471
+ if (!isCarouselMode()) return;
472
+ scrollAdminCarouselToIndex(state.adminCarouselIndex + 1);
473
+ });
474
+ }
475
+
476
+ for (const link of ui.navLinks) {
477
+ const href = normalizeString(link.getAttribute('href'));
478
+ if (!href.startsWith('#')) continue;
479
+ const targetId = href.slice(1);
480
+ if (!targetId) continue;
481
+ link.addEventListener('click', (event) => {
482
+ if (!isCarouselMode()) return;
483
+ if (!scrollAdminCarouselToId(targetId)) return;
484
+ event.preventDefault();
485
+ });
486
+ }
487
+
488
+ window.addEventListener('resize', () => {
489
+ if (!isCarouselMode()) {
490
+ updateAdminCarouselControls();
491
+ return;
492
+ }
493
+ scrollAdminCarouselToIndex(state.adminCarouselIndex, { behavior: 'auto' });
494
+ });
495
+
496
+ const hashTarget = normalizeString(window.location.hash);
497
+ if (isCarouselMode() && hashTarget && scrollAdminCarouselToId(hashTarget, { behavior: 'auto' })) {
498
+ return;
499
+ }
500
+ updateAdminCarouselControls(slides);
501
+ };
502
+
503
+ const setPaginationHidden = ({ wrapper, counter, meta }) => {
504
+ if (wrapper) wrapper.hidden = true;
505
+ if (counter) counter.textContent = '1 / 1';
506
+ if (meta) meta.textContent = '';
507
+ };
508
+
509
+ const paginateItems = ({ items = [], statePageKey = '', pageSize = 10, wrapper = null, counter = null, meta = null, prevBtn = null, nextBtn = null } = {}) => {
510
+ const safeItems = Array.isArray(items) ? items : [];
511
+ const safePageSize = toPositiveInt(pageSize, 10);
512
+
513
+ if (!safeItems.length) {
514
+ setPaginationHidden({ wrapper, counter, meta });
515
+ if (prevBtn) prevBtn.disabled = true;
516
+ if (nextBtn) nextBtn.disabled = true;
517
+ return [];
518
+ }
519
+
520
+ const totalPages = Math.max(1, Math.ceil(safeItems.length / safePageSize));
521
+ const page = Math.min(Math.max(1, toPositiveInt(state[statePageKey], 1)), totalPages);
522
+ state[statePageKey] = page;
523
+
524
+ const startIndex = (page - 1) * safePageSize;
525
+ const endIndex = Math.min(startIndex + safePageSize, safeItems.length);
526
+ const pageItems = safeItems.slice(startIndex, endIndex);
527
+
528
+ if (wrapper) wrapper.hidden = safeItems.length <= safePageSize;
529
+ if (counter) counter.textContent = `${page} / ${totalPages}`;
530
+ if (meta) meta.textContent = `Mostrando ${startIndex + 1}-${endIndex} de ${safeItems.length}`;
531
+ if (prevBtn) prevBtn.disabled = page <= 1;
532
+ if (nextBtn) nextBtn.disabled = page >= totalPages;
533
+
534
+ return pageItems;
535
+ };
536
+
537
+ const formatPhone = (digits) => {
538
+ const value = normalizeDigits(digits);
539
+ if (!value) return '';
540
+ if (value.length <= 4) return value;
541
+ return `${value.slice(0, 2)} ${value.slice(2, -4)}-${value.slice(-4)}`.trim();
542
+ };
543
+
544
+ const formatNumber = (value) =>
545
+ new Intl.NumberFormat('pt-BR', {
546
+ maximumFractionDigits: 0,
547
+ }).format(Math.max(0, Number(value || 0)));
548
+
549
+ const formatDateTime = (value) => {
550
+ const ms = Date.parse(String(value || ''));
551
+ if (!Number.isFinite(ms)) return 'n/d';
552
+ return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(ms));
553
+ };
554
+
555
+ const formatRelativeTime = (value) => {
556
+ const ms = Date.parse(String(value || ''));
557
+ if (!Number.isFinite(ms)) return 'n/d';
558
+ const deltaMs = Date.now() - ms;
559
+ const absMs = Math.abs(deltaMs);
560
+ const suffix = deltaMs >= 0 ? 'atrás' : 'à frente';
561
+ if (absMs < 1000) return 'agora';
562
+ const seconds = Math.round(absMs / 1000);
563
+ if (seconds < 60) return `${seconds}s ${suffix}`;
564
+ const minutes = Math.round(seconds / 60);
565
+ if (minutes < 60) return `${minutes}m ${suffix}`;
566
+ const hours = Math.round(minutes / 60);
567
+ if (hours < 24) return `${hours}h ${suffix}`;
568
+ const days = Math.round(hours / 24);
569
+ return `${days}d ${suffix}`;
570
+ };
571
+
572
+ const formatPercent = (value) => {
573
+ const numeric = Number(value);
574
+ if (!Number.isFinite(numeric)) return 'n/d';
575
+ return `${numeric.toFixed(1)}%`;
576
+ };
577
+
578
+ const formatMilliseconds = (value) => {
579
+ const numeric = Number(value);
580
+ if (!Number.isFinite(numeric)) return 'n/d';
581
+ return `${Math.round(numeric)} ms`;
582
+ };
583
+
584
+ const formatIntegerOrNd = (value) => {
585
+ const numeric = Number(value);
586
+ if (!Number.isFinite(numeric)) return 'n/d';
587
+ return formatNumber(numeric);
588
+ };
589
+
590
+ const toFiniteNumber = (value, fallback = 0) => {
591
+ const numeric = Number(value);
592
+ return Number.isFinite(numeric) ? numeric : fallback;
593
+ };
594
+
595
+ const formatDeltaLabel = (current, previous, { percent = true, suffix = '' } = {}) => {
596
+ const curr = Number(current);
597
+ const prev = Number(previous);
598
+ if (!Number.isFinite(curr) || !Number.isFinite(prev)) return 'n/d';
599
+
600
+ const delta = curr - prev;
601
+ const prefix = delta > 0 ? '+' : '';
602
+ if (!percent) {
603
+ return `${prefix}${formatNumber(delta)}${suffix}`.trim();
604
+ }
605
+ if (prev === 0) {
606
+ return delta === 0 ? '0.0%' : `${prefix}100.0%`;
607
+ }
608
+ const ratio = (delta / Math.abs(prev)) * 100;
609
+ return `${ratio >= 0 ? '+' : ''}${ratio.toFixed(1)}%`;
610
+ };
611
+
612
+ const setMetricContext = (el, value) => {
613
+ if (!el) return;
614
+ el.textContent = String(value || '').trim() || 'n/d';
615
+ };
616
+
617
+ const setButtonProcessing = (button, processingText = 'Processando...') => {
618
+ if (!button) return;
619
+ if (!button.dataset.idleText) {
620
+ button.dataset.idleText = button.textContent || '';
621
+ }
622
+ button.disabled = true;
623
+ button.dataset.state = 'processing';
624
+ button.textContent = processingText;
625
+ };
626
+
627
+ const setButtonIdle = (button) => {
628
+ if (!button) return;
629
+ button.disabled = false;
630
+ button.dataset.state = 'idle';
631
+ if (button.dataset.idleText) {
632
+ button.textContent = button.dataset.idleText;
633
+ }
634
+ };
635
+
636
+ const flashButtonSuccess = (button, successText = 'Concluído', timeoutMs = 1200) => {
637
+ if (!button) return;
638
+ const idleText = button.dataset.idleText || button.textContent || '';
639
+ button.textContent = successText;
640
+ button.dataset.state = 'success';
641
+ window.setTimeout(() => {
642
+ button.dataset.state = 'idle';
643
+ button.textContent = idleText;
644
+ }, timeoutMs);
645
+ };
646
+
647
+ const resolveEnvironmentLabel = () => {
648
+ const host = String(window.location.hostname || '').toLowerCase();
649
+ if (host.includes('localhost') || host.includes('127.0.0.1') || host.includes('staging') || host.includes('dev')) return 'Staging';
650
+ return 'Production';
651
+ };
652
+
653
+ const applyEnvironmentBadge = () => {
654
+ if (!ui.envBadge) return;
655
+ ui.envBadge.textContent = resolveEnvironmentLabel();
656
+ };
657
+
658
+ const setGlobalStatusChip = (tone = 'online', text = 'Online') => {
659
+ if (!ui.globalStatus || !ui.globalStatusText) return;
660
+ ui.globalStatus.classList.remove('warning', 'incident');
661
+ if (tone === 'warning') ui.globalStatus.classList.add('warning');
662
+ if (tone === 'incident') ui.globalStatus.classList.add('incident');
663
+ ui.globalStatusText.textContent = text;
664
+ };
665
+
666
+ const updateSecurityStrip = (sessionData = null) => {
667
+ const ownerJid = normalizeString(sessionData?.owner_jid);
668
+ const secureProtocol = window.location.protocol === 'https:';
669
+ const ipLabel = 'IP mascarado';
670
+ const isTwoFactorPossible = Boolean(ownerJid);
671
+
672
+ setText(ui.securitySession, secureProtocol ? 'Sessão segura (HTTPS)' : 'Sessão sem HTTPS');
673
+ setText(ui.securityEncryption, secureProtocol ? 'Criptografia ativa TLS' : 'Criptografia limitada');
674
+ setText(ui.securityIp, ipLabel);
675
+ setText(ui.security2fa, isTwoFactorPossible ? '2FA: disponível' : '2FA: n/d');
676
+ };
677
+
678
+ const buildRegex = (query) => {
679
+ const source = String(query || '')
680
+ .trim()
681
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
682
+ if (!source) return null;
683
+ return new RegExp(source, 'ig');
684
+ };
685
+
686
+ const appendHighlightedText = (target, text, query) => {
687
+ if (!target) return;
688
+ const content = String(text || '');
689
+ const pattern = buildRegex(query);
690
+ if (!pattern) {
691
+ target.textContent = content;
692
+ return;
693
+ }
694
+
695
+ target.textContent = '';
696
+ let lastIndex = 0;
697
+ for (const match of content.matchAll(pattern)) {
698
+ const start = match.index || 0;
699
+ if (start > lastIndex) {
700
+ target.appendChild(document.createTextNode(content.slice(lastIndex, start)));
701
+ }
702
+ const mark = document.createElement('mark');
703
+ mark.textContent = content.slice(start, start + match[0].length);
704
+ target.appendChild(mark);
705
+ lastIndex = start + match[0].length;
706
+ }
707
+ if (lastIndex < content.length) {
708
+ target.appendChild(document.createTextNode(content.slice(lastIndex)));
709
+ }
710
+ };
711
+
712
+ const confirmCriticalAction = (message) => window.confirm(message);
713
+
714
+ const buildLoginRedirectUrl = () => {
715
+ const loginUrl = new URL(state.loginPath, window.location.origin);
716
+ const nextPath = `${window.location.pathname || '/user/systemadm/'}${window.location.search || ''}`;
717
+ loginUrl.searchParams.set('next', nextPath);
718
+ return `${loginUrl.pathname}${loginUrl.search}`;
719
+ };
720
+
721
+ const buildWhatsAppMenuUrl = (phoneDigits) => {
722
+ const params = new URLSearchParams({
723
+ text: '/menu',
724
+ type: 'custom_url',
725
+ app_absent: '0',
726
+ });
727
+ const digits = normalizeDigits(phoneDigits);
728
+ if (digits) params.set('phone', digits);
729
+ return `https://api.whatsapp.com/send/?${params.toString()}`;
730
+ };
731
+
732
+ const fetchJson = async (url, init = {}) => {
733
+ const response = await fetch(url, {
734
+ credentials: 'include',
735
+ ...init,
736
+ });
737
+
738
+ let payload = null;
739
+ try {
740
+ payload = await response.json();
741
+ } catch {
742
+ payload = null;
743
+ }
744
+
745
+ if (!response.ok) {
746
+ const err = new Error(payload?.error || `Falha HTTP ${response.status}`);
747
+ err.statusCode = response.status;
748
+ throw err;
749
+ }
750
+ return payload || {};
751
+ };
752
+
753
+ const fetchWithAuth = async (url, init = {}) => {
754
+ const response = await fetch(url, {
755
+ credentials: 'include',
756
+ ...init,
757
+ });
758
+ if (!response.ok) {
759
+ let message = `Falha HTTP ${response.status}`;
760
+ try {
761
+ const payload = await response.json();
762
+ if (payload?.error) message = payload.error;
763
+ } catch {
764
+ // noop
765
+ }
766
+ const error = new Error(message);
767
+ error.statusCode = response.status;
768
+ throw error;
769
+ }
770
+ return response;
771
+ };
772
+
773
+ const redirectToLogin = () => {
774
+ window.location.assign(buildLoginRedirectUrl());
775
+ };
776
+
777
+ const normalizeOwnerJidCandidate = (value) => {
778
+ const jid = normalizeString(value);
779
+ if (!jid || !jid.includes('@')) return '';
780
+ if (jid.endsWith('@g.us')) return '';
781
+ return jid;
782
+ };
783
+
784
+ const compactIdentityPayload = (raw = {}) => {
785
+ const payload = {};
786
+ const sessionToken = normalizeString(raw.session_token);
787
+ const googleSub = normalizeString(raw.google_sub);
788
+ const email = normalizeString(raw.email);
789
+ const ownerJid = normalizeOwnerJidCandidate(raw.owner_jid);
790
+ if (sessionToken) payload.session_token = sessionToken;
791
+ if (googleSub) payload.google_sub = googleSub;
792
+ if (email) payload.email = email;
793
+ if (ownerJid) payload.owner_jid = ownerJid;
794
+ return payload;
795
+ };
796
+
797
+ const buildIdentityLabel = (identity = {}) => {
798
+ const email = normalizeString(identity.email);
799
+ const ownerJid = normalizeString(identity.owner_jid);
800
+ const googleSub = normalizeString(identity.google_sub);
801
+ const sessionToken = normalizeString(identity.session_token);
802
+ if (email) return email;
803
+ if (ownerJid) return ownerJid;
804
+ if (googleSub) return googleSub;
805
+ if (sessionToken) return `${sessionToken.slice(0, 8)}...`;
806
+ return 'identidade';
807
+ };
808
+
809
+ const getAdminSession = () => state.adminStatusPayload?.session || null;
810
+ const isAdminAuthenticated = () => Boolean(getAdminSession()?.authenticated);
811
+ const isAdminEligible = () => Boolean(state.adminStatusPayload?.eligible_google_login || isAdminAuthenticated());
812
+
813
+ const resolveAdminRole = () =>
814
+ String(getAdminSession()?.role || state.adminStatusPayload?.eligible_role || '')
815
+ .trim()
816
+ .toLowerCase();
817
+
818
+ const formatAdminRole = (role) => {
819
+ if (role === 'owner') return 'dono';
820
+ if (role === 'moderator') return 'moderador';
821
+ return 'admin';
822
+ };
823
+
824
+ const createItemMeta = (text) => {
825
+ const p = document.createElement('p');
826
+ p.className = 'admin-item-meta';
827
+ p.textContent = text;
828
+ return p;
829
+ };
830
+
831
+ const createBadge = (label, severity = 'low') => {
832
+ const normalizedSeverity = ['critical', 'high', 'medium', 'low'].includes(String(severity)) ? String(severity) : 'low';
833
+ const badge = document.createElement('span');
834
+ badge.className = `admin-badge ${normalizedSeverity}`;
835
+ badge.textContent = String(label || '').trim() || normalizedSeverity.toUpperCase();
836
+ return badge;
837
+ };
838
+
839
+ const createMiniButton = (label, onClick) => {
840
+ const button = document.createElement('button');
841
+ button.type = 'button';
842
+ button.className = 'admin-mini-btn';
843
+ button.textContent = label;
844
+ button.addEventListener('click', () => {
845
+ if (typeof onClick === 'function') void onClick();
846
+ });
847
+ return button;
848
+ };
849
+
850
+ const createMiniLink = (label, href) => {
851
+ const link = document.createElement('a');
852
+ link.className = 'admin-mini-btn';
853
+ link.textContent = label;
854
+ link.href = href;
855
+ link.target = '_blank';
856
+ link.rel = 'noreferrer noopener';
857
+ return link;
858
+ };
859
+
860
+ const renderListPlaceholder = (container, message) => {
861
+ if (!container) return;
862
+ clearNode(container);
863
+ container.appendChild(createItemMeta(message));
864
+ };
865
+
866
+ const appendListItem = ({ container, title, severity = '', badgeLabel = '', meta = [], actions = [], customNode = null, itemData = {}, highlightQuery = '' }) => {
867
+ if (!container) return;
868
+
869
+ const item = document.createElement('article');
870
+ item.className = 'admin-item';
871
+ for (const [key, value] of Object.entries(itemData || {})) {
872
+ item.dataset[key] = String(value || '');
873
+ }
874
+
875
+ const titleEl = document.createElement('p');
876
+ titleEl.className = 'admin-item-title';
877
+ appendHighlightedText(titleEl, title, highlightQuery);
878
+ item.appendChild(titleEl);
879
+
880
+ if (badgeLabel) {
881
+ item.appendChild(createBadge(badgeLabel, severity));
882
+ }
883
+
884
+ for (const line of meta) {
885
+ const text = normalizeString(line);
886
+ if (!text) continue;
887
+ const metaNode = createItemMeta('');
888
+ appendHighlightedText(metaNode, text, highlightQuery);
889
+ item.appendChild(metaNode);
890
+ }
891
+
892
+ if (customNode) item.appendChild(customNode);
893
+
894
+ if (Array.isArray(actions) && actions.length) {
895
+ const actionsWrap = document.createElement('div');
896
+ actionsWrap.className = 'admin-item-actions';
897
+ for (const actionNode of actions) {
898
+ if (actionNode instanceof Element) actionsWrap.appendChild(actionNode);
899
+ }
900
+ if (actionsWrap.childNodes.length > 0) {
901
+ item.appendChild(actionsWrap);
902
+ }
903
+ }
904
+
905
+ container.appendChild(item);
906
+ };
907
+
908
+ const setAdminBusy = (value) => {
909
+ const busy = Boolean(value);
910
+ state.adminBusy = busy;
911
+ document.body.dataset.adminBusy = busy ? 'true' : 'false';
912
+
913
+ const authenticated = isAdminAuthenticated();
914
+ const eligible = isAdminEligible();
915
+
916
+ if (ui.adminPanel) {
917
+ const controls = ui.adminPanel.querySelectorAll('button, input, select, textarea');
918
+ for (const control of controls) {
919
+ const inUnlockForm = Boolean(control.closest('#user-admin-unlock-form'));
920
+ const inOverview = Boolean(control.closest('#user-admin-overview'));
921
+ if (inUnlockForm) {
922
+ control.disabled = busy || !eligible || authenticated;
923
+ continue;
924
+ }
925
+ if (inOverview) {
926
+ control.disabled = busy || !authenticated;
927
+ continue;
928
+ }
929
+ control.disabled = busy;
930
+ }
931
+ }
932
+
933
+ if (ui.adminPassword) ui.adminPassword.disabled = busy || !isAdminEligible() || isAdminAuthenticated();
934
+ if (ui.adminUnlockBtn) ui.adminUnlockBtn.disabled = busy || !isAdminEligible() || isAdminAuthenticated();
935
+ if (ui.adminRefreshBtn) ui.adminRefreshBtn.disabled = busy || !isAdminAuthenticated();
936
+ if (ui.adminLogoutBtn) ui.adminLogoutBtn.disabled = busy || !isAdminAuthenticated();
937
+ };
938
+
939
+ const renderSession = (sessionData) => {
940
+ const user = sessionData?.user || {};
941
+ const ownerPhone = normalizeString(sessionData?.owner_phone);
942
+ const ownerJid = normalizeString(sessionData?.owner_jid);
943
+
944
+ setText(ui.name, user?.name || 'Conta Google');
945
+ setText(ui.email, user?.email || 'Email não disponível');
946
+ if (ownerPhone) {
947
+ setText(ui.whatsapp, `WhatsApp vinculado: +${formatPhone(ownerPhone)}`);
948
+ } else if (ownerJid) {
949
+ setText(ui.whatsapp, `WhatsApp vinculado via owner: ${ownerJid}`);
950
+ } else {
951
+ setText(ui.whatsapp, 'WhatsApp não vinculado.');
952
+ }
953
+
954
+ if (ui.avatar) {
955
+ const picture = normalizeString(user?.picture) || FALLBACK_AVATAR;
956
+ ui.avatar.src = picture;
957
+ ui.avatar.onerror = () => {
958
+ ui.avatar.src = FALLBACK_AVATAR;
959
+ };
960
+ if (ui.topAvatar) {
961
+ ui.topAvatar.src = picture;
962
+ ui.topAvatar.onerror = () => {
963
+ ui.topAvatar.src = FALLBACK_AVATAR;
964
+ };
965
+ }
966
+ }
967
+
968
+ setText(ui.topAdminName, normalizeString(user?.name) || 'Admin');
969
+ updateSecurityStrip(sessionData);
970
+
971
+ setText(ui.ownerJid, ownerJid || 'n/d');
972
+ setText(ui.googleSub, normalizeString(user?.sub) || 'n/d');
973
+ setText(ui.expiresAt, formatDateTime(sessionData?.expires_at));
974
+
975
+ if (ui.profile) ui.profile.hidden = false;
976
+ if (ui.summary) ui.summary.hidden = false;
977
+ if (ui.actions) ui.actions.hidden = false;
978
+ };
979
+
980
+ const renderPackMetrics = (payload) => {
981
+ const data = payload?.data || {};
982
+ const packs = Array.isArray(data?.packs) ? data.packs : [];
983
+ const stats = isObject(data?.stats) ? data.stats : {};
984
+
985
+ let stickers = 0;
986
+ let downloads = 0;
987
+ let likes = 0;
988
+
989
+ for (const pack of packs) {
990
+ stickers += Number(pack?.sticker_count || 0);
991
+ downloads += Number(pack?.engagement?.open_count || 0);
992
+ likes += Number(pack?.engagement?.like_count || 0);
993
+ }
994
+
995
+ setText(ui.metricPacks, formatNumber(stats.total || packs.length));
996
+ setText(ui.metricStickers, formatNumber(stickers));
997
+ setText(ui.metricDownloads, formatNumber(downloads));
998
+ setText(ui.metricLikes, formatNumber(likes));
999
+ if (ui.grid) ui.grid.hidden = false;
1000
+ };
1001
+
1002
+ const setAdminMetricsDefaults = () => {
1003
+ setText(ui.adminBotsOnline, '0');
1004
+ setText(ui.adminMessagesToday, 'n/d');
1005
+ setText(ui.adminSpamBlocked, 'n/d');
1006
+ setText(ui.adminUptime, 'n/d');
1007
+ setText(ui.adminErrors5xx, '0');
1008
+ setText(ui.adminTotalPacks, '0');
1009
+ setText(ui.adminTotalStickers, '0');
1010
+ setText(ui.adminActiveBans, '0');
1011
+ setText(ui.adminKnownUsers, '0');
1012
+ setText(ui.adminActiveSessions, '0');
1013
+ setText(ui.adminVisits24h, '0');
1014
+ setText(ui.adminVisits7d, '0');
1015
+ setText(ui.adminUniqueVisitors7d, '0');
1016
+ setMetricContext(ui.adminBotsOnlineContext, 'vs ontem: n/d');
1017
+ setMetricContext(ui.adminMessagesTodayContext, 'vs ontem: n/d');
1018
+ setMetricContext(ui.adminUptimeContext, 'janela: processo atual');
1019
+ setMetricContext(ui.adminErrors5xxContext, 'vs ontem: n/d');
1020
+ setMetricContext(ui.adminTotalPacksContext, 'delta 24h: n/d');
1021
+ setMetricContext(ui.adminTotalStickersContext, 'delta 24h: n/d');
1022
+ setMetricContext(ui.adminSpamBlockedContext, 'vs ontem: n/d');
1023
+ setMetricContext(ui.adminActiveBansContext, 'delta 24h: n/d');
1024
+ setMetricContext(ui.adminKnownUsersContext, 'delta 7d: n/d');
1025
+ setMetricContext(ui.adminActiveSessionsContext, 'agora: n/d');
1026
+ setMetricContext(ui.adminVisits24hContext, 'janela: 24h');
1027
+ setMetricContext(ui.adminVisits7dContext, 'janela: 7 dias');
1028
+ setMetricContext(ui.adminUniqueVisitors7dContext, 'janela: 7 dias');
1029
+
1030
+ setText(ui.adminHealthCpu, 'n/d');
1031
+ setText(ui.adminHealthRam, 'n/d');
1032
+ setText(ui.adminHealthLatency, 'n/d');
1033
+ setText(ui.adminHealthQueue, 'n/d');
1034
+ setText(ui.adminHealthDb, 'n/d');
1035
+ setText(ui.adminHealthCpuMeta, 'Limite recomendado: 88%');
1036
+ setText(ui.adminHealthRamMeta, 'Limite recomendado: 90%');
1037
+ setText(ui.adminHealthLatencyMeta, 'Alerta acima de 300ms');
1038
+ setText(ui.adminHealthQueueMeta, 'Ideal: abaixo de 120 jobs');
1039
+ setText(ui.adminHealthDbMeta, 'SLA alvo: 99.95%');
1040
+ setText(ui.adminLastUpdated, 'n/d');
1041
+ state.moderationPage = 1;
1042
+ state.auditPage = 1;
1043
+ state.usersPage = 1;
1044
+ state.sessionsPage = 1;
1045
+ state.alertsPage = 1;
1046
+ if (ui.adminHealthDbBadge) {
1047
+ ui.adminHealthDbBadge.classList.remove('healthy', 'degraded', 'down');
1048
+ ui.adminHealthDbBadge.textContent = 'Unknown';
1049
+ }
1050
+ setHealthMeter(ui.adminHealthCpuBar, 0);
1051
+ setHealthMeter(ui.adminHealthRamBar, 0);
1052
+ setHealthMeter(ui.adminHealthLatencyBar, 0);
1053
+ setHealthMeter(ui.adminHealthQueueBar, 0);
1054
+
1055
+ renderListPlaceholder(ui.adminModerationList, 'Nenhum evento recente de moderação.');
1056
+ renderListPlaceholder(ui.adminSessionsList, 'Nenhuma sessão ativa encontrada.');
1057
+ renderListPlaceholder(ui.adminUsersList, 'Nenhum usuário encontrado.');
1058
+ renderListPlaceholder(ui.adminBansList, 'Nenhuma conta bloqueada.');
1059
+ renderListPlaceholder(ui.adminAuditList, 'Sem eventos de auditoria recentes.');
1060
+ setPaginationHidden({
1061
+ wrapper: ui.adminModerationPagination,
1062
+ counter: ui.adminModerationPageCounter,
1063
+ meta: ui.adminModerationPageMeta,
1064
+ });
1065
+ setPaginationHidden({
1066
+ wrapper: ui.adminAuditPagination,
1067
+ counter: ui.adminAuditPageCounter,
1068
+ meta: ui.adminAuditPageMeta,
1069
+ });
1070
+ setPaginationHidden({
1071
+ wrapper: ui.adminUsersPagination,
1072
+ counter: ui.adminUsersPageCounter,
1073
+ meta: ui.adminUsersPageMeta,
1074
+ });
1075
+ setPaginationHidden({
1076
+ wrapper: ui.adminSessionsPagination,
1077
+ counter: ui.adminSessionsPageCounter,
1078
+ meta: ui.adminSessionsPageMeta,
1079
+ });
1080
+ setPaginationHidden({
1081
+ wrapper: ui.adminAlertsPagination,
1082
+ counter: ui.adminAlertsPageCounter,
1083
+ meta: ui.adminAlertsPageMeta,
1084
+ });
1085
+ renderListPlaceholder(ui.adminFlagsList, 'Nenhuma feature flag disponível.');
1086
+ renderListPlaceholder(ui.adminAlertsList, 'Sem alertas ativos no momento.');
1087
+ renderListPlaceholder(ui.adminSearchResults, 'Faça uma busca para ver usuários, grupos, packs e sessões.');
1088
+ setText(ui.adminOpsStatus, state.adminOpsMessage || 'Ações operacionais disponíveis.');
1089
+ setRiskPill(ui.riskCpu, 'CPU normal');
1090
+ setRiskPill(ui.riskSpam, 'Spam sob controle');
1091
+ setRiskPill(ui.riskBans, 'Bans estáveis');
1092
+ setRiskPill(ui.riskErrors, 'Erros baixos');
1093
+ updateSecurityStrip(null);
1094
+ setGlobalStatusChip('online', 'Online');
1095
+ };
1096
+
1097
+ const buildBanPayloadFromEvent = (event) => {
1098
+ const metadata = isObject(event?.metadata) ? event.metadata : {};
1099
+ const payload = compactIdentityPayload({
1100
+ google_sub: metadata.google_sub,
1101
+ email: metadata.email,
1102
+ owner_jid: event?.sender_id || metadata.owner_jid,
1103
+ });
1104
+ if (!Object.keys(payload).length) return null;
1105
+ payload.reason = `Ban via moderação (${normalizeString(event?.event_type) || 'evento'})`;
1106
+ return payload;
1107
+ };
1108
+
1109
+ const buildForceLogoutPayloadFromAny = (entry = {}) =>
1110
+ compactIdentityPayload({
1111
+ session_token: entry?.session_token,
1112
+ google_sub: entry?.google_sub,
1113
+ email: entry?.email,
1114
+ owner_jid: entry?.owner_jid,
1115
+ });
1116
+
1117
+ const handleAdminForceLogout = async (identity, contextLabel = '') => {
1118
+ const payload = compactIdentityPayload(identity);
1119
+ if (!Object.keys(payload).length) {
1120
+ showAdminError('Não foi possível forçar logout: identidade ausente.');
1121
+ return;
1122
+ }
1123
+ if (state.adminBusy) return;
1124
+
1125
+ const label = normalizeString(contextLabel) || buildIdentityLabel(payload);
1126
+ if (!confirmCriticalAction(`Forçar logout de ${label}?`)) {
1127
+ return;
1128
+ }
1129
+
1130
+ showAdminError('');
1131
+ setAdminBusy(true);
1132
+ try {
1133
+ const response = await fetchJson(adminForceLogoutApiPath, {
1134
+ method: 'POST',
1135
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
1136
+ body: JSON.stringify(payload),
1137
+ });
1138
+ const removed = Number(response?.data?.removed_sessions || 0);
1139
+ state.adminOpsMessage = `Logout forçado concluído para ${label}. Sessões removidas: ${removed}.`;
1140
+ showToast({ kind: 'success', title: 'Sessão', message: state.adminOpsMessage });
1141
+ await refreshAdminArea({ keepCurrentError: true });
1142
+ } catch (error) {
1143
+ showAdminError(error?.message || 'Falha ao forçar logout.');
1144
+ } finally {
1145
+ setAdminBusy(false);
1146
+ renderAdminPanel();
1147
+ }
1148
+ };
1149
+
1150
+ const handleAdminBanCreate = async (banPayload, contextLabel = '') => {
1151
+ if (!isObject(banPayload)) return;
1152
+ const payload = {
1153
+ ...compactIdentityPayload(banPayload),
1154
+ reason: normalizeString(banPayload.reason),
1155
+ };
1156
+ if (!payload.google_sub && !payload.email && !payload.owner_jid) {
1157
+ showAdminError('Não foi possível banir: identidade ausente.');
1158
+ return;
1159
+ }
1160
+ if (state.adminBusy) return;
1161
+
1162
+ const label = normalizeString(contextLabel) || buildIdentityLabel(payload);
1163
+ if (!confirmCriticalAction(`Confirmar bloqueio da conta ${label}?`)) {
1164
+ return;
1165
+ }
1166
+
1167
+ showAdminError('');
1168
+ setAdminBusy(true);
1169
+ try {
1170
+ const response = await fetchJson(adminBansApiPath, {
1171
+ method: 'POST',
1172
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
1173
+ body: JSON.stringify(payload),
1174
+ });
1175
+ const created = Boolean(response?.data?.created);
1176
+ state.adminOpsMessage = created ? `Conta banida: ${label}.` : `Conta já estava banida: ${label}.`;
1177
+ showToast({
1178
+ kind: created ? 'warn' : 'success',
1179
+ title: 'Ban',
1180
+ message: state.adminOpsMessage,
1181
+ });
1182
+ await refreshAdminArea({ keepCurrentError: true });
1183
+ } catch (error) {
1184
+ showAdminError(error?.message || 'Falha ao criar ban.');
1185
+ } finally {
1186
+ setAdminBusy(false);
1187
+ renderAdminPanel();
1188
+ }
1189
+ };
1190
+
1191
+ const handleAdminBanRevoke = async (banId) => {
1192
+ const normalizedId = normalizeString(banId);
1193
+ if (!normalizedId || state.adminBusy) return;
1194
+ if (!confirmCriticalAction(`Revogar ban ${normalizedId}?`)) return;
1195
+
1196
+ showAdminError('');
1197
+ setAdminBusy(true);
1198
+ try {
1199
+ await fetchJson(`${adminBansApiPath}/${encodeURIComponent(normalizedId)}/revoke`, {
1200
+ method: 'DELETE',
1201
+ });
1202
+ state.adminOpsMessage = `Ban ${normalizedId} revogado com sucesso.`;
1203
+ showToast({ kind: 'success', title: 'Ban', message: state.adminOpsMessage });
1204
+ await refreshAdminArea({ keepCurrentError: true });
1205
+ } catch (error) {
1206
+ showAdminError(error?.message || 'Falha ao revogar ban.');
1207
+ } finally {
1208
+ setAdminBusy(false);
1209
+ renderAdminPanel();
1210
+ }
1211
+ };
1212
+
1213
+ const handleAdminFeatureFlagUpdate = async ({ flagName = '', isEnabled = false, rolloutPercent = 100, description = '' } = {}) => {
1214
+ const normalizedName = normalizeString(flagName);
1215
+ if (!normalizedName || state.adminBusy) return;
1216
+
1217
+ showAdminError('');
1218
+ setAdminBusy(true);
1219
+ try {
1220
+ await fetchJson(adminFeatureFlagsApiPath, {
1221
+ method: 'POST',
1222
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
1223
+ body: JSON.stringify({
1224
+ flag_name: normalizedName,
1225
+ is_enabled: Boolean(isEnabled),
1226
+ rollout_percent: Math.max(0, Math.min(100, Math.floor(Number(rolloutPercent) || 0))),
1227
+ description: normalizeString(description),
1228
+ }),
1229
+ });
1230
+ state.adminOpsMessage = `Feature flag ${normalizedName} atualizada.`;
1231
+ showToast({ kind: 'success', title: 'Feature Flag', message: state.adminOpsMessage });
1232
+ await refreshAdminArea({ keepCurrentError: true });
1233
+ } catch (error) {
1234
+ showAdminError(error?.message || 'Falha ao atualizar feature flag.');
1235
+ } finally {
1236
+ setAdminBusy(false);
1237
+ renderAdminPanel();
1238
+ }
1239
+ };
1240
+
1241
+ const handleAdminOpsAction = async (action, triggerButton = null) => {
1242
+ const normalizedAction = normalizeString(action);
1243
+ if (!normalizedAction || state.adminBusy) return;
1244
+ if (CRITICAL_ADMIN_ACTIONS.has(normalizedAction)) {
1245
+ const confirmed = confirmCriticalAction(`Executar ação crítica: ${normalizedAction.replace(/_/g, ' ')}?`);
1246
+ if (!confirmed) return;
1247
+ }
1248
+
1249
+ showAdminError('');
1250
+ setButtonProcessing(triggerButton, 'Executando...');
1251
+ setAdminBusy(true);
1252
+ let success = false;
1253
+ try {
1254
+ const response = await fetchJson(adminOpsApiPath, {
1255
+ method: 'POST',
1256
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
1257
+ body: JSON.stringify({ action: normalizedAction }),
1258
+ });
1259
+ const message = normalizeString(response?.data?.message) || `Ação ${normalizedAction} concluída.`;
1260
+ state.adminOpsMessage = `${message} (${formatDateTime(response?.data?.updated_at)})`;
1261
+ setText(ui.adminOpsStatus, state.adminOpsMessage);
1262
+ showToast({ kind: 'success', title: 'Operação', message: state.adminOpsMessage });
1263
+ await refreshAdminArea({ keepCurrentError: true });
1264
+ success = true;
1265
+ } catch (error) {
1266
+ showAdminError(error?.message || 'Falha ao executar ação operacional.');
1267
+ } finally {
1268
+ setAdminBusy(false);
1269
+ setButtonIdle(triggerButton);
1270
+ if (success) flashButtonSuccess(triggerButton, 'Executado');
1271
+ renderAdminPanel();
1272
+ }
1273
+ };
1274
+
1275
+ const renderModerationQueue = (events) => {
1276
+ const list = Array.isArray(events) ? events : [];
1277
+ const severityFilter = normalizeString(state.moderationFilterSeverity || 'all').toLowerCase();
1278
+ const typeFilter = normalizeString(state.moderationFilterType || 'all').toLowerCase();
1279
+ const filtered = list.filter((event) => {
1280
+ const eventSeverity = normalizeSeverity(event?.severity);
1281
+ const eventType = normalizeString(event?.event_type || '').toLowerCase();
1282
+ const severityOk = severityFilter === 'all' || eventSeverity === severityFilter;
1283
+ const typeOk = typeFilter === 'all' || eventType.includes(typeFilter);
1284
+ return severityOk && typeOk;
1285
+ });
1286
+ clearNode(ui.adminModerationList);
1287
+
1288
+ if (!filtered.length) {
1289
+ renderListPlaceholder(ui.adminModerationList, 'Nenhum evento recente de moderação.');
1290
+ setPaginationHidden({
1291
+ wrapper: ui.adminModerationPagination,
1292
+ counter: ui.adminModerationPageCounter,
1293
+ meta: ui.adminModerationPageMeta,
1294
+ });
1295
+ if (ui.adminModerationPrevBtn) ui.adminModerationPrevBtn.disabled = true;
1296
+ if (ui.adminModerationNextBtn) ui.adminModerationNextBtn.disabled = true;
1297
+ return;
1298
+ }
1299
+
1300
+ const pageItems = paginateItems({
1301
+ items: filtered,
1302
+ statePageKey: 'moderationPage',
1303
+ pageSize: state.moderationPageSize,
1304
+ wrapper: ui.adminModerationPagination,
1305
+ counter: ui.adminModerationPageCounter,
1306
+ meta: ui.adminModerationPageMeta,
1307
+ prevBtn: ui.adminModerationPrevBtn,
1308
+ nextBtn: ui.adminModerationNextBtn,
1309
+ });
1310
+
1311
+ for (const event of pageItems) {
1312
+ const title = normalizeString(event?.title) || 'Evento de moderação';
1313
+ const severity = normalizeSeverity(event?.severity);
1314
+ const badgeLabel = severity.toUpperCase();
1315
+ const createdAt = formatDateTime(event?.created_at || event?.revoked_at);
1316
+ const meta = [normalizeString(event?.subtitle), `Tipo: ${normalizeString(event?.event_type) || 'evento'} • ${createdAt}`];
1317
+
1318
+ if (normalizeString(event?.reason)) meta.push(`Motivo: ${normalizeString(event.reason)}`);
1319
+
1320
+ const actions = [];
1321
+ if (event?.event_type === 'ban') {
1322
+ if (event?.ban_id && !event?.revoked_at) {
1323
+ actions.push(createMiniButton('Revogar ban', () => handleAdminBanRevoke(event.ban_id)));
1324
+ }
1325
+ } else {
1326
+ const banPayload = buildBanPayloadFromEvent(event);
1327
+ if (banPayload) {
1328
+ actions.push(createMiniButton('Banir conta', () => handleAdminBanCreate(banPayload, buildIdentityLabel(banPayload))));
1329
+ }
1330
+ const logoutPayload = compactIdentityPayload({
1331
+ session_token: event?.metadata?.session_token,
1332
+ google_sub: event?.metadata?.google_sub,
1333
+ email: event?.metadata?.email,
1334
+ owner_jid: event?.sender_id || event?.metadata?.owner_jid,
1335
+ });
1336
+ if (Object.keys(logoutPayload).length) {
1337
+ actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(logoutPayload, buildIdentityLabel(logoutPayload))));
1338
+ }
1339
+ }
1340
+
1341
+ appendListItem({
1342
+ container: ui.adminModerationList,
1343
+ title,
1344
+ severity,
1345
+ badgeLabel,
1346
+ meta,
1347
+ actions,
1348
+ itemData: {
1349
+ severity,
1350
+ type: normalizeString(event?.event_type || '').toLowerCase(),
1351
+ },
1352
+ });
1353
+ }
1354
+ };
1355
+
1356
+ const renderActiveSessions = (sessions) => {
1357
+ const list = Array.isArray(sessions) ? sessions : [];
1358
+ clearNode(ui.adminSessionsList);
1359
+
1360
+ if (!list.length) {
1361
+ renderListPlaceholder(ui.adminSessionsList, 'Nenhuma sessão ativa encontrada.');
1362
+ setPaginationHidden({
1363
+ wrapper: ui.adminSessionsPagination,
1364
+ counter: ui.adminSessionsPageCounter,
1365
+ meta: ui.adminSessionsPageMeta,
1366
+ });
1367
+ if (ui.adminSessionsPrevBtn) ui.adminSessionsPrevBtn.disabled = true;
1368
+ if (ui.adminSessionsNextBtn) ui.adminSessionsNextBtn.disabled = true;
1369
+ return;
1370
+ }
1371
+
1372
+ const pageItems = paginateItems({
1373
+ items: list,
1374
+ statePageKey: 'sessionsPage',
1375
+ pageSize: state.sessionsPageSize,
1376
+ wrapper: ui.adminSessionsPagination,
1377
+ counter: ui.adminSessionsPageCounter,
1378
+ meta: ui.adminSessionsPageMeta,
1379
+ prevBtn: ui.adminSessionsPrevBtn,
1380
+ nextBtn: ui.adminSessionsNextBtn,
1381
+ });
1382
+
1383
+ for (const session of pageItems) {
1384
+ const identity = compactIdentityPayload(session);
1385
+ const title = normalizeString(session?.name || session?.email || session?.owner_jid || 'Sessão ativa');
1386
+ const meta = [`Email: ${normalizeString(session?.email) || 'n/d'}`, `Owner: ${normalizeString(session?.owner_jid) || 'n/d'}`, `Último acesso: ${formatDateTime(session?.last_seen_at)} • Expira: ${formatDateTime(session?.expires_at)}`];
1387
+ const actions = [];
1388
+ if (Object.keys(identity).length) {
1389
+ actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
1390
+ }
1391
+ appendListItem({
1392
+ container: ui.adminSessionsList,
1393
+ title,
1394
+ severity: 'low',
1395
+ badgeLabel: 'ATIVA',
1396
+ meta,
1397
+ actions,
1398
+ });
1399
+ }
1400
+ };
1401
+
1402
+ const renderKnownUsers = (users) => {
1403
+ const list = Array.isArray(users) ? users : [];
1404
+ clearNode(ui.adminUsersList);
1405
+
1406
+ if (!list.length) {
1407
+ renderListPlaceholder(ui.adminUsersList, 'Nenhum usuário encontrado.');
1408
+ setPaginationHidden({
1409
+ wrapper: ui.adminUsersPagination,
1410
+ counter: ui.adminUsersPageCounter,
1411
+ meta: ui.adminUsersPageMeta,
1412
+ });
1413
+ if (ui.adminUsersPrevBtn) ui.adminUsersPrevBtn.disabled = true;
1414
+ if (ui.adminUsersNextBtn) ui.adminUsersNextBtn.disabled = true;
1415
+ return;
1416
+ }
1417
+
1418
+ const pageItems = paginateItems({
1419
+ items: list,
1420
+ statePageKey: 'usersPage',
1421
+ pageSize: state.usersPageSize,
1422
+ wrapper: ui.adminUsersPagination,
1423
+ counter: ui.adminUsersPageCounter,
1424
+ meta: ui.adminUsersPageMeta,
1425
+ prevBtn: ui.adminUsersPrevBtn,
1426
+ nextBtn: ui.adminUsersNextBtn,
1427
+ });
1428
+
1429
+ for (const user of pageItems) {
1430
+ const identity = buildForceLogoutPayloadFromAny(user);
1431
+ const title = normalizeString(user?.name || user?.email || user?.owner_jid || 'Usuário');
1432
+ const meta = [`Email: ${normalizeString(user?.email) || 'n/d'}`, `Owner: ${normalizeString(user?.owner_jid) || 'n/d'}`, `Último login: ${formatDateTime(user?.last_login_at)} • Último acesso: ${formatDateTime(user?.last_seen_at)}`];
1433
+ const actions = [];
1434
+ if (Object.keys(identity).length) {
1435
+ actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
1436
+ }
1437
+ appendListItem({
1438
+ container: ui.adminUsersList,
1439
+ title,
1440
+ severity: 'low',
1441
+ badgeLabel: 'USER',
1442
+ meta,
1443
+ actions,
1444
+ });
1445
+ }
1446
+ };
1447
+
1448
+ const renderBlockedAccounts = (bans) => {
1449
+ const list = Array.isArray(bans) ? bans : [];
1450
+ clearNode(ui.adminBansList);
1451
+
1452
+ if (!list.length) {
1453
+ renderListPlaceholder(ui.adminBansList, 'Nenhuma conta bloqueada.');
1454
+ return;
1455
+ }
1456
+
1457
+ for (const ban of list) {
1458
+ const identity = normalizeString(ban?.email || ban?.owner_jid || ban?.google_sub || `ban:${ban?.id || ''}`);
1459
+ const isRevoked = Boolean(ban?.revoked_at);
1460
+ const meta = [`Criado: ${formatDateTime(ban?.created_at)}${isRevoked ? ` • Revogado: ${formatDateTime(ban?.revoked_at)}` : ''}`, `Motivo: ${normalizeString(ban?.reason) || 'não informado'}`];
1461
+ const actions = [];
1462
+ if (!isRevoked && normalizeString(ban?.id)) {
1463
+ actions.push(createMiniButton('Revogar ban', () => handleAdminBanRevoke(ban.id)));
1464
+ }
1465
+ appendListItem({
1466
+ container: ui.adminBansList,
1467
+ title: identity,
1468
+ severity: isRevoked ? 'low' : 'critical',
1469
+ badgeLabel: isRevoked ? 'REVOGADO' : 'BLOQUEADO',
1470
+ meta,
1471
+ actions,
1472
+ });
1473
+ }
1474
+ };
1475
+
1476
+ const renderAuditLog = (events) => {
1477
+ const list = Array.isArray(events) ? events : [];
1478
+ const statusFilter = normalizeString(state.auditFilterStatus || 'all').toLowerCase();
1479
+ const query = normalizeString(state.auditSearchQuery).toLowerCase();
1480
+ const filtered = list.filter((item) => {
1481
+ const status = normalizeString(item?.status || 'success').toLowerCase();
1482
+ const statusOk = statusFilter === 'all' || status === statusFilter;
1483
+ if (!statusOk) return false;
1484
+ if (!query) return true;
1485
+
1486
+ const details = isObject(item?.details) ? Object.entries(item.details).slice(0, 3) : [];
1487
+ const haystack = [normalizeString(item?.action), normalizeString(item?.target_type), normalizeString(item?.target_id), normalizeString(item?.admin_email), normalizeString(item?.admin_google_sub), normalizeString(item?.admin_owner_jid), details.map(([key, value]) => `${key}=${value}`).join(' ')].join(' ').toLowerCase();
1488
+ return haystack.includes(query);
1489
+ });
1490
+ clearNode(ui.adminAuditList);
1491
+
1492
+ if (!filtered.length) {
1493
+ renderListPlaceholder(ui.adminAuditList, 'Sem eventos de auditoria recentes.');
1494
+ setPaginationHidden({
1495
+ wrapper: ui.adminAuditPagination,
1496
+ counter: ui.adminAuditPageCounter,
1497
+ meta: ui.adminAuditPageMeta,
1498
+ });
1499
+ if (ui.adminAuditPrevBtn) ui.adminAuditPrevBtn.disabled = true;
1500
+ if (ui.adminAuditNextBtn) ui.adminAuditNextBtn.disabled = true;
1501
+ return;
1502
+ }
1503
+
1504
+ const pageItems = paginateItems({
1505
+ items: filtered,
1506
+ statePageKey: 'auditPage',
1507
+ pageSize: state.auditPageSize,
1508
+ wrapper: ui.adminAuditPagination,
1509
+ counter: ui.adminAuditPageCounter,
1510
+ meta: ui.adminAuditPageMeta,
1511
+ prevBtn: ui.adminAuditPrevBtn,
1512
+ nextBtn: ui.adminAuditNextBtn,
1513
+ });
1514
+
1515
+ for (const item of pageItems) {
1516
+ const action = normalizeString(item?.action || 'action');
1517
+ const targetType = normalizeString(item?.target_type || 'target');
1518
+ const targetId = normalizeString(item?.target_id || '');
1519
+ const status = normalizeString(item?.status || 'success');
1520
+ const details = isObject(item?.details) ? Object.entries(item.details).slice(0, 3) : [];
1521
+ const detailLine = details.length ? `Detalhes: ${details.map(([key, value]) => `${key}=${value}`).join(' • ')}` : '';
1522
+
1523
+ const meta = [`Admin: ${normalizeString(item?.admin_email || item?.admin_google_sub || item?.admin_owner_jid || 'n/d')} (${formatAdminRole(normalizeString(item?.admin_role || 'admin'))})`, `Alvo: ${targetType}${targetId ? ` / ${targetId}` : ''} • Em: ${formatDateTime(item?.created_at)}`];
1524
+ if (detailLine) meta.push(detailLine);
1525
+
1526
+ appendListItem({
1527
+ container: ui.adminAuditList,
1528
+ title: action,
1529
+ severity: status === 'success' ? 'low' : 'high',
1530
+ badgeLabel: status.toUpperCase(),
1531
+ meta,
1532
+ highlightQuery: query,
1533
+ });
1534
+ }
1535
+ };
1536
+
1537
+ const renderFeatureFlags = (flags) => {
1538
+ const list = Array.isArray(flags) ? flags : [];
1539
+ clearNode(ui.adminFlagsList);
1540
+
1541
+ if (!list.length) {
1542
+ renderListPlaceholder(ui.adminFlagsList, 'Nenhuma feature flag disponível.');
1543
+ return;
1544
+ }
1545
+
1546
+ for (const flag of list) {
1547
+ const flagName = normalizeString(flag?.flag_name);
1548
+ const isEnabled = Boolean(flag?.is_enabled);
1549
+ const rollout = Math.max(0, Math.min(100, Math.floor(Number(flag?.rollout_percent) || 0)));
1550
+ const description = normalizeString(flag?.description);
1551
+ const updatedBy = normalizeString(flag?.updated_by);
1552
+
1553
+ const rolloutForm = document.createElement('form');
1554
+ rolloutForm.className = 'admin-inline-form';
1555
+
1556
+ const rolloutInput = document.createElement('input');
1557
+ rolloutInput.className = 'admin-input';
1558
+ rolloutInput.type = 'number';
1559
+ rolloutInput.min = '0';
1560
+ rolloutInput.max = '100';
1561
+ rolloutInput.step = '1';
1562
+ rolloutInput.value = String(rollout);
1563
+ rolloutInput.setAttribute('aria-label', `Rollout de ${flagName}`);
1564
+
1565
+ const rolloutBtn = document.createElement('button');
1566
+ rolloutBtn.type = 'submit';
1567
+ rolloutBtn.className = 'admin-mini-btn';
1568
+ rolloutBtn.textContent = 'Salvar rollout';
1569
+
1570
+ rolloutForm.appendChild(rolloutInput);
1571
+ rolloutForm.appendChild(rolloutBtn);
1572
+ rolloutForm.addEventListener('submit', (event) => {
1573
+ event.preventDefault();
1574
+ void handleAdminFeatureFlagUpdate({
1575
+ flagName,
1576
+ isEnabled,
1577
+ rolloutPercent: rolloutInput.value,
1578
+ description,
1579
+ });
1580
+ });
1581
+
1582
+ const actions = [
1583
+ createMiniButton(isEnabled ? 'Desativar' : 'Ativar', () =>
1584
+ handleAdminFeatureFlagUpdate({
1585
+ flagName,
1586
+ isEnabled: !isEnabled,
1587
+ rolloutPercent: rollout,
1588
+ description,
1589
+ }),
1590
+ ),
1591
+ ];
1592
+
1593
+ appendListItem({
1594
+ container: ui.adminFlagsList,
1595
+ title: flagName || 'feature_flag',
1596
+ severity: isEnabled ? 'low' : 'medium',
1597
+ badgeLabel: isEnabled ? 'ON' : 'OFF',
1598
+ meta: [`Rollout: ${rollout}%`, description ? `Descrição: ${description}` : 'Descrição: n/d', `Atualizado por: ${updatedBy || 'n/d'} • ${formatDateTime(flag?.updated_at)}`],
1599
+ actions,
1600
+ customNode: rolloutForm,
1601
+ });
1602
+ }
1603
+ };
1604
+
1605
+ const renderAlerts = (alerts) => {
1606
+ const list = Array.isArray(alerts) ? alerts : [];
1607
+ clearNode(ui.adminAlertsList);
1608
+
1609
+ if (!list.length) {
1610
+ renderListPlaceholder(ui.adminAlertsList, 'Sem alertas ativos no momento.');
1611
+ setPaginationHidden({
1612
+ wrapper: ui.adminAlertsPagination,
1613
+ counter: ui.adminAlertsPageCounter,
1614
+ meta: ui.adminAlertsPageMeta,
1615
+ });
1616
+ if (ui.adminAlertsPrevBtn) ui.adminAlertsPrevBtn.disabled = true;
1617
+ if (ui.adminAlertsNextBtn) ui.adminAlertsNextBtn.disabled = true;
1618
+ return;
1619
+ }
1620
+
1621
+ const pageItems = paginateItems({
1622
+ items: list,
1623
+ statePageKey: 'alertsPage',
1624
+ pageSize: state.alertsPageSize,
1625
+ wrapper: ui.adminAlertsPagination,
1626
+ counter: ui.adminAlertsPageCounter,
1627
+ meta: ui.adminAlertsPageMeta,
1628
+ prevBtn: ui.adminAlertsPrevBtn,
1629
+ nextBtn: ui.adminAlertsNextBtn,
1630
+ });
1631
+
1632
+ for (const alert of pageItems) {
1633
+ const severity = normalizeSeverity(alert?.severity);
1634
+ const title = normalizeString(alert?.title || alert?.code || 'Alerta');
1635
+ const meta = [normalizeString(alert?.message || ''), `Código: ${normalizeString(alert?.code || 'n/d')} • ${formatDateTime(alert?.created_at)}`];
1636
+ appendListItem({
1637
+ container: ui.adminAlertsList,
1638
+ title,
1639
+ severity,
1640
+ badgeLabel: severity.toUpperCase(),
1641
+ meta,
1642
+ });
1643
+ }
1644
+ };
1645
+
1646
+ const renderSearchResults = (payload = state.adminSearchPayload) => {
1647
+ if (!ui.adminSearchResults) return;
1648
+ clearNode(ui.adminSearchResults);
1649
+
1650
+ if (!payload || !isObject(payload)) {
1651
+ renderListPlaceholder(ui.adminSearchResults, 'Faça uma busca para ver usuários, grupos, packs e sessões.');
1652
+ return;
1653
+ }
1654
+
1655
+ const q = normalizeString(payload?.q);
1656
+ const totals = isObject(payload?.totals) ? payload.totals : {};
1657
+ const results = isObject(payload?.results) ? payload.results : {};
1658
+
1659
+ appendListItem({
1660
+ container: ui.adminSearchResults,
1661
+ title: `Resultado para "${q || 'consulta'}"`,
1662
+ severity: 'low',
1663
+ badgeLabel: 'BUSCA',
1664
+ meta: [`Usuários: ${formatIntegerOrNd(totals.users)}`, `Sessões: ${formatIntegerOrNd(totals.sessions)}`, `Grupos: ${formatIntegerOrNd(totals.groups)}`, `Packs: ${formatIntegerOrNd(totals.packs)}`],
1665
+ highlightQuery: q,
1666
+ });
1667
+
1668
+ const users = Array.isArray(results.users) ? results.users : [];
1669
+ for (const user of users) {
1670
+ const identity = buildForceLogoutPayloadFromAny(user);
1671
+ const actions = [];
1672
+ if (Object.keys(identity).length) {
1673
+ actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
1674
+ }
1675
+ appendListItem({
1676
+ container: ui.adminSearchResults,
1677
+ title: `[Usuário] ${normalizeString(user?.name || user?.email || user?.owner_jid || 'registro')}`,
1678
+ severity: 'low',
1679
+ badgeLabel: 'USER',
1680
+ meta: [`Email: ${normalizeString(user?.email) || 'n/d'}`, `Owner: ${normalizeString(user?.owner_jid) || 'n/d'}`],
1681
+ actions,
1682
+ highlightQuery: q,
1683
+ });
1684
+ }
1685
+
1686
+ const sessions = Array.isArray(results.sessions) ? results.sessions : [];
1687
+ for (const session of sessions) {
1688
+ const identity = buildForceLogoutPayloadFromAny(session);
1689
+ const actions = [];
1690
+ if (Object.keys(identity).length) {
1691
+ actions.push(createMiniButton('Forçar logout', () => handleAdminForceLogout(identity, buildIdentityLabel(identity))));
1692
+ }
1693
+ appendListItem({
1694
+ container: ui.adminSearchResults,
1695
+ title: `[Sessão] ${normalizeString(session?.name || session?.email || session?.owner_jid || 'ativa')}`,
1696
+ severity: 'low',
1697
+ badgeLabel: 'SESSÃO',
1698
+ meta: [`Email: ${normalizeString(session?.email) || 'n/d'}`, `Expira: ${formatDateTime(session?.expires_at)}`],
1699
+ actions,
1700
+ highlightQuery: q,
1701
+ });
1702
+ }
1703
+
1704
+ const groups = Array.isArray(results.groups) ? results.groups : [];
1705
+ for (const group of groups) {
1706
+ appendListItem({
1707
+ container: ui.adminSearchResults,
1708
+ title: `[Grupo] ${normalizeString(group?.subject || group?.id || 'grupo')}`,
1709
+ severity: 'medium',
1710
+ badgeLabel: 'GRUPO',
1711
+ meta: [`ID: ${normalizeString(group?.id) || 'n/d'}`, `Atualizado: ${formatDateTime(group?.updated_at)}`],
1712
+ highlightQuery: q,
1713
+ });
1714
+ }
1715
+
1716
+ const packs = Array.isArray(results.packs) ? results.packs : [];
1717
+ for (const pack of packs) {
1718
+ const packUrl = normalizeString(pack?.web_url);
1719
+ const actions = [];
1720
+ if (packUrl) actions.push(createMiniLink('Abrir pack', packUrl));
1721
+ appendListItem({
1722
+ container: ui.adminSearchResults,
1723
+ title: `[Pack] ${normalizeString(pack?.name || pack?.pack_key || 'pack')}`,
1724
+ severity: 'low',
1725
+ badgeLabel: normalizeString(pack?.visibility || 'pack').toUpperCase(),
1726
+ meta: [`Owner: ${normalizeString(pack?.owner_jid) || 'n/d'}`, `Stickers: ${formatIntegerOrNd(pack?.stickers_count)}`],
1727
+ actions,
1728
+ highlightQuery: q,
1729
+ });
1730
+ }
1731
+
1732
+ if (!users.length && !sessions.length && !groups.length && !packs.length) {
1733
+ appendListItem({
1734
+ container: ui.adminSearchResults,
1735
+ title: 'Nenhum resultado encontrado.',
1736
+ severity: 'low',
1737
+ badgeLabel: 'VAZIO',
1738
+ meta: ['Tente outro termo de busca.'],
1739
+ });
1740
+ }
1741
+ };
1742
+
1743
+ const renderSystemHealth = (health) => {
1744
+ const cpu = Number(health?.cpu_percent);
1745
+ const ram = Number(health?.ram_percent);
1746
+ const latency = Number(health?.http_latency_p95_ms);
1747
+ const queue = Number(health?.queue_pending);
1748
+ const dbStatus = normalizeString(health?.db_status).toLowerCase();
1749
+
1750
+ setText(ui.adminHealthCpu, formatPercent(cpu));
1751
+ setText(ui.adminHealthRam, formatPercent(ram));
1752
+ setText(ui.adminHealthLatency, formatMilliseconds(latency));
1753
+ setText(ui.adminHealthQueue, formatIntegerOrNd(queue));
1754
+
1755
+ setHealthMeter(ui.adminHealthCpuBar, cpu, { max: 100, warnAt: 70, dangerAt: 88 });
1756
+ setHealthMeter(ui.adminHealthRamBar, ram, { max: 100, warnAt: 75, dangerAt: 90 });
1757
+ setHealthMeter(ui.adminHealthLatencyBar, latency, { max: 900, warnAt: 35, dangerAt: 60 });
1758
+ setHealthMeter(ui.adminHealthQueueBar, queue, { max: 400, warnAt: 30, dangerAt: 55 });
1759
+
1760
+ let dbBadgeClass = '';
1761
+ let dbLabel = 'Unknown';
1762
+ if (dbStatus === 'ok') {
1763
+ dbBadgeClass = 'healthy';
1764
+ dbLabel = 'Healthy';
1765
+ } else if (dbStatus === 'degraded') {
1766
+ dbBadgeClass = 'degraded';
1767
+ dbLabel = 'Degraded';
1768
+ } else if (dbStatus === 'down') {
1769
+ dbBadgeClass = 'down';
1770
+ dbLabel = 'Down';
1771
+ }
1772
+
1773
+ setText(ui.adminHealthDb, dbLabel);
1774
+ if (ui.adminHealthDbBadge) {
1775
+ ui.adminHealthDbBadge.classList.remove('healthy', 'degraded', 'down');
1776
+ if (dbBadgeClass) ui.adminHealthDbBadge.classList.add(dbBadgeClass);
1777
+ ui.adminHealthDbBadge.textContent = dbLabel;
1778
+ }
1779
+
1780
+ return { cpu, ram, latency, queue, dbStatus };
1781
+ };
1782
+
1783
+ const updateOperationalSignals = ({ counters = {}, dashboard = {}, health = {}, alerts = [] } = {}) => {
1784
+ const normalizedAlerts = Array.isArray(alerts) ? alerts : [];
1785
+ const hasCriticalAlert = normalizedAlerts.some((entry) => {
1786
+ const severity = normalizeSeverity(entry?.severity);
1787
+ return severity === 'critical' || severity === 'high';
1788
+ });
1789
+ const hasMediumAlert = normalizedAlerts.some((entry) => normalizeSeverity(entry?.severity) === 'medium');
1790
+
1791
+ const cpuPercent = Number(health?.cpu_percent);
1792
+ const spamBlocked = Number(dashboard?.spam_blocked_today || 0);
1793
+ const activeBans = Number(counters?.active_bans || 0);
1794
+ const errors5xx = Number(dashboard?.errors_5xx || 0);
1795
+ const dbStatus = normalizeString(health?.db_status).toLowerCase();
1796
+
1797
+ if (Number.isFinite(cpuPercent) && cpuPercent >= 88) {
1798
+ setRiskPill(ui.riskCpu, `CPU alta: ${cpuPercent.toFixed(1)}%`, 'danger');
1799
+ } else if (Number.isFinite(cpuPercent) && cpuPercent >= 75) {
1800
+ setRiskPill(ui.riskCpu, `CPU atenção: ${cpuPercent.toFixed(1)}%`, 'warn');
1801
+ } else {
1802
+ setRiskPill(ui.riskCpu, 'CPU normal');
1803
+ }
1804
+
1805
+ if (spamBlocked >= 220) {
1806
+ setRiskPill(ui.riskSpam, `Spam elevado: ${formatNumber(spamBlocked)}`, 'danger');
1807
+ } else if (spamBlocked >= 90) {
1808
+ setRiskPill(ui.riskSpam, `Spam em atenção: ${formatNumber(spamBlocked)}`, 'warn');
1809
+ } else {
1810
+ setRiskPill(ui.riskSpam, 'Spam sob controle');
1811
+ }
1812
+
1813
+ if (activeBans >= 30) {
1814
+ setRiskPill(ui.riskBans, `Bans críticos: ${formatNumber(activeBans)}`, 'danger');
1815
+ } else if (activeBans >= 10) {
1816
+ setRiskPill(ui.riskBans, `Bans em alta: ${formatNumber(activeBans)}`, 'warn');
1817
+ } else {
1818
+ setRiskPill(ui.riskBans, 'Bans estáveis');
1819
+ }
1820
+
1821
+ if (errors5xx >= 30) {
1822
+ setRiskPill(ui.riskErrors, `Erros críticos: ${formatNumber(errors5xx)}`, 'danger');
1823
+ } else if (errors5xx >= 10) {
1824
+ setRiskPill(ui.riskErrors, `Erros em atenção: ${formatNumber(errors5xx)}`, 'warn');
1825
+ } else {
1826
+ setRiskPill(ui.riskErrors, 'Erros baixos');
1827
+ }
1828
+
1829
+ if (dbStatus === 'down' || hasCriticalAlert || errors5xx >= 30 || (Number.isFinite(cpuPercent) && cpuPercent >= 92)) {
1830
+ setGlobalStatusChip('incident', 'Incident');
1831
+ return;
1832
+ }
1833
+ if (dbStatus === 'degraded' || hasMediumAlert || errors5xx >= 10 || (Number.isFinite(cpuPercent) && cpuPercent >= 75)) {
1834
+ setGlobalStatusChip('warning', 'Warning');
1835
+ return;
1836
+ }
1837
+ setGlobalStatusChip('online', 'Online');
1838
+ };
1839
+
1840
+ const renderAdminOverview = () => {
1841
+ const payload = state.adminOverviewPayload || {};
1842
+ const previousPayload = state.previousAdminOverviewPayload || null;
1843
+ const counters = isObject(payload?.counters) ? payload.counters : {};
1844
+ const dashboard = isObject(payload?.dashboard_quick) ? payload.dashboard_quick : {};
1845
+ const usersSessions = isObject(payload?.users_sessions) ? payload.users_sessions : {};
1846
+ const health = isObject(payload?.system_health) ? payload.system_health : {};
1847
+ const previousCounters = isObject(previousPayload?.counters) ? previousPayload.counters : {};
1848
+ const previousDashboard = isObject(previousPayload?.dashboard_quick) ? previousPayload.dashboard_quick : {};
1849
+ const hasPrevious = Boolean(previousPayload);
1850
+ const lastUpdated = normalizeString(payload?.updated_at);
1851
+ const lastUpdatedLabel = lastUpdated ? `${formatDateTime(lastUpdated)} (${formatRelativeTime(lastUpdated)})` : 'n/d';
1852
+
1853
+ state.moderationFilterSeverity = normalizeString(ui.adminModerationFilterSeverity?.value || state.moderationFilterSeverity || 'all').toLowerCase();
1854
+ state.moderationFilterType = normalizeString(ui.adminModerationFilterType?.value || state.moderationFilterType || 'all').toLowerCase();
1855
+ state.auditFilterStatus = normalizeString(ui.adminAuditFilterStatus?.value || state.auditFilterStatus || 'all').toLowerCase();
1856
+ state.auditSearchQuery = normalizeString(ui.adminAuditSearch?.value || state.auditSearchQuery || '');
1857
+
1858
+ const botsOnline = toFiniteNumber(dashboard?.bots_online, 0);
1859
+ const messagesToday = toFiniteNumber(dashboard?.messages_today, 0);
1860
+ const spamBlockedToday = toFiniteNumber(dashboard?.spam_blocked_today, 0);
1861
+ const errors5xx = toFiniteNumber(dashboard?.errors_5xx, 0);
1862
+ const totalPacks = toFiniteNumber(counters?.total_packs_any_status, 0);
1863
+ const totalStickers = toFiniteNumber(counters?.total_stickers_any_status, 0);
1864
+ const activeBans = toFiniteNumber(counters?.active_bans, 0);
1865
+ const knownUsers = toFiniteNumber(counters?.known_google_users, 0);
1866
+ const activeSessions = toFiniteNumber(counters?.active_google_sessions, 0);
1867
+ const visits24h = toFiniteNumber(counters?.visit_events_24h, 0);
1868
+ const visits7d = toFiniteNumber(counters?.visit_events_7d, 0);
1869
+ const uniqueVisitors7d = toFiniteNumber(counters?.unique_visitors_7d, 0);
1870
+
1871
+ setText(ui.adminBotsOnline, formatIntegerOrNd(botsOnline));
1872
+ setText(ui.adminMessagesToday, formatIntegerOrNd(messagesToday));
1873
+ setText(ui.adminSpamBlocked, formatIntegerOrNd(spamBlockedToday));
1874
+ setText(ui.adminUptime, normalizeString(dashboard?.uptime) || 'n/d');
1875
+ setText(ui.adminErrors5xx, formatIntegerOrNd(errors5xx));
1876
+ setText(ui.adminTotalPacks, formatIntegerOrNd(totalPacks));
1877
+ setText(ui.adminTotalStickers, formatIntegerOrNd(totalStickers));
1878
+ setText(ui.adminActiveBans, formatIntegerOrNd(activeBans));
1879
+ setText(ui.adminKnownUsers, formatIntegerOrNd(knownUsers));
1880
+ setText(ui.adminActiveSessions, formatIntegerOrNd(activeSessions));
1881
+ setText(ui.adminVisits24h, formatIntegerOrNd(visits24h));
1882
+ setText(ui.adminVisits7d, formatIntegerOrNd(visits7d));
1883
+ setText(ui.adminUniqueVisitors7d, formatIntegerOrNd(uniqueVisitors7d));
1884
+
1885
+ const deltaOrNd = (current, previous, options) => (hasPrevious ? formatDeltaLabel(current, previous, options) : 'n/d');
1886
+ setMetricContext(ui.adminBotsOnlineContext, `vs leitura anterior: ${deltaOrNd(botsOnline, toFiniteNumber(previousDashboard?.bots_online, botsOnline))}`);
1887
+ setMetricContext(ui.adminMessagesTodayContext, `vs leitura anterior: ${deltaOrNd(messagesToday, toFiniteNumber(previousDashboard?.messages_today, messagesToday))}`);
1888
+ setMetricContext(ui.adminUptimeContext, 'janela: processo atual');
1889
+ setMetricContext(ui.adminErrors5xxContext, `vs leitura anterior: ${deltaOrNd(errors5xx, toFiniteNumber(previousDashboard?.errors_5xx, errors5xx), { percent: false, suffix: ' eventos' })}`);
1890
+ setMetricContext(ui.adminTotalPacksContext, `delta leitura: ${deltaOrNd(totalPacks, toFiniteNumber(previousCounters?.total_packs_any_status, totalPacks), { percent: false })}`);
1891
+ setMetricContext(ui.adminTotalStickersContext, `delta leitura: ${deltaOrNd(totalStickers, toFiniteNumber(previousCounters?.total_stickers_any_status, totalStickers), { percent: false })}`);
1892
+ setMetricContext(ui.adminSpamBlockedContext, `vs leitura anterior: ${deltaOrNd(spamBlockedToday, toFiniteNumber(previousDashboard?.spam_blocked_today, spamBlockedToday))}`);
1893
+ setMetricContext(ui.adminActiveBansContext, `delta leitura: ${deltaOrNd(activeBans, toFiniteNumber(previousCounters?.active_bans, activeBans), { percent: false })}`);
1894
+ setMetricContext(ui.adminKnownUsersContext, `delta leitura: ${deltaOrNd(knownUsers, toFiniteNumber(previousCounters?.known_google_users, knownUsers), { percent: false })}`);
1895
+ setMetricContext(ui.adminActiveSessionsContext, `delta leitura: ${deltaOrNd(activeSessions, toFiniteNumber(previousCounters?.active_google_sessions, activeSessions), { percent: false })}`);
1896
+ setMetricContext(ui.adminVisits24hContext, `leitura atual: ${formatNumber(visits24h)} eventos`);
1897
+ setMetricContext(ui.adminVisits7dContext, `leitura atual: ${formatNumber(visits7d)} eventos`);
1898
+ setMetricContext(ui.adminUniqueVisitors7dContext, `leitura atual: ${formatNumber(uniqueVisitors7d)} visitantes`);
1899
+ setText(ui.adminLastUpdated, lastUpdatedLabel);
1900
+
1901
+ renderSystemHealth(health);
1902
+ setText(ui.adminHealthCpuMeta, `Limite: 88% • Atualizado ${formatRelativeTime(lastUpdated)}`);
1903
+ setText(ui.adminHealthRamMeta, `Limite: 90% • Atualizado ${formatRelativeTime(lastUpdated)}`);
1904
+ setText(ui.adminHealthLatencyMeta, `Alerta: >300ms • Atualizado ${formatRelativeTime(lastUpdated)}`);
1905
+ setText(ui.adminHealthQueueMeta, `Ideal: <120 jobs • Atualizado ${formatRelativeTime(lastUpdated)}`);
1906
+ setText(ui.adminHealthDbMeta, `SLA alvo: 99.95% • Atualizado ${formatRelativeTime(lastUpdated)}`);
1907
+ renderModerationQueue(payload?.moderation_queue);
1908
+ renderActiveSessions(usersSessions?.active_sessions);
1909
+ renderKnownUsers(usersSessions?.users);
1910
+ renderBlockedAccounts(usersSessions?.blocked_accounts);
1911
+ renderAuditLog(payload?.audit_log);
1912
+ renderFeatureFlags(payload?.feature_flags);
1913
+ renderAlerts(payload?.alerts);
1914
+ updateOperationalSignals({
1915
+ counters,
1916
+ dashboard,
1917
+ health,
1918
+ alerts: payload?.alerts,
1919
+ });
1920
+ renderSearchResults();
1921
+ setText(ui.adminOpsStatus, state.adminOpsMessage || 'Ações operacionais disponíveis.');
1922
+ updateAdminCarouselControls();
1923
+ };
1924
+
1925
+ const renderAdminPanel = () => {
1926
+ if (!ui.adminPanel) return;
1927
+
1928
+ const enabled = state.adminStatusPayload?.enabled !== false;
1929
+ const authenticated = isAdminAuthenticated();
1930
+ const eligible = isAdminEligible();
1931
+ const role = resolveAdminRole();
1932
+
1933
+ if (!enabled || (!eligible && !authenticated)) {
1934
+ ui.adminPanel.hidden = true;
1935
+ if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = true;
1936
+ if (ui.adminOverview) ui.adminOverview.hidden = true;
1937
+ showAdminError('');
1938
+ return;
1939
+ }
1940
+
1941
+ ui.adminPanel.hidden = false;
1942
+ setText(ui.adminRole, formatAdminRole(role));
1943
+
1944
+ if (authenticated) {
1945
+ setText(ui.adminStatus, `Sessão admin ativa como ${formatAdminRole(role)}. Ferramentas operacionais liberadas abaixo.`);
1946
+ if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = true;
1947
+ if (ui.adminOverview) ui.adminOverview.hidden = false;
1948
+ renderAdminOverview();
1949
+ } else {
1950
+ setText(ui.adminStatus, `Conta elegível para admin (${formatAdminRole(role)}). Informe a senha para liberar os dados sensíveis.`);
1951
+ if (ui.adminUnlockForm) ui.adminUnlockForm.hidden = false;
1952
+ if (ui.adminOverview) ui.adminOverview.hidden = true;
1953
+ setAdminMetricsDefaults();
1954
+ }
1955
+
1956
+ setAdminBusy(state.adminBusy);
1957
+ };
1958
+
1959
+ const loadBotPhone = async () => {
1960
+ try {
1961
+ const payload = await fetchJson(botContactApiPath, { method: 'GET' });
1962
+ state.botPhone = normalizeDigits(payload?.data?.phone || '');
1963
+ } catch {
1964
+ state.botPhone = '';
1965
+ }
1966
+ if (ui.chatLink) ui.chatLink.href = buildWhatsAppMenuUrl(state.botPhone);
1967
+ };
1968
+
1969
+ const loadAdminStatus = async () => {
1970
+ const payload = await fetchJson(adminSessionApiPath, { method: 'GET' });
1971
+ state.adminStatusPayload = payload?.data || null;
1972
+ };
1973
+
1974
+ const loadAdminOverview = async () => {
1975
+ if (!isAdminAuthenticated()) {
1976
+ state.previousAdminOverviewPayload = null;
1977
+ state.adminOverviewPayload = null;
1978
+ return;
1979
+ }
1980
+ const payload = await fetchJson(adminOverviewApiPath, { method: 'GET' });
1981
+ state.previousAdminOverviewPayload = state.adminOverviewPayload || null;
1982
+ state.adminOverviewPayload = payload?.data || null;
1983
+ };
1984
+
1985
+ const refreshAdminArea = async ({ keepCurrentError = false } = {}) => {
1986
+ if (!keepCurrentError) showAdminError('');
1987
+ try {
1988
+ await loadAdminStatus();
1989
+ await loadAdminOverview();
1990
+ } catch (error) {
1991
+ if (error?.statusCode === 404) {
1992
+ state.adminStatusPayload = { enabled: false };
1993
+ state.previousAdminOverviewPayload = null;
1994
+ state.adminOverviewPayload = null;
1995
+ } else {
1996
+ showAdminError(error?.message || 'Falha ao carregar área admin.');
1997
+ }
1998
+ }
1999
+ renderAdminPanel();
2000
+ };
2001
+
2002
+ const setCompactMode = (enabled, { persist = true } = {}) => {
2003
+ state.compactMode = Boolean(enabled);
2004
+ document.body.classList.toggle('compact', state.compactMode);
2005
+ if (ui.compactToggle) {
2006
+ ui.compactToggle.textContent = state.compactMode ? 'Modo confortável' : 'Modo compacto';
2007
+ }
2008
+
2009
+ const activeLink = ui.navLinks.find((link) => link.classList.contains('active'));
2010
+ const activeTarget = normalizeString(activeLink?.getAttribute('href')).replace(/^#/, '');
2011
+ if (isCarouselMode()) {
2012
+ if (activeTarget) {
2013
+ scrollAdminCarouselToId(activeTarget, { behavior: 'auto' });
2014
+ } else {
2015
+ scrollAdminCarouselToIndex(state.adminCarouselIndex, { behavior: 'auto' });
2016
+ }
2017
+ } else if (ui.adminLayout) {
2018
+ ui.adminLayout.style.blockSize = '';
2019
+ }
2020
+
2021
+ updateAdminCarouselControls();
2022
+ if (!persist) return;
2023
+ try {
2024
+ window.localStorage.setItem(COMPACT_MODE_STORAGE_KEY, state.compactMode ? '1' : '0');
2025
+ } catch {
2026
+ // noop
2027
+ }
2028
+ };
2029
+
2030
+ const restoreCompactMode = () => {
2031
+ try {
2032
+ const raw = window.localStorage.getItem(COMPACT_MODE_STORAGE_KEY);
2033
+ setCompactMode(raw === '1', { persist: false });
2034
+ } catch {
2035
+ setCompactMode(false, { persist: false });
2036
+ }
2037
+ };
2038
+
2039
+ const bindSectionObserver = () => {
2040
+ if (!ui.navLinks.length) return;
2041
+ const subpageSections = getAdminSubpageSections();
2042
+ if (subpageSections.length) {
2043
+ const handleRouteChange = () => {
2044
+ activateAdminSubpage(window.location.hash || 'overview', {
2045
+ syncHash: true,
2046
+ resetScroll: false,
2047
+ });
2048
+ };
2049
+
2050
+ for (const link of ui.navLinks) {
2051
+ const href = normalizeString(link.getAttribute('href'));
2052
+ if (!href.startsWith('#')) continue;
2053
+ link.addEventListener('click', (event) => {
2054
+ const targetId = href.slice(1);
2055
+ if (!targetId) return;
2056
+ if (!activateAdminSubpage(targetId, { syncHash: true })) return;
2057
+ event.preventDefault();
2058
+ });
2059
+ }
2060
+
2061
+ window.addEventListener('hashchange', handleRouteChange);
2062
+ handleRouteChange();
2063
+ return;
2064
+ }
2065
+
2066
+ const entries = [];
2067
+ for (const link of ui.navLinks) {
2068
+ const href = normalizeString(link.getAttribute('href'));
2069
+ if (!href.startsWith('#')) continue;
2070
+ const target = document.querySelector(href);
2071
+ if (target) entries.push({ link, target });
2072
+ }
2073
+ if (!entries.length) return;
2074
+
2075
+ if (!('IntersectionObserver' in window)) {
2076
+ setActiveNavLink(entries[0].target.id);
2077
+ return;
2078
+ }
2079
+
2080
+ const observer = new window.IntersectionObserver(
2081
+ (observed) => {
2082
+ if (isCarouselMode()) return;
2083
+ const visible = observed.filter((entry) => entry.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
2084
+ if (visible?.target?.id) setActiveNavLink(visible.target.id);
2085
+ },
2086
+ {
2087
+ root: null,
2088
+ rootMargin: '-35% 0px -55% 0px',
2089
+ threshold: [0.1, 0.25, 0.45, 0.7],
2090
+ },
2091
+ );
2092
+
2093
+ for (const item of entries) observer.observe(item.target);
2094
+ setActiveNavLink(entries[0].target.id);
2095
+ };
2096
+
2097
+ const bindKeyboardShortcuts = () => {
2098
+ window.addEventListener('keydown', (event) => {
2099
+ const isModKey = event.ctrlKey || event.metaKey;
2100
+ if (isModKey && event.key.toLowerCase() === 'k') {
2101
+ event.preventDefault();
2102
+ ui.adminSearchInput?.focus();
2103
+ ui.adminSearchInput?.select();
2104
+ }
2105
+ });
2106
+ };
2107
+
2108
+ const handleAdminUnlock = async () => {
2109
+ const password = normalizeString(ui.adminPassword?.value);
2110
+ if (!password) {
2111
+ showAdminError('Informe a senha do painel admin.');
2112
+ return;
2113
+ }
2114
+ if (state.adminBusy) return;
2115
+
2116
+ showAdminError('');
2117
+ setButtonProcessing(ui.adminUnlockBtn, 'Desbloqueando...');
2118
+ setAdminBusy(true);
2119
+ let unlocked = false;
2120
+ try {
2121
+ const payload = await fetchJson(adminSessionApiPath, {
2122
+ method: 'POST',
2123
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
2124
+ body: JSON.stringify({ password }),
2125
+ });
2126
+ state.adminStatusPayload = payload?.data || null;
2127
+ state.adminOpsMessage = '';
2128
+ state.adminSearchPayload = null;
2129
+ if (ui.adminPassword) ui.adminPassword.value = '';
2130
+ await loadAdminOverview();
2131
+ showToast({
2132
+ kind: 'success',
2133
+ title: 'Admin',
2134
+ message: 'Área administrativa desbloqueada com sucesso.',
2135
+ });
2136
+ unlocked = true;
2137
+ } catch (error) {
2138
+ showAdminError(error?.message || 'Falha ao desbloquear área admin.');
2139
+ await loadAdminStatus().catch(() => {});
2140
+ state.previousAdminOverviewPayload = null;
2141
+ state.adminOverviewPayload = null;
2142
+ } finally {
2143
+ setAdminBusy(false);
2144
+ setButtonIdle(ui.adminUnlockBtn);
2145
+ if (unlocked) flashButtonSuccess(ui.adminUnlockBtn, 'Liberado');
2146
+ renderAdminPanel();
2147
+ }
2148
+ };
2149
+
2150
+ const handleAdminLogout = async (triggerButton = null) => {
2151
+ if (state.adminBusy) return;
2152
+ if (!confirmCriticalAction('Encerrar sessão administrativa atual?')) return;
2153
+ showAdminError('');
2154
+ setButtonProcessing(triggerButton, 'Saindo...');
2155
+ setAdminBusy(true);
2156
+ try {
2157
+ await fetchJson(adminSessionApiPath, { method: 'DELETE' });
2158
+ } catch {
2159
+ // no-op
2160
+ }
2161
+ state.previousAdminOverviewPayload = null;
2162
+ state.adminOverviewPayload = null;
2163
+ state.adminSearchPayload = null;
2164
+ state.adminOpsMessage = '';
2165
+ await loadAdminStatus().catch(() => {
2166
+ state.adminStatusPayload = null;
2167
+ });
2168
+ showToast({ kind: 'success', title: 'Admin', message: 'Sessão administrativa encerrada.' });
2169
+ setAdminBusy(false);
2170
+ setButtonIdle(triggerButton);
2171
+ flashButtonSuccess(triggerButton, 'Encerrado');
2172
+ renderAdminPanel();
2173
+ };
2174
+
2175
+ const handleAdminRefresh = async (triggerButton = null) => {
2176
+ if (state.adminBusy) return;
2177
+ setButtonProcessing(triggerButton, 'Atualizando...');
2178
+ setAdminBusy(true);
2179
+ await refreshAdminArea({ keepCurrentError: false });
2180
+ setAdminBusy(false);
2181
+ setButtonIdle(triggerButton);
2182
+ flashButtonSuccess(triggerButton, 'Atualizado');
2183
+ renderAdminPanel();
2184
+ showToast({
2185
+ kind: 'success',
2186
+ title: 'Atualização',
2187
+ message: 'Dados administrativos atualizados.',
2188
+ });
2189
+ };
2190
+
2191
+ const handleAdminSearchSubmit = async () => {
2192
+ if (state.adminBusy) return;
2193
+
2194
+ const q = normalizeString(ui.adminSearchInput?.value);
2195
+ if (!q) {
2196
+ state.adminSearchPayload = null;
2197
+ renderSearchResults();
2198
+ return;
2199
+ }
2200
+
2201
+ showAdminError('');
2202
+ setButtonProcessing(ui.adminSearchBtn, 'Buscando...');
2203
+ setAdminBusy(true);
2204
+ try {
2205
+ const query = new URLSearchParams({ q, limit: '12' }).toString();
2206
+ const payload = await fetchJson(`${adminSearchApiPath}?${query}`, { method: 'GET' });
2207
+ state.adminSearchPayload = payload?.data || null;
2208
+ state.adminOpsMessage = `Busca concluída para "${q}".`;
2209
+ renderSearchResults();
2210
+ setText(ui.adminOpsStatus, state.adminOpsMessage);
2211
+ showToast({ kind: 'success', title: 'Busca', message: state.adminOpsMessage });
2212
+ } catch (error) {
2213
+ showAdminError(error?.message || 'Falha ao buscar dados.');
2214
+ } finally {
2215
+ setAdminBusy(false);
2216
+ setButtonIdle(ui.adminSearchBtn);
2217
+ flashButtonSuccess(ui.adminSearchBtn, 'Concluído');
2218
+ renderAdminPanel();
2219
+ }
2220
+ };
2221
+
2222
+ const downloadBlob = (blob, filename) => {
2223
+ const objectUrl = window.URL.createObjectURL(blob);
2224
+ const link = document.createElement('a');
2225
+ link.href = objectUrl;
2226
+ link.download = filename;
2227
+ document.body.appendChild(link);
2228
+ link.click();
2229
+ link.remove();
2230
+ window.setTimeout(() => {
2231
+ window.URL.revokeObjectURL(objectUrl);
2232
+ }, 250);
2233
+ };
2234
+
2235
+ const extractFilenameFromDisposition = (contentDisposition, fallbackName) => {
2236
+ const source = normalizeString(contentDisposition);
2237
+ if (!source) return fallbackName;
2238
+ const utf8Match = source.match(/filename\*=UTF-8''([^;]+)/i);
2239
+ if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
2240
+ const plainMatch = source.match(/filename="?([^"]+)"?/i);
2241
+ if (plainMatch?.[1]) return plainMatch[1];
2242
+ return fallbackName;
2243
+ };
2244
+
2245
+ const handleAdminExport = async ({ type = 'metrics', format = 'json', triggerButton = null } = {}) => {
2246
+ if (state.adminBusy) return;
2247
+
2248
+ const normalizedType = normalizeString(type || 'metrics').toLowerCase();
2249
+ const normalizedFormat = normalizeString(format || 'json').toLowerCase();
2250
+ const fallbackName = `admin-${normalizedType}-${Date.now()}.${normalizedFormat === 'csv' ? 'csv' : 'json'}`;
2251
+
2252
+ showAdminError('');
2253
+ setButtonProcessing(triggerButton, 'Exportando...');
2254
+ setAdminBusy(true);
2255
+ let exported = false;
2256
+ try {
2257
+ const query = new URLSearchParams({
2258
+ type: normalizedType,
2259
+ format: normalizedFormat,
2260
+ }).toString();
2261
+ const response = await fetchWithAuth(`${adminExportApiPath}?${query}`, { method: 'GET' });
2262
+
2263
+ if (normalizedFormat === 'csv') {
2264
+ const blob = await response.blob();
2265
+ const contentDisposition = response.headers.get('content-disposition');
2266
+ const filename = extractFilenameFromDisposition(contentDisposition, fallbackName);
2267
+ downloadBlob(blob, filename);
2268
+ } else {
2269
+ const payload = await response.json().catch(() => ({}));
2270
+ const blob = new Blob([JSON.stringify(payload?.data || payload || {}, null, 2)], {
2271
+ type: 'application/json; charset=utf-8',
2272
+ });
2273
+ downloadBlob(blob, fallbackName);
2274
+ }
2275
+
2276
+ state.adminOpsMessage = `Exportação ${normalizedType.toUpperCase()} (${normalizedFormat.toUpperCase()}) concluída.`;
2277
+ setText(ui.adminOpsStatus, state.adminOpsMessage);
2278
+ showToast({ kind: 'success', title: 'Exportação', message: state.adminOpsMessage });
2279
+ exported = true;
2280
+ } catch (error) {
2281
+ showAdminError(error?.message || 'Falha ao exportar dados.');
2282
+ } finally {
2283
+ setAdminBusy(false);
2284
+ setButtonIdle(triggerButton);
2285
+ if (exported) flashButtonSuccess(triggerButton, 'Baixado');
2286
+ renderAdminPanel();
2287
+ }
2288
+ };
2289
+
2290
+ const handleLogout = async () => {
2291
+ if (!ui.logoutBtn) return;
2292
+ if (!confirmCriticalAction('Encerrar sessão da conta atual?')) return;
2293
+ setButtonProcessing(ui.logoutBtn, 'Encerrando...');
2294
+ try {
2295
+ await fetchJson(sessionApiPath, { method: 'DELETE' });
2296
+ } catch {
2297
+ // clear local navigation even if request fails
2298
+ }
2299
+ window.location.assign(`${state.loginPath}/`);
2300
+ };
2301
+
2302
+ const init = async () => {
2303
+ const manageHref = `${state.stickersPath.replace(/\/+$/, '') || DEFAULT_STICKERS_PATH}/perfil`;
2304
+ if (ui.manageHeadLink) ui.manageHeadLink.href = manageHref;
2305
+ if (ui.manageMainLink) ui.manageMainLink.href = manageHref;
2306
+ if (ui.currentYear) ui.currentYear.textContent = String(new Date().getFullYear());
2307
+ applyEnvironmentBadge();
2308
+ restoreCompactMode();
2309
+ bindAdminCarousel();
2310
+ bindSectionObserver();
2311
+ bindKeyboardShortcuts();
2312
+
2313
+ setText(ui.status, 'Validando sua sessão...');
2314
+ showError('');
2315
+ setAdminMetricsDefaults();
2316
+
2317
+ let sessionData = null;
2318
+ try {
2319
+ const sessionPayload = await fetchJson(sessionApiPath, { method: 'GET' });
2320
+ sessionData = sessionPayload?.data || {};
2321
+ if (!sessionData?.authenticated || !sessionData?.user?.sub) {
2322
+ redirectToLogin();
2323
+ return;
2324
+ }
2325
+ } catch (error) {
2326
+ showError(error?.message || 'Falha ao validar sessão.');
2327
+ setText(ui.status, 'Não foi possível validar sua sessão agora.');
2328
+ return;
2329
+ }
2330
+
2331
+ renderSession(sessionData);
2332
+ await loadBotPhone();
2333
+ await refreshAdminArea();
2334
+
2335
+ try {
2336
+ const myProfilePayload = await fetchJson(myProfileApiPath, { method: 'GET' });
2337
+ const sessionOk = Boolean(myProfilePayload?.data?.session?.authenticated);
2338
+ if (!sessionOk) {
2339
+ redirectToLogin();
2340
+ return;
2341
+ }
2342
+ renderPackMetrics(myProfilePayload);
2343
+ setText(ui.status, 'Sessão ativa. Dados da sua conta carregados com sucesso.');
2344
+ } catch (error) {
2345
+ showError(error?.message || 'Falha ao carregar dados da conta.');
2346
+ setText(ui.status, 'Sessão ativa, mas não foi possível carregar todos os dados.');
2347
+ }
2348
+ };
2349
+
2350
+ if (ui.logoutBtn) {
2351
+ ui.logoutBtn.addEventListener('click', () => {
2352
+ void handleLogout();
2353
+ });
2354
+ }
2355
+
2356
+ if (ui.adminUnlockForm) {
2357
+ ui.adminUnlockForm.addEventListener('submit', (event) => {
2358
+ event.preventDefault();
2359
+ void handleAdminUnlock();
2360
+ });
2361
+ }
2362
+
2363
+ if (ui.adminLogoutBtn) {
2364
+ ui.adminLogoutBtn.addEventListener('click', (event) => {
2365
+ const button = event.currentTarget instanceof Element ? event.currentTarget : ui.adminLogoutBtn;
2366
+ void handleAdminLogout(button);
2367
+ });
2368
+ }
2369
+
2370
+ if (ui.adminRefreshBtn) {
2371
+ ui.adminRefreshBtn.addEventListener('click', (event) => {
2372
+ const button = event.currentTarget instanceof Element ? event.currentTarget : ui.adminRefreshBtn;
2373
+ void handleAdminRefresh(button);
2374
+ });
2375
+ }
2376
+
2377
+ if (ui.adminSearchForm) {
2378
+ ui.adminSearchForm.addEventListener('submit', (event) => {
2379
+ event.preventDefault();
2380
+ void handleAdminSearchSubmit();
2381
+ });
2382
+ }
2383
+
2384
+ if (ui.compactToggle) {
2385
+ ui.compactToggle.addEventListener('click', () => {
2386
+ setCompactMode(!state.compactMode);
2387
+ });
2388
+ }
2389
+
2390
+ if (ui.adminModerationFilterSeverity) {
2391
+ ui.adminModerationFilterSeverity.addEventListener('change', () => {
2392
+ state.moderationFilterSeverity = normalizeString(ui.adminModerationFilterSeverity?.value || 'all').toLowerCase();
2393
+ state.moderationPage = 1;
2394
+ renderModerationQueue(state.adminOverviewPayload?.moderation_queue);
2395
+ });
2396
+ }
2397
+
2398
+ if (ui.adminModerationFilterType) {
2399
+ ui.adminModerationFilterType.addEventListener('change', () => {
2400
+ state.moderationFilterType = normalizeString(ui.adminModerationFilterType?.value || 'all').toLowerCase();
2401
+ state.moderationPage = 1;
2402
+ renderModerationQueue(state.adminOverviewPayload?.moderation_queue);
2403
+ });
2404
+ }
2405
+
2406
+ if (ui.adminAuditFilterStatus) {
2407
+ ui.adminAuditFilterStatus.addEventListener('change', () => {
2408
+ state.auditFilterStatus = normalizeString(ui.adminAuditFilterStatus?.value || 'all').toLowerCase();
2409
+ state.auditPage = 1;
2410
+ renderAuditLog(state.adminOverviewPayload?.audit_log);
2411
+ });
2412
+ }
2413
+
2414
+ if (ui.adminAuditSearch) {
2415
+ ui.adminAuditSearch.addEventListener('input', () => {
2416
+ state.auditSearchQuery = normalizeString(ui.adminAuditSearch?.value || '');
2417
+ state.auditPage = 1;
2418
+ renderAuditLog(state.adminOverviewPayload?.audit_log);
2419
+ });
2420
+ }
2421
+
2422
+ if (ui.adminModerationPrevBtn) {
2423
+ ui.adminModerationPrevBtn.addEventListener('click', () => {
2424
+ state.moderationPage = Math.max(1, state.moderationPage - 1);
2425
+ renderModerationQueue(state.adminOverviewPayload?.moderation_queue);
2426
+ });
2427
+ }
2428
+
2429
+ if (ui.adminModerationNextBtn) {
2430
+ ui.adminModerationNextBtn.addEventListener('click', () => {
2431
+ state.moderationPage += 1;
2432
+ renderModerationQueue(state.adminOverviewPayload?.moderation_queue);
2433
+ });
2434
+ }
2435
+
2436
+ if (ui.adminAuditPrevBtn) {
2437
+ ui.adminAuditPrevBtn.addEventListener('click', () => {
2438
+ state.auditPage = Math.max(1, state.auditPage - 1);
2439
+ renderAuditLog(state.adminOverviewPayload?.audit_log);
2440
+ });
2441
+ }
2442
+
2443
+ if (ui.adminAuditNextBtn) {
2444
+ ui.adminAuditNextBtn.addEventListener('click', () => {
2445
+ state.auditPage += 1;
2446
+ renderAuditLog(state.adminOverviewPayload?.audit_log);
2447
+ });
2448
+ }
2449
+
2450
+ if (ui.adminUsersPrevBtn) {
2451
+ ui.adminUsersPrevBtn.addEventListener('click', () => {
2452
+ state.usersPage = Math.max(1, state.usersPage - 1);
2453
+ renderKnownUsers(state.adminOverviewPayload?.users_sessions?.users);
2454
+ });
2455
+ }
2456
+
2457
+ if (ui.adminUsersNextBtn) {
2458
+ ui.adminUsersNextBtn.addEventListener('click', () => {
2459
+ state.usersPage += 1;
2460
+ renderKnownUsers(state.adminOverviewPayload?.users_sessions?.users);
2461
+ });
2462
+ }
2463
+
2464
+ if (ui.adminSessionsPrevBtn) {
2465
+ ui.adminSessionsPrevBtn.addEventListener('click', () => {
2466
+ state.sessionsPage = Math.max(1, state.sessionsPage - 1);
2467
+ renderActiveSessions(state.adminOverviewPayload?.users_sessions?.active_sessions);
2468
+ });
2469
+ }
2470
+
2471
+ if (ui.adminSessionsNextBtn) {
2472
+ ui.adminSessionsNextBtn.addEventListener('click', () => {
2473
+ state.sessionsPage += 1;
2474
+ renderActiveSessions(state.adminOverviewPayload?.users_sessions?.active_sessions);
2475
+ });
2476
+ }
2477
+
2478
+ if (ui.adminAlertsPrevBtn) {
2479
+ ui.adminAlertsPrevBtn.addEventListener('click', () => {
2480
+ state.alertsPage = Math.max(1, state.alertsPage - 1);
2481
+ renderAlerts(state.adminOverviewPayload?.alerts);
2482
+ });
2483
+ }
2484
+
2485
+ if (ui.adminAlertsNextBtn) {
2486
+ ui.adminAlertsNextBtn.addEventListener('click', () => {
2487
+ state.alertsPage += 1;
2488
+ renderAlerts(state.adminOverviewPayload?.alerts);
2489
+ });
2490
+ }
2491
+
2492
+ if (ui.adminOpButtons.length) {
2493
+ for (const button of ui.adminOpButtons) {
2494
+ button.addEventListener('click', () => {
2495
+ const action = normalizeString(button.dataset.adminOpAction);
2496
+ if (!action) return;
2497
+ void handleAdminOpsAction(action, button);
2498
+ });
2499
+ }
2500
+ }
2501
+
2502
+ if (ui.adminExportMetricsJsonBtn) {
2503
+ ui.adminExportMetricsJsonBtn.addEventListener('click', () => {
2504
+ void handleAdminExport({
2505
+ type: 'metrics',
2506
+ format: 'json',
2507
+ triggerButton: ui.adminExportMetricsJsonBtn,
2508
+ });
2509
+ });
2510
+ }
2511
+ if (ui.adminExportMetricsCsvBtn) {
2512
+ ui.adminExportMetricsCsvBtn.addEventListener('click', () => {
2513
+ void handleAdminExport({
2514
+ type: 'metrics',
2515
+ format: 'csv',
2516
+ triggerButton: ui.adminExportMetricsCsvBtn,
2517
+ });
2518
+ });
2519
+ }
2520
+ if (ui.adminExportEventsJsonBtn) {
2521
+ ui.adminExportEventsJsonBtn.addEventListener('click', () => {
2522
+ void handleAdminExport({
2523
+ type: 'events',
2524
+ format: 'json',
2525
+ triggerButton: ui.adminExportEventsJsonBtn,
2526
+ });
2527
+ });
2528
+ }
2529
+ if (ui.adminExportEventsCsvBtn) {
2530
+ ui.adminExportEventsCsvBtn.addEventListener('click', () => {
2531
+ void handleAdminExport({
2532
+ type: 'events',
2533
+ format: 'csv',
2534
+ triggerButton: ui.adminExportEventsCsvBtn,
2535
+ });
2536
+ });
2537
+ }
2538
+
2539
+ void init();
2540
+ }