@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.
- package/.env.example +15 -0
- package/README.md +11 -0
- package/analysis-only.skill +0 -0
- package/index.js +23 -0
- package/package.json +8 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +90 -6
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminHeadless.controller.js +9 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +126 -4
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +810 -48
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminHeadless.routes.js +2 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +185 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +738 -0
- package/src/services/consoleOverride.service.js +7 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/jsonConfigs.service.js +2 -2
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +215 -15
- package/src/services/uiComponentsAi.service.js +6 -19
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +33 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-dashboard.ejs +28 -8
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +163 -1
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +597 -3
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +12 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- 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
|
+
};
|