@intranefr/superbackend 1.4.4 → 1.5.1

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