@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
@@ -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
@@ -289,3 +294,4 @@ process.on('SIGTERM', () => {
289
294
  });
290
295
 
291
296
  module.exports = consoleOverride;
297
+ module.exports.TRULY_ORIGINAL_CONSOLE = TRULY_ORIGINAL_CONSOLE;
@@ -0,0 +1,350 @@
1
+ const cron = require('node-cron');
2
+ const parser = require('cron-parser');
3
+ const { startRun } = require('./scriptsRunner.service');
4
+ const CronJob = require('../models/CronJob');
5
+ const CronExecution = require('../models/CronExecution');
6
+ const ScriptDefinition = require('../models/ScriptDefinition');
7
+ const ScriptRun = require('../models/ScriptRun');
8
+
9
+ const console = global.console
10
+
11
+ class CronScheduler {
12
+ constructor() {
13
+ this.scheduledJobs = new Map(); // Map<jobId, cron.ScheduledTask>
14
+ this.isRunning = false;
15
+ }
16
+
17
+ async start() {
18
+ if (this.isRunning) return;
19
+ this.isRunning = true;
20
+
21
+ // Load all enabled jobs from database
22
+ const enabledJobs = await CronJob.find({ enabled: true }).lean();
23
+
24
+ for (const job of enabledJobs) {
25
+ try {
26
+ await this.scheduleJob(job);
27
+ } catch (err) {
28
+ console.error(`Failed to schedule cron job ${job.name}:`, err);
29
+ }
30
+ }
31
+
32
+ console.log(`Cron scheduler started with ${enabledJobs.length} jobs`);
33
+ }
34
+
35
+ async stop() {
36
+ // Unschedule all jobs
37
+ for (const [jobId, task] of this.scheduledJobs) {
38
+ task.stop();
39
+ }
40
+ this.scheduledJobs.clear();
41
+ this.isRunning = false;
42
+ console.log('Cron scheduler stopped');
43
+ }
44
+
45
+ async scheduleJob(cronJob) {
46
+ const jobId = String(cronJob._id);
47
+
48
+ // Unschedule if already scheduled
49
+ if (this.scheduledJobs.has(jobId)) {
50
+ this.unscheduleJob(jobId);
51
+ }
52
+
53
+ // Validate cron expression
54
+ if (!cron.validate(cronJob.cronExpression)) {
55
+ throw new Error(`Invalid cron expression: ${cronJob.cronExpression}`);
56
+ }
57
+
58
+ // Create the scheduled task
59
+ const task = cron.schedule(cronJob.cronExpression, async () => {
60
+ await this.executeJob(cronJob);
61
+ }, {
62
+ scheduled: false, // Don't start immediately
63
+ timezone: cronJob.timezone || 'UTC',
64
+ });
65
+
66
+ // Start the task
67
+ task.start();
68
+ this.scheduledJobs.set(jobId, task);
69
+
70
+ // Update next run time
71
+ const nextRunAt = this.calculateNextRun(cronJob.cronExpression, cronJob.timezone);
72
+ await CronJob.updateOne(
73
+ { _id: jobId },
74
+ { $set: { nextRunAt } }
75
+ );
76
+
77
+ console.log(`Scheduled cron job: ${cronJob.name} (${cronJob.cronExpression})`);
78
+ }
79
+
80
+ async unscheduleJob(jobId) {
81
+ const task = this.scheduledJobs.get(String(jobId));
82
+ if (task) {
83
+ task.stop();
84
+ this.scheduledJobs.delete(String(jobId));
85
+
86
+ // Clear next run time
87
+ await CronJob.updateOne(
88
+ { _id: jobId },
89
+ { $set: { nextRunAt: null } }
90
+ );
91
+
92
+ console.log(`Unscheduled cron job: ${jobId}`);
93
+ }
94
+ }
95
+
96
+ async executeJob(cronJob) {
97
+ const execution = await CronExecution.create({
98
+ cronJobId: cronJob._id,
99
+ status: 'running',
100
+ startedAt: new Date(),
101
+ triggeredAt: new Date(),
102
+ actualRunAt: new Date(),
103
+ });
104
+
105
+ console.log(`Executing cron job: ${cronJob.name} (execution: ${execution._id})`);
106
+
107
+ try {
108
+ if (cronJob.taskType === 'script') {
109
+ await this.executeScriptJob(cronJob, execution);
110
+ } else if (cronJob.taskType === 'http') {
111
+ await this.executeHttpJob(cronJob, execution);
112
+ } else {
113
+ throw new Error(`Unknown task type: ${cronJob.taskType}`);
114
+ }
115
+
116
+ // Update execution as succeeded
117
+ await CronExecution.updateOne(
118
+ { _id: execution._id },
119
+ {
120
+ $set: {
121
+ status: 'succeeded',
122
+ finishedAt: new Date(),
123
+ },
124
+ }
125
+ );
126
+ } catch (err) {
127
+ // Update execution as failed
128
+ await CronExecution.updateOne(
129
+ { _id: execution._id },
130
+ {
131
+ $set: {
132
+ status: 'failed',
133
+ finishedAt: new Date(),
134
+ error: err.message,
135
+ },
136
+ }
137
+ );
138
+ console.error(`Cron job failed: ${cronJob.name}`, err);
139
+ }
140
+
141
+ // Update next run time for the job
142
+ if (cronJob.enabled) {
143
+ const nextRunAt = this.calculateNextRun(cronJob.cronExpression, cronJob.timezone);
144
+ await CronJob.updateOne(
145
+ { _id: cronJob._id },
146
+ { $set: { nextRunAt } }
147
+ );
148
+ }
149
+
150
+ return execution;
151
+ }
152
+
153
+ async executeScriptJob(cronJob, execution) {
154
+ console.log(`[CronScheduler] Executing script job: ${cronJob.name} (scriptId: ${cronJob.scriptId})`);
155
+
156
+ // Get the script definition
157
+ const scriptDef = await ScriptDefinition.findById(cronJob.scriptId);
158
+ if (!scriptDef) {
159
+ throw new Error(`Script not found: ${cronJob.scriptId}`);
160
+ }
161
+
162
+ if (!scriptDef.enabled) {
163
+ throw new Error(`Script is disabled: ${scriptDef.name}`);
164
+ }
165
+
166
+ // Merge environment variables
167
+ const env = [...scriptDef.env];
168
+ if (cronJob.scriptEnv && cronJob.scriptEnv.length > 0) {
169
+ // Override with cron-specific env vars
170
+ for (const cronEnv of cronJob.scriptEnv) {
171
+ const existingIndex = env.findIndex(e => e.key === cronEnv.key);
172
+ if (existingIndex >= 0) {
173
+ env[existingIndex] = cronEnv;
174
+ } else {
175
+ env.push(cronEnv);
176
+ }
177
+ }
178
+ }
179
+
180
+ // Create a modified script definition for execution
181
+ const modifiedScript = {
182
+ ...scriptDef.toObject(),
183
+ env,
184
+ timeoutMs: cronJob.timeoutMs || scriptDef.timeoutMs,
185
+ };
186
+
187
+ console.log(`[CronScheduler] Starting script execution for: ${scriptDef.name}`);
188
+ // Start the script execution
189
+ const run = await startRun(modifiedScript, {
190
+ trigger: 'schedule',
191
+ meta: { cronJobId: cronJob._id, cronExecutionId: execution._id },
192
+ });
193
+
194
+ console.log(`[CronScheduler] Script run created with ID: ${run._id}`);
195
+ // Wait for completion and capture output
196
+ const output = await this.waitForScriptCompletion(run._id);
197
+
198
+ console.log(`[CronScheduler] Script execution completed for: ${scriptDef.name}`);
199
+ // Update execution with output
200
+ await CronExecution.updateOne(
201
+ { _id: execution._id },
202
+ { $set: { output } }
203
+ );
204
+ }
205
+
206
+ async executeHttpJob(cronJob, execution) {
207
+ const { httpMethod, httpUrl, httpHeaders, httpBody, httpBodyType, httpAuth } = cronJob;
208
+
209
+ // Prepare headers
210
+ const headers = {};
211
+ if (httpHeaders) {
212
+ for (const header of httpHeaders) {
213
+ headers[header.key] = header.value;
214
+ }
215
+ }
216
+
217
+ // Add authentication
218
+ if (httpAuth && httpAuth.type === 'bearer' && httpAuth.token) {
219
+ headers['Authorization'] = `Bearer ${httpAuth.token}`;
220
+ } else if (httpAuth && httpAuth.type === 'basic' && httpAuth.username && httpAuth.password) {
221
+ const encoded = Buffer.from(`${httpAuth.username}:${httpAuth.password}`).toString('base64');
222
+ headers['Authorization'] = `Basic ${encoded}`;
223
+ }
224
+
225
+ // Prepare body
226
+ let body = null;
227
+ if (httpBody && ['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
228
+ if (httpBodyType === 'json') {
229
+ headers['Content-Type'] = 'application/json';
230
+ body = httpBody;
231
+ } else if (httpBodyType === 'form') {
232
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
233
+ body = httpBody;
234
+ } else {
235
+ headers['Content-Type'] = 'text/plain';
236
+ body = httpBody;
237
+ }
238
+ }
239
+
240
+ // Make the HTTP request
241
+ const response = await fetch(httpUrl, {
242
+ method: httpMethod,
243
+ headers,
244
+ body,
245
+ timeout: cronJob.timeoutMs || 300000,
246
+ });
247
+
248
+ // Get response text
249
+ const output = await response.text();
250
+
251
+ // Update execution with HTTP response details
252
+ await CronExecution.updateOne(
253
+ { _id: execution._id },
254
+ {
255
+ $set: {
256
+ output,
257
+ httpStatusCode: response.status,
258
+ httpResponseHeaders: Object.fromEntries(response.headers.entries()),
259
+ },
260
+ }
261
+ );
262
+
263
+ // Throw error if response is not successful
264
+ if (!response.ok) {
265
+ throw new Error(`HTTP request failed: ${response.status} ${response.statusText}`);
266
+ }
267
+ }
268
+
269
+ async waitForScriptCompletion(runId, timeout = 300000) {
270
+ console.log(`[CronScheduler] Waiting for script completion: ${runId}`);
271
+ return new Promise((resolve, reject) => {
272
+ let output = '';
273
+ const startTime = Date.now();
274
+ const timeoutId = setTimeout(() => {
275
+ console.log(`[CronScheduler] Script execution timeout: ${runId}`);
276
+ reject(new Error('Script execution timeout'));
277
+ }, timeout);
278
+
279
+ // Check for completion periodically
280
+ const checkInterval = setInterval(async () => {
281
+ try {
282
+ const run = await ScriptRun.findById(runId);
283
+ if (!run) {
284
+ console.log(`[CronScheduler] Script run not found: ${runId}`);
285
+ clearInterval(checkInterval);
286
+ clearTimeout(timeoutId);
287
+ reject(new Error('Script run not found'));
288
+ return;
289
+ }
290
+
291
+ // Wait for the script to start running (it starts as 'queued')
292
+ if (run.status === 'queued') {
293
+ // Still waiting to start
294
+ return;
295
+ }
296
+
297
+ console.log(`[CronScheduler] Script status: ${run.status} for run: ${runId}`);
298
+
299
+ if (run.status === 'succeeded') {
300
+ clearInterval(checkInterval);
301
+ clearTimeout(timeoutId);
302
+ resolve(run.outputTail || '');
303
+ } else if (run.status === 'failed' || run.status === 'timed_out') {
304
+ clearInterval(checkInterval);
305
+ clearTimeout(timeoutId);
306
+ const errorMsg = run.error || 'Script execution failed';
307
+ reject(new Error(errorMsg));
308
+ } else if (run.status === 'running') {
309
+ // Still running, continue waiting
310
+ return;
311
+ } else {
312
+ clearInterval(checkInterval);
313
+ clearTimeout(timeoutId);
314
+ reject(new Error(`Unexpected script status: ${run.status}`));
315
+ }
316
+ } catch (err) {
317
+ console.log(`[CronScheduler] Error checking script status: ${err.message}`);
318
+ clearInterval(checkInterval);
319
+ clearTimeout(timeoutId);
320
+ reject(err);
321
+ }
322
+ }, 1000);
323
+ });
324
+ }
325
+
326
+ calculateNextRun(cronExpression, timezone = 'UTC') {
327
+ try {
328
+ const interval = parser.parseExpression(cronExpression, {
329
+ tz: timezone,
330
+ });
331
+ return interval.next().toDate();
332
+ } catch (err) {
333
+ console.error('Failed to calculate next run:', err);
334
+ return null;
335
+ }
336
+ }
337
+
338
+ getScheduledJobs() {
339
+ return Array.from(this.scheduledJobs.keys());
340
+ }
341
+
342
+ isJobScheduled(jobId) {
343
+ return this.scheduledJobs.has(String(jobId));
344
+ }
345
+ }
346
+
347
+ // Create singleton instance
348
+ const cronScheduler = new CronScheduler();
349
+
350
+ module.exports = cronScheduler;