@intranefr/superbackend 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +185 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +738 -0
  120. package/src/services/consoleOverride.service.js +7 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -0,0 +1,738 @@
1
+ const crypto = require("crypto");
2
+
3
+ const ConsoleEntry = require("../models/ConsoleEntry");
4
+ const ConsoleLog = require("../models/ConsoleLog");
5
+ const cacheLayer = require("./cacheLayer.service");
6
+ const {
7
+ getJsonConfig,
8
+ createJsonConfig,
9
+ updateJsonConfig,
10
+ } = require("./jsonConfigs.service");
11
+ const JsonConfig = require("../models/JsonConfig");
12
+ const { logErrorSync } = require("./errorLogger");
13
+ const CronJob = require("../models/CronJob");
14
+ const ScriptDefinition = require("../models/ScriptDefinition");
15
+
16
+ // Import consoleOverride to access the truly original console
17
+ const consoleOverride = require("./consoleOverride.service");
18
+
19
+ // Simplified module prefix tracking
20
+ let currentModulePrefix = '';
21
+
22
+ // Module prefix mapping based on module name
23
+ const MODULE_PREFIXES = {
24
+ 'cronScheduler.service.js': '[SuperBackend][Cron]',
25
+ 'healthChecksScheduler.service.js': '[SuperBackend][Health]',
26
+ 'consoleManager.service.js': '[SuperBackend][Console]',
27
+ 'middleware': '[SuperBackend][Core]',
28
+ 'middleware.js': '[SuperBackend][Core]'
29
+ };
30
+
31
+ let isActive = false;
32
+ let previousConsole = null;
33
+ let isHandling = false;
34
+
35
+ const DEFAULT_ALIAS = "console-manager";
36
+
37
+ const METHODS = ["debug", "log", "info", "warn", "error"];
38
+
39
+ function clamp(str, maxLen) {
40
+ const s = String(str ?? "");
41
+ if (s.length <= maxLen) return s;
42
+ return s.slice(0, maxLen);
43
+ }
44
+
45
+ function normalizeMessage(message) {
46
+ if (!message) return "";
47
+ return String(message)
48
+ .replace(
49
+ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
50
+ "<UUID>",
51
+ )
52
+ .replace(/[0-9a-f]{24}/gi, "<OBJECTID>")
53
+ .replace(/\b\d{4,}\b/g, "<NUM>")
54
+ .replace(/\s+/g, " ")
55
+ .trim()
56
+ .slice(0, 500);
57
+ }
58
+
59
+ // Simplified approach - no stack trace parsing needed
60
+ function getModulePrefix() {
61
+ return currentModulePrefix;
62
+ }
63
+
64
+ function computeHash({ method, messageTemplate, topFrame }) {
65
+ const parts = [method || "", messageTemplate || "", topFrame || ""];
66
+ const hash = crypto
67
+ .createHash("sha256")
68
+ .update(parts.join("|"))
69
+ .digest("hex");
70
+ return hash.slice(0, 32);
71
+ }
72
+
73
+ function safeArgsPreview(
74
+ args,
75
+ { maxArgChars = 2000, maxArgsSerialized = 5 } = {},
76
+ ) {
77
+ const list = Array.isArray(args)
78
+ ? args.slice(0, Math.max(0, Number(maxArgsSerialized || 0) || 0))
79
+ : [];
80
+ const out = [];
81
+
82
+ for (const a of list) {
83
+ if (typeof a === "string") {
84
+ out.push(clamp(a, maxArgChars));
85
+ continue;
86
+ }
87
+
88
+ if (a instanceof Error) {
89
+ out.push(clamp(a.stack || a.message || a.name || "Error", maxArgChars));
90
+ continue;
91
+ }
92
+
93
+ try {
94
+ out.push(clamp(JSON.stringify(a), maxArgChars));
95
+ } catch {
96
+ out.push(clamp(String(a), maxArgChars));
97
+ }
98
+ }
99
+
100
+ return out.join(" ");
101
+ }
102
+
103
+ async function ensureJsonConfigExists() {
104
+ const existing = await JsonConfig.findOne({
105
+ $or: [{ slug: DEFAULT_ALIAS }, { alias: DEFAULT_ALIAS }],
106
+ })
107
+ .select("_id")
108
+ .lean();
109
+ if (existing) return;
110
+
111
+ const initial = {
112
+ defaultEntryEnabled: true,
113
+ defaults: {
114
+ persist: {
115
+ cache: false,
116
+ db: false,
117
+ warnErrorToCacheDb: false,
118
+ },
119
+ },
120
+ db: {
121
+ enabled: false,
122
+ ttlDays: 7,
123
+ sampleRatePercent: 100,
124
+ },
125
+ cache: {
126
+ enabled: false,
127
+ ttlSeconds: 3600,
128
+ namespace: "console-manager",
129
+ },
130
+ performance: {
131
+ maxArgChars: 2000,
132
+ maxArgsSerialized: 5,
133
+ },
134
+ };
135
+
136
+ await createJsonConfig({
137
+ title: "Console Manager",
138
+ alias: DEFAULT_ALIAS,
139
+ jsonRaw: JSON.stringify(initial, null, 2),
140
+ publicEnabled: false,
141
+ cacheTtlSeconds: 2,
142
+ });
143
+ }
144
+
145
+ async function getConfigSafe() {
146
+ try {
147
+ await ensureJsonConfigExists();
148
+ } catch {
149
+ // ignore
150
+ }
151
+
152
+ try {
153
+ const cfg = await getJsonConfig(DEFAULT_ALIAS, { bypassCache: false });
154
+ return cfg && typeof cfg === "object" ? cfg : null;
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ function shouldSample(percent) {
161
+ const p = Number(percent);
162
+ if (!Number.isFinite(p)) return true;
163
+ if (p >= 100) return true;
164
+ if (p <= 0) return false;
165
+ return Math.random() * 100 <= p;
166
+ }
167
+
168
+ function computeExpiresAt(ttlDays) {
169
+ const days = Number(ttlDays);
170
+ if (!Number.isFinite(days) || days <= 0) return null;
171
+ return new Date(Date.now() + days * 24 * 60 * 60 * 1000);
172
+ }
173
+
174
+ let queue = [];
175
+ let isDraining = false;
176
+ const MAX_QUEUE = 5000;
177
+
178
+ function enqueue(task) {
179
+ if (queue.length >= MAX_QUEUE) {
180
+ queue.shift();
181
+ }
182
+ queue.push(task);
183
+ drainAsync();
184
+ }
185
+
186
+ function drainAsync() {
187
+ if (isDraining) return;
188
+ isDraining = true;
189
+
190
+ setImmediate(async () => {
191
+ try {
192
+ for (let i = 0; i < 200; i += 1) {
193
+ const task = queue.shift();
194
+ if (!task) break;
195
+ // eslint-disable-next-line no-await-in-loop
196
+ await task();
197
+ }
198
+ } catch {
199
+ // ignore
200
+ } finally {
201
+ isDraining = false;
202
+ if (queue.length > 0) {
203
+ drainAsync();
204
+ }
205
+ }
206
+ });
207
+ }
208
+
209
+ async function upsertEntry({
210
+ hash,
211
+ method,
212
+ messageTemplate,
213
+ topFrame,
214
+ cfg,
215
+ args,
216
+ }) {
217
+ const defaultEnabled = cfg?.defaultEntryEnabled !== false;
218
+
219
+ const shouldDefaultPersistWarnError = Boolean(
220
+ cfg?.defaults?.persist?.warnErrorToCacheDb,
221
+ );
222
+ const defaultPersistCache = Boolean(cfg?.defaults?.persist?.cache);
223
+ const defaultPersistDb = Boolean(cfg?.defaults?.persist?.db);
224
+
225
+ const autoPersist =
226
+ shouldDefaultPersistWarnError && (method === "warn" || method === "error");
227
+
228
+ const persistToCache = Boolean(defaultPersistCache || autoPersist);
229
+ const persistToDb = Boolean(defaultPersistDb || autoPersist);
230
+
231
+ const perf = cfg?.performance || {};
232
+
233
+ const lastSample = {
234
+ argsPreview: safeArgsPreview(args, perf),
235
+ };
236
+
237
+ const now = new Date();
238
+
239
+ const doc = await ConsoleEntry.findOneAndUpdate(
240
+ { hash },
241
+ {
242
+ $set: {
243
+ method,
244
+ messageTemplate,
245
+ topFrame,
246
+ lastSeenAt: now,
247
+ lastSample,
248
+ },
249
+ $inc: { countTotal: 1 },
250
+ $setOnInsert: {
251
+ enabled: defaultEnabled,
252
+ enabledExplicit: false,
253
+ persistToCache,
254
+ persistToDb,
255
+ persistExplicit: false,
256
+ tags: [],
257
+ firstSeenAt: now,
258
+ },
259
+ },
260
+ { upsert: true, new: true },
261
+ ).lean();
262
+
263
+ return doc;
264
+ }
265
+
266
+ function buildMessageFromArgs(args) {
267
+ for (const a of args) {
268
+ if (typeof a === "string" && a.trim()) return a;
269
+ if (a instanceof Error) return a.message || a.name || "Error";
270
+ }
271
+ try {
272
+ return args
273
+ .map((a) => (typeof a === "string" ? a : JSON.stringify(a)))
274
+ .join(" ");
275
+ } catch {
276
+ return args.map((a) => String(a)).join(" ");
277
+ }
278
+ }
279
+
280
+ function captureErrorAggregationFromArgs(args) {
281
+ try {
282
+ let errorObj = null;
283
+ for (const a of args) {
284
+ if (a instanceof Error) {
285
+ errorObj = a;
286
+ break;
287
+ }
288
+ }
289
+
290
+ const message = errorObj ? errorObj.message : buildMessageFromArgs(args);
291
+
292
+ logErrorSync({
293
+ source: "backend",
294
+ severity: "error",
295
+ errorName: errorObj?.name || "ConsoleError",
296
+ message,
297
+ stack: errorObj?.stack,
298
+ extra: {
299
+ consoleArgs: Array.isArray(args) ? args.length : 0,
300
+ consoleManager: true,
301
+ },
302
+ });
303
+ } catch {
304
+ // ignore
305
+ }
306
+ }
307
+
308
+ async function persistOccurrence({ cfg, entry, method, args }) {
309
+ const cacheCfg = cfg?.cache || {};
310
+ const dbCfg = cfg?.db || {};
311
+
312
+ const persistCache =
313
+ Boolean(cacheCfg.enabled) && Boolean(entry?.persistToCache);
314
+ const persistDb = Boolean(dbCfg.enabled) && Boolean(entry?.persistToDb);
315
+
316
+ if (!persistCache && !persistDb) return;
317
+
318
+ const perf = cfg?.performance || {};
319
+ const argsPreview = safeArgsPreview(args, perf);
320
+
321
+ if (persistCache) {
322
+ const namespace = String(cacheCfg.namespace || "console-manager");
323
+ const ttlSeconds = Number(cacheCfg.ttlSeconds || 0) || 3600;
324
+
325
+ enqueue(async () => {
326
+ try {
327
+ const countKey = `entry:${entry.hash}:count`;
328
+ const lastKey = `entry:${entry.hash}:last`;
329
+
330
+ const existingCount = await cacheLayer.get(countKey, { namespace });
331
+ const nextCount = (Number(existingCount || 0) || 0) + 1;
332
+
333
+ await cacheLayer.set(countKey, nextCount, { namespace, ttlSeconds });
334
+ await cacheLayer.set(lastKey, new Date().toISOString(), {
335
+ namespace,
336
+ ttlSeconds,
337
+ });
338
+ } catch {
339
+ // ignore
340
+ }
341
+ });
342
+ }
343
+
344
+ if (persistDb) {
345
+ if (!shouldSample(dbCfg.sampleRatePercent)) return;
346
+
347
+ const expiresAt = computeExpiresAt(dbCfg.ttlDays || 7);
348
+
349
+ const message = clamp(buildMessageFromArgs(args), 2000);
350
+
351
+ enqueue(async () => {
352
+ try {
353
+ await ConsoleLog.create({
354
+ entryHash: entry.hash,
355
+ method,
356
+ message,
357
+ argsPreview: clamp(argsPreview, 5000),
358
+ tagsSnapshot: Array.isArray(entry.tags) ? entry.tags : [],
359
+ requestId: "",
360
+ createdAt: new Date(),
361
+ expiresAt,
362
+ });
363
+ } catch {
364
+ // ignore
365
+ }
366
+ });
367
+ }
368
+ }
369
+
370
+ let memoryEntries = new Map();
371
+ let configFromMemory = null;
372
+
373
+ /**
374
+ * It needs to be sync
375
+ *
376
+ * persist entry + db persistance is async, we do not wait for it
377
+ * @param {*} method
378
+ * @param {*} args
379
+ * @param {*} stack
380
+ * @returns
381
+ */
382
+ function handleConsoleCall(method, args, stack) {
383
+ const message = buildMessageFromArgs(args);
384
+ const messageTemplate = normalizeMessage(message);
385
+ // Use empty string for topFrame since we're not using stack trace parsing
386
+ const topFrame = "";
387
+ const hash = computeHash({ method, messageTemplate, topFrame });
388
+
389
+ // Get the current module prefix (simplified approach)
390
+ const prefix = getModulePrefix();
391
+
392
+ // Add prefix to args if prefix exists
393
+ let prefixedArgs = args;
394
+ if (prefix) {
395
+ prefixedArgs = [prefix, ...args];
396
+ }
397
+
398
+ let entryFromMemory = memoryEntries.get(hash);
399
+
400
+ asyncUpdate();
401
+
402
+ if (!configFromMemory && !entryFromMemory) {
403
+ // First pass - always log and wait for async update to complete
404
+ previousConsole[method](...prefixedArgs);
405
+ return;
406
+ }
407
+
408
+ // Check if this specific entry is enabled
409
+ const isEnabled = entryFromMemory
410
+ ? entryFromMemory.enabled !== false
411
+ : configFromMemory?.defaultEntryEnabled !== false;
412
+
413
+ if (isEnabled) {
414
+ previousConsole[method](...prefixedArgs);
415
+ } else {
416
+ // Entry is disabled - suppress stdout but still capture error aggregation for errors
417
+ if (method === "error") {
418
+ captureErrorAggregationFromArgs(args);
419
+ }
420
+ }
421
+
422
+ async function asyncUpdate() {
423
+ const cfg = await getConfigSafe();
424
+ configFromMemory = cfg;
425
+ let entry;
426
+ let error = null;
427
+ try {
428
+ entry = await upsertEntry({
429
+ hash,
430
+ method,
431
+ messageTemplate,
432
+ topFrame,
433
+ cfg,
434
+ args,
435
+ });
436
+ } catch (e) {
437
+ error = e;
438
+ entry = null;
439
+ }
440
+ if (entry) {
441
+ try {
442
+ await persistOccurrence({ cfg, entry, method, args });
443
+ } catch {
444
+ // ignore
445
+ }
446
+ }
447
+ memoryEntries.set(hash, entry);
448
+ if (error) {
449
+ previousConsole.error("Failed to upsert console entry:", error);
450
+ }
451
+ }
452
+ }
453
+
454
+ async function waitForDbReady({ maxWaitMs = 15000 } = {}) {
455
+ const start = Date.now();
456
+ // eslint-disable-next-line no-constant-condition
457
+ while (true) {
458
+ if (require("mongoose").connection.readyState === 1) return true;
459
+ if (Date.now() - start > maxWaitMs) return false;
460
+ // eslint-disable-next-line no-await-in-loop
461
+ await new Promise((r) => setTimeout(r, 250));
462
+ }
463
+ }
464
+
465
+ async function ensureRetentionCron() {
466
+ const ok = await waitForDbReady({ maxWaitMs: 15000 });
467
+ if (!ok) return;
468
+
469
+ await ensureJsonConfigExists();
470
+
471
+ const scriptCodeIdentifier = "console-manager-retention";
472
+ let script = await ScriptDefinition.findOne({
473
+ codeIdentifier: scriptCodeIdentifier,
474
+ });
475
+ if (!script) {
476
+ script = await ScriptDefinition.create({
477
+ name: "Console Manager Retention",
478
+ codeIdentifier: scriptCodeIdentifier,
479
+ description:
480
+ "Deletes ConsoleLog records older than configured retention (best-effort).",
481
+ type: "node",
482
+ runner: "host",
483
+ script: `const mongoose = require('mongoose');
484
+
485
+ (async () => {
486
+ const uri = process.env.MONGODB_URI || process.env.MONGO_URI;
487
+ if (!uri) {
488
+ throw new Error('Missing MONGODB_URI/MONGO_URI');
489
+ }
490
+
491
+ await mongoose.connect(uri, { serverSelectionTimeoutMS: 5000, maxPoolSize: 2 });
492
+
493
+ const jsonConfigSchema = new mongoose.Schema(
494
+ {
495
+ title: { type: String, required: true },
496
+ slug: { type: String, required: true, unique: true, index: true },
497
+ alias: { type: String, unique: true, sparse: true, index: true },
498
+ publicEnabled: { type: Boolean, default: false },
499
+ cacheTtlSeconds: { type: Number, default: 0 },
500
+ jsonRaw: { type: String, required: true },
501
+ jsonHash: { type: String, default: null },
502
+ },
503
+ { timestamps: true, collection: 'jsonconfigs' },
504
+ );
505
+ const JsonConfig = mongoose.models.JsonConfig || mongoose.model('JsonConfig', jsonConfigSchema);
506
+
507
+ let ttlDays = 7;
508
+ const cfgDoc = await JsonConfig.findOne({ $or: [{ slug: 'console-manager' }, { alias: 'console-manager' }] }).lean();
509
+ if (cfgDoc && cfgDoc.jsonRaw) {
510
+ try {
511
+ const cfg = JSON.parse(String(cfgDoc.jsonRaw));
512
+ const n = Number(cfg?.db?.ttlDays);
513
+ if (Number.isFinite(n) && n > 0) ttlDays = n;
514
+ } catch {
515
+ // ignore
516
+ }
517
+ }
518
+
519
+ const consoleLogSchema = new mongoose.Schema(
520
+ {
521
+ entryHash: { type: String, required: true, index: true },
522
+ method: { type: String, enum: ['debug', 'log', 'info', 'warn', 'error'], required: true, index: true },
523
+ message: { type: String, default: '', maxlength: 2000 },
524
+ argsPreview: { type: String, default: '', maxlength: 5000 },
525
+ tagsSnapshot: { type: [String], default: [], index: true },
526
+ requestId: { type: String, default: '', index: true },
527
+ createdAt: { type: Date, default: Date.now, index: true },
528
+ expiresAt: { type: Date, default: null, index: true },
529
+ },
530
+ { timestamps: false, collection: 'console_logs' },
531
+ );
532
+ consoleLogSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
533
+ consoleLogSchema.index({ entryHash: 1, createdAt: -1 });
534
+ const ConsoleLog = mongoose.models.ConsoleLog || mongoose.model('ConsoleLog', consoleLogSchema);
535
+
536
+ const now = new Date();
537
+ const cutoff = new Date(now.getTime() - ttlDays * 24 * 60 * 60 * 1000);
538
+ await ConsoleLog.deleteMany({ $or: [{ expiresAt: { $lt: now } }, { createdAt: { $lt: cutoff } }] });
539
+
540
+ await mongoose.disconnect();
541
+ })().catch(async (err) => {
542
+ try { console.error('[ConsoleManagerRetention] Failed:', err?.message || err); } catch {}
543
+ try { await mongoose.disconnect(); } catch {}
544
+ process.exitCode = 1;
545
+ });
546
+ `,
547
+ enabled: true,
548
+ timeoutMs: 5 * 60 * 1000,
549
+ });
550
+ } else if (script.runner !== "host" || script.type !== "node") {
551
+ script.type = "node";
552
+ script.runner = "host";
553
+ await script.save();
554
+ }
555
+
556
+ const cronName = "Console Manager Retention";
557
+ let job = await CronJob.findOne({ name: cronName, taskType: "script" });
558
+ if (!job) {
559
+ job = await CronJob.create({
560
+ name: cronName,
561
+ description: "Daily cleanup for Console Manager logs (7 days).",
562
+ cronExpression: "0 3 * * *",
563
+ timezone: "UTC",
564
+ enabled: true,
565
+ nextRunAt: null,
566
+ taskType: "script",
567
+ scriptId: script._id,
568
+ scriptEnv: [],
569
+ timeoutMs: 5 * 60 * 1000,
570
+ createdBy: "system",
571
+ });
572
+ }
573
+
574
+ try {
575
+ const cronScheduler = require("./cronScheduler.service");
576
+ if (job.enabled) {
577
+ await cronScheduler.scheduleJob(job);
578
+ }
579
+ } catch {
580
+ // ignore
581
+ }
582
+ }
583
+
584
+ const consoleManager = {
585
+ getConsole:()=>console,
586
+
587
+ // New method to set module prefix
588
+ setModulePrefix(moduleName) {
589
+ // If moduleName is already a prefix, use it directly
590
+ if (moduleName.startsWith('[') && moduleName.endsWith(']')) {
591
+ currentModulePrefix = moduleName;
592
+ } else {
593
+ // Otherwise look it up in the mapping
594
+ currentModulePrefix = MODULE_PREFIXES[moduleName] || '';
595
+ }
596
+ },
597
+
598
+ // Get current module prefix
599
+ getModulePrefix() {
600
+ return currentModulePrefix;
601
+ },
602
+
603
+ init() {
604
+ if (isActive) return;
605
+ if (isHandling) return;
606
+
607
+ // Use the truly original console from consoleOverride if available
608
+ // Otherwise fall back to current console
609
+ previousConsole = consoleOverride.TRULY_ORIGINAL_CONSOLE || { ...console };
610
+
611
+ METHODS.forEach((method) => {
612
+ console[method] = (...args) => {
613
+ if (!previousConsole) {
614
+ // Fallback to original console if not initialized
615
+ return;
616
+ }
617
+
618
+ if (isHandling) {
619
+ // Prevent re-entrancy, just forward to previous console
620
+ previousConsole[method](...args);
621
+ return;
622
+ }
623
+
624
+ // Capture stack trace here to get the actual caller
625
+ const stack = new Error().stack;
626
+
627
+ isHandling = true;
628
+ try {
629
+ handleConsoleCall(method, args, stack);
630
+ } catch (e) {
631
+ // If anything fails, fallback to previous console
632
+ previousConsole.error("[Console Manager Error]", e);
633
+ previousConsole[method](...args);
634
+ } finally {
635
+ isHandling = false;
636
+ }
637
+ };
638
+ });
639
+
640
+ // Also override global.console to ensure all modules use the managed console
641
+ console.overrided=true
642
+ global.console = console;
643
+
644
+ isActive = true;
645
+
646
+ previousConsole.info("[Console Manager] Console override initialized");
647
+
648
+ setImmediate(() => {
649
+ ensureRetentionCron().catch(() => {});
650
+ });
651
+ },
652
+
653
+ restore() {
654
+ if (!isActive && !previousConsole) return;
655
+
656
+ if (previousConsole) {
657
+ METHODS.forEach((method) => {
658
+ console[method] = previousConsole[method];
659
+ });
660
+ global.console = previousConsole;
661
+ previousConsole = null;
662
+ }
663
+
664
+ isActive = false;
665
+ queue = [];
666
+ isDraining = false;
667
+ isHandling = false;
668
+ },
669
+
670
+ isActive() {
671
+ return isActive;
672
+ },
673
+
674
+ async getConfig() {
675
+ const cfg = await getConfigSafe();
676
+ return cfg;
677
+ },
678
+
679
+ async updateConfig(newCfg) {
680
+ await ensureJsonConfigExists();
681
+ const doc = await JsonConfig.findOne({
682
+ $or: [{ slug: DEFAULT_ALIAS }, { alias: DEFAULT_ALIAS }],
683
+ });
684
+ if (!doc) {
685
+ throw new Error("Console Manager config not found");
686
+ }
687
+
688
+ const updated = await updateJsonConfig(doc._id, {
689
+ jsonRaw: JSON.stringify(newCfg, null, 2),
690
+ title: "Console Manager",
691
+ alias: DEFAULT_ALIAS,
692
+ publicEnabled: false,
693
+ cacheTtlSeconds: 2,
694
+ });
695
+
696
+ return updated;
697
+ },
698
+
699
+ async applyDefaultsRetroactively(cfg) {
700
+ const defaultEnabled = cfg?.defaultEntryEnabled !== false;
701
+ const warnErrorToCacheDb = Boolean(
702
+ cfg?.defaults?.persist?.warnErrorToCacheDb,
703
+ );
704
+ const defaultPersistCache = Boolean(cfg?.defaults?.persist?.cache);
705
+ const defaultPersistDb = Boolean(cfg?.defaults?.persist?.db);
706
+
707
+ await ConsoleEntry.updateMany(
708
+ { enabledExplicit: false },
709
+ { $set: { enabled: defaultEnabled } },
710
+ );
711
+
712
+ await ConsoleEntry.updateMany(
713
+ { persistExplicit: false, method: { $in: ["warn", "error"] } },
714
+ {
715
+ $set: {
716
+ persistToCache: Boolean(defaultPersistCache || warnErrorToCacheDb),
717
+ persistToDb: Boolean(defaultPersistDb || warnErrorToCacheDb),
718
+ },
719
+ },
720
+ );
721
+
722
+ await ConsoleEntry.updateMany(
723
+ { persistExplicit: false, method: { $in: ["debug", "log", "info"] } },
724
+ {
725
+ $set: {
726
+ persistToCache: Boolean(defaultPersistCache),
727
+ persistToDb: Boolean(defaultPersistDb),
728
+ },
729
+ },
730
+ );
731
+ },
732
+ };
733
+
734
+ module.exports = {
735
+ consoleManager,
736
+ MODULE_PREFIXES,
737
+ handleConsoleCall
738
+ };