@omnizap-system/omnizap 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.env.example +78 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { nowIso as __timeNowIso } from '#time';
4
+
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import process from 'node:process';
8
+
9
+ const rawBaseUrl = String(process.env.SECURITY_WEB_SURFACE_BASE_URL || 'https://omnizap.shop').trim() || 'https://omnizap.shop';
10
+ const reportPath = String(process.env.SECURITY_WEB_SURFACE_REPORT_PATH || './temp/security-web-surface-report.json').trim();
11
+ const requestTimeoutMs = Math.max(1_000, Number(process.env.SECURITY_WEB_SURFACE_TIMEOUT_MS || 10_000));
12
+
13
+ const toBaseOrigin = (value) => {
14
+ const raw = String(value || '').trim();
15
+ if (!raw) return 'https://omnizap.shop';
16
+ try {
17
+ const parsed = new URL(raw);
18
+ return parsed.origin;
19
+ } catch {
20
+ try {
21
+ const parsed = new URL(`https://${raw}`);
22
+ return parsed.origin;
23
+ } catch {
24
+ return 'https://omnizap.shop';
25
+ }
26
+ }
27
+ };
28
+
29
+ const baseOrigin = toBaseOrigin(rawBaseUrl);
30
+
31
+ const PASS = 'PASS';
32
+ const FAIL = 'FAIL';
33
+
34
+ const STATIC_REQUIRED_HEADERS = ['content-security-policy', 'permissions-policy', 'strict-transport-security', 'x-content-type-options', 'x-frame-options'];
35
+
36
+ const API_REQUIRED_HEADERS = ['content-security-policy', 'cross-origin-opener-policy', 'cross-origin-resource-policy', 'permissions-policy', 'strict-transport-security', 'x-content-type-options', 'x-frame-options'];
37
+
38
+ const checks = [
39
+ {
40
+ id: 1,
41
+ name: 'Root static page has security headers',
42
+ path: '/',
43
+ expectedStatuses: [200],
44
+ requiredHeaders: STATIC_REQUIRED_HEADERS,
45
+ },
46
+ {
47
+ id: 2,
48
+ name: 'Legal page has security headers',
49
+ path: '/notice-and-takedown/',
50
+ expectedStatuses: [200],
51
+ requiredHeaders: STATIC_REQUIRED_HEADERS,
52
+ },
53
+ {
54
+ id: 3,
55
+ name: 'Dotenv path is not exposed',
56
+ path: '/.env',
57
+ forbiddenStatuses: [200],
58
+ bodyLeakPatterns: [/DB_PASSWORD|MYSQL_PASSWORD|GITHUB_TOKEN|SECRET|PRIVATE_KEY/i],
59
+ },
60
+ {
61
+ id: 4,
62
+ name: 'Unknown path does not soft-fallback with 200',
63
+ path: '/__security_probe_nonexistent_omnizap__.txt',
64
+ expectedStatuses: [404],
65
+ },
66
+ {
67
+ id: 5,
68
+ name: 'Whitespace path fuzz does not return 200',
69
+ path: '/assets%20/',
70
+ forbiddenStatuses: [200],
71
+ },
72
+ {
73
+ id: 6,
74
+ name: 'API bootstrap keeps hardened headers',
75
+ path: '/api/home-bootstrap',
76
+ expectedStatuses: [200],
77
+ requiredHeaders: API_REQUIRED_HEADERS,
78
+ },
79
+ ];
80
+
81
+ const request = async (targetPath) => {
82
+ const normalizedPath = String(targetPath || '/').startsWith('/') ? String(targetPath || '/') : `/${String(targetPath || '/')}`;
83
+ const url = `${baseOrigin}${normalizedPath}`;
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
86
+
87
+ try {
88
+ const response = await fetch(url, {
89
+ method: 'GET',
90
+ redirect: 'manual',
91
+ signal: controller.signal,
92
+ });
93
+ const text = await response.text();
94
+ return {
95
+ ok: true,
96
+ url,
97
+ status: response.status,
98
+ headers: Object.fromEntries(response.headers.entries()),
99
+ body: text,
100
+ };
101
+ } catch (error) {
102
+ return {
103
+ ok: false,
104
+ url,
105
+ status: null,
106
+ headers: {},
107
+ body: '',
108
+ error: error?.message || String(error),
109
+ };
110
+ } finally {
111
+ clearTimeout(timeout);
112
+ }
113
+ };
114
+
115
+ const summarize = (items) =>
116
+ items.reduce(
117
+ (acc, item) => {
118
+ acc[item.status] = (acc[item.status] || 0) + 1;
119
+ return acc;
120
+ },
121
+ { PASS: 0, FAIL: 0 },
122
+ );
123
+
124
+ const runCheck = async (check) => {
125
+ const response = await request(check.path);
126
+ const reasons = [];
127
+
128
+ if (!response.ok) {
129
+ reasons.push(`network_error:${response.error || 'unknown'}`);
130
+ }
131
+
132
+ if (Array.isArray(check.expectedStatuses) && check.expectedStatuses.length > 0) {
133
+ if (!check.expectedStatuses.includes(response.status)) {
134
+ reasons.push(`unexpected_status:${response.status}`);
135
+ }
136
+ }
137
+
138
+ if (Array.isArray(check.forbiddenStatuses) && check.forbiddenStatuses.length > 0) {
139
+ if (check.forbiddenStatuses.includes(response.status)) {
140
+ reasons.push(`forbidden_status:${response.status}`);
141
+ }
142
+ }
143
+
144
+ const missingHeaders = [];
145
+ for (const headerName of check.requiredHeaders || []) {
146
+ const resolved = response.headers?.[headerName];
147
+ if (!resolved) missingHeaders.push(headerName);
148
+ }
149
+
150
+ if (missingHeaders.length > 0) {
151
+ reasons.push(`missing_headers:${missingHeaders.join(',')}`);
152
+ }
153
+
154
+ const leakPatternHit = (check.bodyLeakPatterns || []).find((pattern) => pattern.test(String(response.body || '')));
155
+ if (leakPatternHit) {
156
+ reasons.push(`body_leak_pattern:${String(leakPatternHit)}`);
157
+ }
158
+
159
+ return {
160
+ id: check.id,
161
+ name: check.name,
162
+ path: check.path,
163
+ status: reasons.length > 0 ? FAIL : PASS,
164
+ reasons,
165
+ evidence: {
166
+ url: response.url,
167
+ status: response.status,
168
+ headers: response.headers,
169
+ body_preview: String(response.body || '').slice(0, 240),
170
+ },
171
+ };
172
+ };
173
+
174
+ const writeReport = async (payload) => {
175
+ const absolutePath = path.resolve(process.cwd(), reportPath);
176
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
177
+ await fs.writeFile(absolutePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
178
+ return absolutePath;
179
+ };
180
+
181
+ const main = async () => {
182
+ const startedAt = __timeNowIso();
183
+ const results = [];
184
+
185
+ for (const check of checks) {
186
+ const result = await runCheck(check);
187
+ results.push(result);
188
+ console.log(`[security-web-surface] ${String(result.id).padStart(2, '0')} ${result.name}: ${result.status}`);
189
+ }
190
+
191
+ const summary = summarize(results);
192
+ const endedAt = __timeNowIso();
193
+ const report = {
194
+ meta: {
195
+ base_origin: baseOrigin,
196
+ started_at: startedAt,
197
+ ended_at: endedAt,
198
+ timeout_ms: requestTimeoutMs,
199
+ },
200
+ summary,
201
+ results,
202
+ };
203
+
204
+ const reportAbsolutePath = await writeReport(report);
205
+ console.log('[security-web-surface] ---');
206
+ console.log(`[security-web-surface] base_origin=${baseOrigin}`);
207
+ console.log(`[security-web-surface] summary=${JSON.stringify(summary)}`);
208
+ console.log(`[security-web-surface] report_path=${reportAbsolutePath}`);
209
+
210
+ if ((summary.FAIL || 0) > 0) {
211
+ process.exitCode = 1;
212
+ }
213
+ };
214
+
215
+ main().catch((error) => {
216
+ console.error(`[security-web-surface] fatal_error=${error?.message || error}`);
217
+ process.exit(1);
218
+ });
@@ -1,7 +1,8 @@
1
1
  import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
2
  import { randomUUID, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
3
- import { URLSearchParams } from 'node:url';
3
+ import { URL, URLSearchParams } from 'node:url';
4
4
 
5
+ import { setAdminOverviewSnapshot } from '../../../app/observability/metrics.js';
5
6
  import { parseAdminModeratorUpsertPayload, parseAdminSessionPasswordPayload } from '../../auth/validation/authSchemas.js';
6
7
 
7
8
  export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger, sendJson, readJsonBody, parseCookies, getCookieValuesFromRequest, appendSetCookie, buildCookieString, sanitizeText, normalizeGoogleSubject, normalizeEmail, normalizeJid, toIsoOrNull, toWhatsAppPhoneDigits, mapGoogleSessionResponseData, resolveGoogleWebSessionFromRequest, revokeGoogleWebSessionsByIdentity, getMarketplaceGlobalStatsCached, getSystemSummaryCached, getFeatureFlagsSnapshot, refreshFeatureFlags, listAdminBans, createAdminBanRecord, revokeAdminBanRecord, normalizeVisitPath, stickerWebPath, findStickerPackByPackKey, stickerPackService, buildManagedPackResponseData, sendManagedMutationStatus, sendManagedPackMutationStatus, deleteManagedPackWithCleanup, mapStickerPackWebManageError, cleanupOrphanStickerAssets, invalidateStickerCatalogDerivedCaches }) => {
@@ -16,6 +17,229 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
16
17
  const ADMIN_PANEL_SESSION_TTL_MS = Math.max(10 * 60 * 1000, Number(process.env.ADM_PANEL_SESSION_TTL_MS) || 12 * 60 * 60 * 1000);
17
18
  const ADMIN_MODERATOR_PASSWORD_MIN_LENGTH = Math.max(6, Number(process.env.ADM_MODERATOR_PASSWORD_MIN_LENGTH) || 8);
18
19
  const ADMIN_PANEL_SESSION_COOKIE_NAME = 'omnizap_admin_panel_session';
20
+ const SYSTEM_ADMIN_GRAFANA_TIME_FROM = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_TIME_FROM || 'now-6h', 40, { allowEmpty: true }) || 'now-6h';
21
+ const SYSTEM_ADMIN_GRAFANA_TIME_TO = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_TIME_TO || 'now', 40, { allowEmpty: true }) || 'now';
22
+ const SYSTEM_ADMIN_GRAFANA_REFRESH = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_REFRESH || '10s', 20, { allowEmpty: true }) || '10s';
23
+ const SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS = Math.max(800, Number(process.env.SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS) || 2500);
24
+ const SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS = Math.max(5000, Number(process.env.SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS) || 15000);
25
+
26
+ const normalizeGrafanaBaseUrl = (value) => {
27
+ const raw = String(value || '').trim();
28
+ if (!raw) return '';
29
+ try {
30
+ const parsed = new URL(raw);
31
+ if (!['http:', 'https:'].includes(parsed.protocol)) return '';
32
+ const normalizedPathname = String(parsed.pathname || '/').replace(/\/+$/, '');
33
+ parsed.search = '';
34
+ parsed.hash = '';
35
+ return `${parsed.origin}${normalizedPathname === '/' ? '' : normalizedPathname}`;
36
+ } catch {
37
+ return '';
38
+ }
39
+ };
40
+
41
+ const normalizeGrafanaUid = (value) =>
42
+ String(value || '')
43
+ .trim()
44
+ .replace(/[^a-zA-Z0-9_-]/g, '')
45
+ .slice(0, 120);
46
+
47
+ const slugifyGrafanaDashboard = (value) => {
48
+ const raw = String(value || '')
49
+ .trim()
50
+ .toLowerCase();
51
+ const slug = raw
52
+ .replace(/[^a-z0-9]+/g, '-')
53
+ .replace(/^-+|-+$/g, '')
54
+ .slice(0, 90);
55
+ return slug || 'dashboard';
56
+ };
57
+
58
+ const parseGrafanaDashboardDefinitions = () => {
59
+ const raw = String(process.env.SYSTEM_ADMIN_GRAFANA_DASHBOARDS || '')
60
+ .split(',')
61
+ .map((item) => String(item || '').trim())
62
+ .filter(Boolean);
63
+
64
+ const parsed = raw
65
+ .map((entry) => {
66
+ const [uidPart, ...titleParts] = entry.split('|');
67
+ const uid = normalizeGrafanaUid(uidPart);
68
+ const title = sanitizeText(titleParts.join('|') || '', 90, { allowEmpty: true }) || '';
69
+ return uid ? { uid, title } : null;
70
+ })
71
+ .filter(Boolean);
72
+
73
+ if (parsed.length) return parsed;
74
+
75
+ return [
76
+ { uid: 'omnizap-system-admin', title: 'System Admin' },
77
+ { uid: 'omnizap-overview', title: 'Overview' },
78
+ { uid: 'omnizap-mysql', title: 'MySQL' },
79
+ ];
80
+ };
81
+
82
+ const buildAdminGrafanaObservabilityLinks = () => {
83
+ const baseUrl = normalizeGrafanaBaseUrl(process.env.SYSTEM_ADMIN_GRAFANA_URL || process.env.GRAFANA_PUBLIC_URL || '');
84
+ if (!baseUrl) {
85
+ return {
86
+ enabled: false,
87
+ base_url: null,
88
+ dashboards: [],
89
+ };
90
+ }
91
+
92
+ const base = new URL(`${baseUrl}/`);
93
+ const definitions = parseGrafanaDashboardDefinitions();
94
+
95
+ const dashboards = definitions
96
+ .map((entry, index) => {
97
+ const uid = normalizeGrafanaUid(entry?.uid || '');
98
+ if (!uid) return null;
99
+ const title = sanitizeText(entry?.title || '', 90, { allowEmpty: true }) || `Dashboard ${index + 1}`;
100
+ const slug = slugifyGrafanaDashboard(title || uid);
101
+ const viewUrl = new URL(`d/${encodeURIComponent(uid)}/${encodeURIComponent(slug)}`, base);
102
+ viewUrl.searchParams.set('orgId', '1');
103
+ viewUrl.searchParams.set('from', SYSTEM_ADMIN_GRAFANA_TIME_FROM);
104
+ viewUrl.searchParams.set('to', SYSTEM_ADMIN_GRAFANA_TIME_TO);
105
+ viewUrl.searchParams.set('refresh', SYSTEM_ADMIN_GRAFANA_REFRESH);
106
+
107
+ const embedUrl = new URL(String(viewUrl));
108
+ embedUrl.searchParams.set('kiosk', 'tv');
109
+
110
+ return {
111
+ uid,
112
+ title,
113
+ view_url: viewUrl.toString(),
114
+ embed_url: embedUrl.toString(),
115
+ };
116
+ })
117
+ .filter(Boolean);
118
+
119
+ return {
120
+ enabled: dashboards.length > 0,
121
+ base_url: baseUrl,
122
+ dashboards,
123
+ };
124
+ };
125
+ const grafanaObservabilityLinks = buildAdminGrafanaObservabilityLinks();
126
+ let grafanaRuntimeSnapshotCache = {
127
+ expires_at_ms: 0,
128
+ payload: null,
129
+ };
130
+
131
+ const resolveGrafanaHealthEndpointUrl = () => {
132
+ const candidates = [process.env.GRAFANA_PROXY_TARGET_URL, process.env.GRAFANA_INTERNAL_URL, process.env.SYSTEM_ADMIN_GRAFANA_URL, process.env.GRAFANA_PUBLIC_URL];
133
+ for (const candidate of candidates) {
134
+ const baseUrl = normalizeGrafanaBaseUrl(candidate);
135
+ if (!baseUrl) continue;
136
+ try {
137
+ return new URL('api/health', `${baseUrl}/`).toString();
138
+ } catch {
139
+ // noop
140
+ }
141
+ }
142
+ return '';
143
+ };
144
+
145
+ const getGrafanaRuntimeSnapshot = async () => {
146
+ const nowMs = __timeNowMs();
147
+ if (grafanaRuntimeSnapshotCache.payload && nowMs < grafanaRuntimeSnapshotCache.expires_at_ms) {
148
+ return grafanaRuntimeSnapshotCache.payload;
149
+ }
150
+
151
+ const dashboardsTotal = Array.isArray(grafanaObservabilityLinks?.dashboards) ? grafanaObservabilityLinks.dashboards.length : 0;
152
+ const baseSnapshot = {
153
+ enabled: Boolean(grafanaObservabilityLinks?.enabled),
154
+ available: false,
155
+ status: 'unavailable',
156
+ response_ms: null,
157
+ checked_at: __timeNowIso(),
158
+ http_status: null,
159
+ version: null,
160
+ database: null,
161
+ commit: null,
162
+ dashboards_total: dashboardsTotal,
163
+ error: null,
164
+ };
165
+
166
+ const healthEndpointUrl = resolveGrafanaHealthEndpointUrl();
167
+ if (!healthEndpointUrl) {
168
+ grafanaRuntimeSnapshotCache = {
169
+ expires_at_ms: nowMs + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
170
+ payload: baseSnapshot,
171
+ };
172
+ return baseSnapshot;
173
+ }
174
+
175
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
176
+ const timeoutId =
177
+ controller && Number.isFinite(SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS)
178
+ ? setTimeout(() => {
179
+ controller.abort();
180
+ }, SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS)
181
+ : null;
182
+ const startedAtMs = __timeNowMs();
183
+
184
+ try {
185
+ const response = await globalThis.fetch(healthEndpointUrl, {
186
+ method: 'GET',
187
+ headers: {
188
+ Accept: 'application/json',
189
+ },
190
+ signal: controller?.signal,
191
+ });
192
+ const responseMs = Math.max(0, __timeNowMs() - startedAtMs);
193
+ let payload = null;
194
+ try {
195
+ payload = await response.json();
196
+ } catch {
197
+ payload = null;
198
+ }
199
+
200
+ const version = sanitizeText(payload?.version || '', 40, { allowEmpty: true }) || null;
201
+ const commit = sanitizeText(payload?.commit || '', 80, { allowEmpty: true }) || null;
202
+ const database = sanitizeText(payload?.database || '', 30, { allowEmpty: true }) || null;
203
+ const normalizedDatabase = String(database || '')
204
+ .trim()
205
+ .toLowerCase();
206
+ const status = !response.ok ? 'offline' : normalizedDatabase === 'ok' ? 'online' : normalizedDatabase ? 'degraded' : 'online';
207
+
208
+ const snapshot = {
209
+ ...baseSnapshot,
210
+ available: response.ok,
211
+ status,
212
+ response_ms: responseMs,
213
+ checked_at: __timeNowIso(),
214
+ http_status: Number(response.status || 0) || null,
215
+ version,
216
+ database,
217
+ commit,
218
+ };
219
+ grafanaRuntimeSnapshotCache = {
220
+ expires_at_ms: __timeNowMs() + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
221
+ payload: snapshot,
222
+ };
223
+ return snapshot;
224
+ } catch (error) {
225
+ const responseMs = Math.max(0, __timeNowMs() - startedAtMs);
226
+ const isAbort = error?.name === 'AbortError';
227
+ const snapshot = {
228
+ ...baseSnapshot,
229
+ status: 'offline',
230
+ response_ms: responseMs,
231
+ checked_at: __timeNowIso(),
232
+ error: sanitizeText(isAbort ? 'timeout' : error?.message || 'fetch_failed', 80, { allowEmpty: true }) || 'fetch_failed',
233
+ };
234
+ grafanaRuntimeSnapshotCache = {
235
+ expires_at_ms: __timeNowMs() + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
236
+ payload: snapshot,
237
+ };
238
+ return snapshot;
239
+ } finally {
240
+ if (timeoutId) clearTimeout(timeoutId);
241
+ }
242
+ };
19
243
 
20
244
  const adminPanelSessionMap = new Map();
21
245
  let adminPanelSessionPruneAt = 0;
@@ -957,7 +1181,7 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
957
1181
  };
958
1182
 
959
1183
  const buildAdminOverviewPayload = async ({ adminSession = null } = {}) => {
960
- const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary, systemSummaryPayload, messageFlowDaily, moderationQueue, auditLog, featureFlags] = await Promise.all([
1184
+ const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary, systemSummaryPayload, messageFlowDaily, moderationQueue, auditLog, featureFlags, grafanaRuntimeSnapshot] = await Promise.all([
961
1185
  getMarketplaceGlobalStatsCached().catch(() => null),
962
1186
  listAdminActiveGoogleWebSessions({ limit: 80 }),
963
1187
  listAdminKnownGoogleUsers({ limit: 120 }),
@@ -976,6 +1200,19 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
976
1200
  buildModerationQueueSnapshot({ limit: 80 }).catch(() => []),
977
1201
  listAdminAuditLog({ limit: 120 }).catch(() => []),
978
1202
  listAdminFeatureFlagsDetailed({ limit: 300 }).catch(() => []),
1203
+ getGrafanaRuntimeSnapshot().catch(() => ({
1204
+ enabled: Boolean(grafanaObservabilityLinks?.enabled),
1205
+ available: false,
1206
+ status: 'offline',
1207
+ response_ms: null,
1208
+ checked_at: __timeNowIso(),
1209
+ http_status: null,
1210
+ version: null,
1211
+ database: null,
1212
+ commit: null,
1213
+ dashboards_total: Array.isArray(grafanaObservabilityLinks?.dashboards) ? grafanaObservabilityLinks.dashboards.length : 0,
1214
+ error: 'runtime_snapshot_failed',
1215
+ })),
979
1216
  ]);
980
1217
 
981
1218
  const systemSummary = systemSummaryPayload?.data || null;
@@ -997,7 +1234,7 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
997
1234
  systemMeta,
998
1235
  });
999
1236
 
1000
- return {
1237
+ const overviewPayload = {
1001
1238
  admin_session: mapAdminPanelSessionResponseData(adminSession),
1002
1239
  marketplace_stats: marketplaceStats,
1003
1240
  counters: {
@@ -1045,9 +1282,22 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
1045
1282
  visit_metrics: visitSummary,
1046
1283
  system_summary: systemSummary,
1047
1284
  system_meta: systemMeta,
1285
+ observability_links: {
1286
+ grafana: grafanaObservabilityLinks,
1287
+ },
1288
+ observability_runtime: {
1289
+ grafana: grafanaRuntimeSnapshot,
1290
+ },
1048
1291
  message_flow_daily: messageFlowDaily,
1049
1292
  updated_at: __timeNowIso(),
1050
1293
  };
1294
+
1295
+ setAdminOverviewSnapshot({
1296
+ overview: overviewPayload,
1297
+ source: 'admin_overview_payload',
1298
+ });
1299
+
1300
+ return overviewPayload;
1051
1301
  };
1052
1302
 
1053
1303
  const findAdminPackContextByKey = async (rawPackKey) => {