@intranefr/superbackend 1.5.0 → 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 (171) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +23 -0
  4. package/package.json +7 -2
  5. package/src/admin/endpointRegistry.js +120 -0
  6. package/src/controllers/admin.controller.js +22 -5
  7. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  8. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  9. package/src/controllers/adminCache.controller.js +342 -0
  10. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  11. package/src/controllers/adminCrons.controller.js +388 -0
  12. package/src/controllers/adminDbBrowser.controller.js +124 -0
  13. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  14. package/src/controllers/adminHeadless.controller.js +9 -2
  15. package/src/controllers/adminHealthChecks.controller.js +570 -0
  16. package/src/controllers/adminI18n.controller.js +51 -29
  17. package/src/controllers/adminLlm.controller.js +126 -2
  18. package/src/controllers/adminPages.controller.js +720 -0
  19. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  20. package/src/controllers/adminProxy.controller.js +113 -0
  21. package/src/controllers/adminRateLimits.controller.js +138 -0
  22. package/src/controllers/adminRbac.controller.js +803 -0
  23. package/src/controllers/adminScripts.controller.js +93 -2
  24. package/src/controllers/adminSeoConfig.controller.js +71 -48
  25. package/src/controllers/blogAdmin.controller.js +279 -0
  26. package/src/controllers/blogAiAdmin.controller.js +224 -0
  27. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  28. package/src/controllers/blogInternal.controller.js +26 -0
  29. package/src/controllers/blogPublic.controller.js +89 -0
  30. package/src/controllers/fileManager.controller.js +190 -0
  31. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  32. package/src/controllers/healthChecksPublic.controller.js +196 -0
  33. package/src/controllers/metrics.controller.js +64 -4
  34. package/src/controllers/orgAdmin.controller.js +80 -0
  35. package/src/middleware/internalCronAuth.js +29 -0
  36. package/src/middleware/rbac.js +62 -0
  37. package/src/middleware.js +756 -48
  38. package/src/models/BlockDefinition.js +27 -0
  39. package/src/models/BlogAutomationLock.js +14 -0
  40. package/src/models/BlogAutomationRun.js +39 -0
  41. package/src/models/BlogPost.js +42 -0
  42. package/src/models/CacheEntry.js +26 -0
  43. package/src/models/ConsoleEntry.js +32 -0
  44. package/src/models/ConsoleLog.js +23 -0
  45. package/src/models/ContextBlockDefinition.js +33 -0
  46. package/src/models/CronExecution.js +47 -0
  47. package/src/models/CronJob.js +70 -0
  48. package/src/models/ExternalDbConnection.js +49 -0
  49. package/src/models/FileEntry.js +22 -0
  50. package/src/models/HealthAutoHealAttempt.js +57 -0
  51. package/src/models/HealthCheck.js +132 -0
  52. package/src/models/HealthCheckRun.js +51 -0
  53. package/src/models/HealthIncident.js +49 -0
  54. package/src/models/Page.js +95 -0
  55. package/src/models/PageCollection.js +42 -0
  56. package/src/models/ProxyEntry.js +66 -0
  57. package/src/models/RateLimitCounter.js +19 -0
  58. package/src/models/RateLimitMetricBucket.js +20 -0
  59. package/src/models/RbacGrant.js +25 -0
  60. package/src/models/RbacGroup.js +16 -0
  61. package/src/models/RbacGroupMember.js +13 -0
  62. package/src/models/RbacGroupRole.js +13 -0
  63. package/src/models/RbacRole.js +25 -0
  64. package/src/models/RbacUserRole.js +13 -0
  65. package/src/routes/adminBlog.routes.js +21 -0
  66. package/src/routes/adminBlogAi.routes.js +16 -0
  67. package/src/routes/adminBlogAutomation.routes.js +27 -0
  68. package/src/routes/adminCache.routes.js +20 -0
  69. package/src/routes/adminConsoleManager.routes.js +302 -0
  70. package/src/routes/adminCrons.routes.js +25 -0
  71. package/src/routes/adminDbBrowser.routes.js +65 -0
  72. package/src/routes/adminEjsVirtual.routes.js +2 -1
  73. package/src/routes/adminHeadless.routes.js +2 -1
  74. package/src/routes/adminHealthChecks.routes.js +28 -0
  75. package/src/routes/adminI18n.routes.js +4 -3
  76. package/src/routes/adminLlm.routes.js +4 -2
  77. package/src/routes/adminPages.routes.js +55 -0
  78. package/src/routes/adminProxy.routes.js +15 -0
  79. package/src/routes/adminRateLimits.routes.js +17 -0
  80. package/src/routes/adminRbac.routes.js +38 -0
  81. package/src/routes/adminSeoConfig.routes.js +5 -4
  82. package/src/routes/adminUiComponents.routes.js +2 -1
  83. package/src/routes/blogInternal.routes.js +14 -0
  84. package/src/routes/blogPublic.routes.js +9 -0
  85. package/src/routes/fileManager.routes.js +62 -0
  86. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  87. package/src/routes/healthChecksPublic.routes.js +9 -0
  88. package/src/routes/log.routes.js +43 -60
  89. package/src/routes/metrics.routes.js +4 -2
  90. package/src/routes/orgAdmin.routes.js +1 -0
  91. package/src/routes/pages.routes.js +123 -0
  92. package/src/routes/proxy.routes.js +46 -0
  93. package/src/routes/rbac.routes.js +47 -0
  94. package/src/routes/webhook.routes.js +2 -1
  95. package/src/routes/workflows.routes.js +4 -0
  96. package/src/services/blockDefinitionsAi.service.js +247 -0
  97. package/src/services/blog.service.js +99 -0
  98. package/src/services/blogAutomation.service.js +978 -0
  99. package/src/services/blogCronsBootstrap.service.js +184 -0
  100. package/src/services/blogPublishing.service.js +58 -0
  101. package/src/services/cacheLayer.service.js +696 -0
  102. package/src/services/consoleManager.service.js +700 -0
  103. package/src/services/consoleOverride.service.js +6 -1
  104. package/src/services/cronScheduler.service.js +350 -0
  105. package/src/services/dbBrowser.service.js +536 -0
  106. package/src/services/ejsVirtual.service.js +102 -32
  107. package/src/services/fileManager.service.js +475 -0
  108. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  109. package/src/services/healthChecks.service.js +650 -0
  110. package/src/services/healthChecksBootstrap.service.js +109 -0
  111. package/src/services/healthChecksScheduler.service.js +106 -0
  112. package/src/services/llmDefaults.service.js +190 -0
  113. package/src/services/migrationAssets/s3.js +2 -2
  114. package/src/services/pages.service.js +602 -0
  115. package/src/services/pagesContext.service.js +331 -0
  116. package/src/services/pagesContextBlocksAi.service.js +349 -0
  117. package/src/services/proxy.service.js +535 -0
  118. package/src/services/rateLimiter.service.js +623 -0
  119. package/src/services/rbac.service.js +212 -0
  120. package/src/services/scriptsRunner.service.js +1 -1
  121. package/src/services/uiComponentsAi.service.js +6 -19
  122. package/src/services/workflow.service.js +23 -8
  123. package/src/utils/orgRoles.js +14 -0
  124. package/src/utils/rbac/engine.js +60 -0
  125. package/src/utils/rbac/rightsRegistry.js +29 -0
  126. package/views/admin-blog-automation.ejs +877 -0
  127. package/views/admin-blog-edit.ejs +542 -0
  128. package/views/admin-blog.ejs +399 -0
  129. package/views/admin-cache.ejs +681 -0
  130. package/views/admin-console-manager.ejs +680 -0
  131. package/views/admin-crons.ejs +645 -0
  132. package/views/admin-db-browser.ejs +445 -0
  133. package/views/admin-ejs-virtual.ejs +16 -10
  134. package/views/admin-file-manager.ejs +942 -0
  135. package/views/admin-health-checks.ejs +725 -0
  136. package/views/admin-i18n.ejs +59 -5
  137. package/views/admin-llm.ejs +99 -1
  138. package/views/admin-organizations.ejs +163 -1
  139. package/views/admin-pages.ejs +2424 -0
  140. package/views/admin-proxy.ejs +491 -0
  141. package/views/admin-rate-limiter.ejs +625 -0
  142. package/views/admin-rbac.ejs +1331 -0
  143. package/views/admin-scripts.ejs +1 -1
  144. package/views/admin-seo-config.ejs +61 -7
  145. package/views/admin-ui-components.ejs +57 -25
  146. package/views/admin-workflows.ejs +7 -7
  147. package/views/file-manager.ejs +866 -0
  148. package/views/pages/blocks/contact.ejs +27 -0
  149. package/views/pages/blocks/cta.ejs +18 -0
  150. package/views/pages/blocks/faq.ejs +20 -0
  151. package/views/pages/blocks/features.ejs +19 -0
  152. package/views/pages/blocks/hero.ejs +13 -0
  153. package/views/pages/blocks/html.ejs +5 -0
  154. package/views/pages/blocks/image.ejs +14 -0
  155. package/views/pages/blocks/testimonials.ejs +26 -0
  156. package/views/pages/blocks/text.ejs +10 -0
  157. package/views/pages/layouts/default.ejs +51 -0
  158. package/views/pages/layouts/minimal.ejs +42 -0
  159. package/views/pages/layouts/sidebar.ejs +54 -0
  160. package/views/pages/partials/footer.ejs +13 -0
  161. package/views/pages/partials/header.ejs +12 -0
  162. package/views/pages/partials/sidebar.ejs +8 -0
  163. package/views/pages/runtime/page.ejs +10 -0
  164. package/views/pages/templates/article.ejs +20 -0
  165. package/views/pages/templates/default.ejs +12 -0
  166. package/views/pages/templates/landing.ejs +14 -0
  167. package/views/pages/templates/listing.ejs +15 -0
  168. package/views/partials/admin-image-upload-modal.ejs +221 -0
  169. package/views/partials/dashboard/nav-items.ejs +11 -0
  170. package/views/partials/llm-provider-model-picker.ejs +183 -0
  171. package/src/routes/llmUi.routes.js +0 -26
@@ -0,0 +1,388 @@
1
+ const CronJob = require('../models/CronJob');
2
+ const CronExecution = require('../models/CronExecution');
3
+ const parser = require('cron-parser');
4
+ const { startRun } = require('../services/scriptsRunner.service');
5
+ const cronScheduler = require('../services/cronScheduler.service');
6
+
7
+ function toSafeJsonError(error) {
8
+ const msg = error?.message || 'Operation failed';
9
+ const code = error?.code;
10
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
11
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
12
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
13
+ return { status: 500, body: { error: msg } };
14
+ }
15
+
16
+ function normalizeEnv(env) {
17
+ const items = Array.isArray(env) ? env : [];
18
+ const out = [];
19
+ for (const it of items) {
20
+ if (!it || typeof it !== 'object') continue;
21
+ const key = String(it.key || '').trim();
22
+ if (!key) continue;
23
+ out.push({ key, value: String(it.value || '') });
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function normalizeHeaders(headers) {
29
+ const items = Array.isArray(headers) ? headers : [];
30
+ const out = [];
31
+ for (const it of items) {
32
+ if (!it || typeof it !== 'object') continue;
33
+ const key = String(it.key || '').trim();
34
+ if (!key) continue;
35
+ out.push({ key, value: String(it.value || '') });
36
+ }
37
+ return out;
38
+ }
39
+
40
+ function calculateNextRun(cronExpression, timezone = 'UTC') {
41
+ try {
42
+ const interval = parser.parseExpression(cronExpression, {
43
+ tz: timezone,
44
+ });
45
+ return interval.next().toDate();
46
+ } catch (err) {
47
+ throw new Error(`Invalid cron expression: ${err.message}`);
48
+ }
49
+ }
50
+
51
+ exports.listCronJobs = async (req, res) => {
52
+ try {
53
+ const items = await CronJob.find()
54
+ .populate('scriptId', 'name type runner')
55
+ .sort({ updatedAt: -1 })
56
+ .lean();
57
+ res.json({ items });
58
+ } catch (err) {
59
+ const safe = toSafeJsonError(err);
60
+ res.status(safe.status).json(safe.body);
61
+ }
62
+ };
63
+
64
+ exports.getCronJob = async (req, res) => {
65
+ try {
66
+ const doc = await CronJob.findById(req.params.id)
67
+ .populate('scriptId', 'name type runner')
68
+ .lean();
69
+ if (!doc) return res.status(404).json({ error: 'Not found' });
70
+ res.json({ item: doc });
71
+ } catch (err) {
72
+ const safe = toSafeJsonError(err);
73
+ res.status(safe.status).json(safe.body);
74
+ }
75
+ };
76
+
77
+ exports.createCronJob = async (req, res) => {
78
+ try {
79
+ const payload = req.body || {};
80
+
81
+ // Validate cron expression
82
+ const nextRunAt = calculateNextRun(payload.cronExpression, payload.timezone);
83
+
84
+ // Validate task type requirements
85
+ if (payload.taskType === 'script' && (!payload.scriptId || payload.scriptId === "")) {
86
+ return res.status(400).json({ error: 'Script ID is required for script-type cron jobs' });
87
+ }
88
+ if (payload.taskType === 'http' && (!payload.httpUrl || payload.httpUrl.trim() === "")) {
89
+ return res.status(400).json({ error: 'URL is required for HTTP-type cron jobs' });
90
+ }
91
+
92
+ // Handle scriptId - convert empty string to null for script jobs
93
+ let scriptId = payload.scriptId;
94
+ if (scriptId === "" || scriptId === null || scriptId === undefined) {
95
+ scriptId = undefined;
96
+ }
97
+
98
+ const doc = await CronJob.create({
99
+ name: String(payload.name || '').trim(),
100
+ description: String(payload.description || ''),
101
+ cronExpression: String(payload.cronExpression || '').trim(),
102
+ timezone: String(payload.timezone || 'UTC'),
103
+ enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
104
+ nextRunAt,
105
+ taskType: String(payload.taskType || '').trim(),
106
+ scriptId,
107
+ scriptEnv: normalizeEnv(payload.scriptEnv),
108
+ httpMethod: String(payload.httpMethod || 'GET'),
109
+ httpUrl: payload.taskType === 'http' ? String(payload.httpUrl || '').trim() : undefined,
110
+ httpHeaders: normalizeHeaders(payload.httpHeaders),
111
+ httpBody: String(payload.httpBody || ''),
112
+ httpBodyType: String(payload.httpBodyType || 'raw'),
113
+ httpAuth: {
114
+ type: String(payload.httpAuth?.type || 'none'),
115
+ token: String(payload.httpAuth?.token || ''),
116
+ username: String(payload.httpAuth?.username || ''),
117
+ password: String(payload.httpAuth?.password || ''),
118
+ },
119
+ timeoutMs: payload.timeoutMs === undefined ? 300000 : Number(payload.timeoutMs),
120
+ createdBy: req.user?.username || 'admin',
121
+ });
122
+
123
+ // Schedule the job if enabled
124
+ if (doc.enabled) {
125
+ await cronScheduler.scheduleJob(doc);
126
+ }
127
+
128
+ res.status(201).json({ item: doc.toObject() });
129
+ } catch (err) {
130
+ const safe = toSafeJsonError(err);
131
+ res.status(safe.status).json(safe.body);
132
+ }
133
+ };
134
+
135
+ exports.updateCronJob = async (req, res) => {
136
+ try {
137
+ const payload = req.body || {};
138
+
139
+ const doc = await CronJob.findById(req.params.id);
140
+ if (!doc) return res.status(404).json({ error: 'Not found' });
141
+
142
+ const wasEnabled = doc.enabled;
143
+ let needsReschedule = false;
144
+
145
+ if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
146
+ if (payload.description !== undefined) doc.description = String(payload.description || '');
147
+ if (payload.cronExpression !== undefined) {
148
+ doc.cronExpression = String(payload.cronExpression || '').trim();
149
+ needsReschedule = true;
150
+ }
151
+ if (payload.timezone !== undefined) {
152
+ doc.timezone = String(payload.timezone || 'UTC');
153
+ needsReschedule = true;
154
+ }
155
+ if (payload.enabled !== undefined) {
156
+ doc.enabled = Boolean(payload.enabled);
157
+ needsReschedule = true;
158
+ }
159
+ if (payload.taskType !== undefined) {
160
+ const newTaskType = String(payload.taskType || '').trim();
161
+
162
+ // Validate task type requirements
163
+ if (newTaskType === 'script' && (!payload.scriptId || payload.scriptId === "")) {
164
+ return res.status(400).json({ error: 'Script ID is required for script-type cron jobs' });
165
+ }
166
+ if (newTaskType === 'http' && (!payload.httpUrl || payload.httpUrl.trim() === "")) {
167
+ return res.status(400).json({ error: 'URL is required for HTTP-type cron jobs' });
168
+ }
169
+
170
+ doc.taskType = newTaskType;
171
+ }
172
+ if (payload.scriptId !== undefined) {
173
+ // Handle scriptId - convert empty string to null
174
+ let scriptId = payload.scriptId;
175
+ if (scriptId === "" || scriptId === null || scriptId === undefined) {
176
+ doc.scriptId = undefined;
177
+ } else {
178
+ doc.scriptId = scriptId;
179
+ }
180
+ }
181
+ if (payload.scriptEnv !== undefined) doc.scriptEnv = normalizeEnv(payload.scriptEnv);
182
+ if (payload.httpMethod !== undefined) doc.httpMethod = String(payload.httpMethod || 'GET');
183
+ if (payload.httpUrl !== undefined) doc.httpUrl = String(payload.httpUrl || '').trim();
184
+ if (payload.httpHeaders !== undefined) doc.httpHeaders = normalizeHeaders(payload.httpHeaders);
185
+ if (payload.httpBody !== undefined) doc.httpBody = String(payload.httpBody || '');
186
+ if (payload.httpBodyType !== undefined) doc.httpBodyType = String(payload.httpBodyType || 'raw');
187
+ if (payload.httpAuth !== undefined) {
188
+ doc.httpAuth = {
189
+ type: String(payload.httpAuth?.type || 'none'),
190
+ token: String(payload.httpAuth?.token || ''),
191
+ username: String(payload.httpAuth?.username || ''),
192
+ password: String(payload.httpAuth?.password || ''),
193
+ };
194
+ }
195
+ if (payload.timeoutMs !== undefined) doc.timeoutMs = Number(payload.timeoutMs || 0);
196
+
197
+ // Recalculate next run time if schedule changed
198
+ if (needsReschedule) {
199
+ if (doc.enabled) {
200
+ doc.nextRunAt = calculateNextRun(doc.cronExpression, doc.timezone);
201
+ } else {
202
+ doc.nextRunAt = null;
203
+ }
204
+ }
205
+
206
+ await doc.save();
207
+
208
+ // Update scheduler
209
+ if (needsReschedule) {
210
+ if (wasEnabled && !doc.enabled) {
211
+ await cronScheduler.unscheduleJob(doc._id);
212
+ } else if (!wasEnabled && doc.enabled) {
213
+ await cronScheduler.scheduleJob(doc);
214
+ } else if (wasEnabled && doc.enabled) {
215
+ await cronScheduler.unscheduleJob(doc._id);
216
+ await cronScheduler.scheduleJob(doc);
217
+ }
218
+ }
219
+
220
+ res.json({ item: doc.toObject() });
221
+ } catch (err) {
222
+ const safe = toSafeJsonError(err);
223
+ res.status(safe.status).json(safe.body);
224
+ }
225
+ };
226
+
227
+ exports.deleteCronJob = async (req, res) => {
228
+ try {
229
+ const doc = await CronJob.findById(req.params.id);
230
+ if (!doc) return res.status(404).json({ error: 'Not found' });
231
+
232
+ // Unschedule before deleting
233
+ if (doc.enabled) {
234
+ await cronScheduler.unscheduleJob(doc._id);
235
+ }
236
+
237
+ await CronJob.deleteOne({ _id: doc._id });
238
+ res.json({ deleted: true });
239
+ } catch (err) {
240
+ const safe = toSafeJsonError(err);
241
+ res.status(safe.status).json(safe.body);
242
+ }
243
+ };
244
+
245
+ exports.enableCronJob = async (req, res) => {
246
+ try {
247
+ const doc = await CronJob.findById(req.params.id);
248
+ if (!doc) return res.status(404).json({ error: 'Not found' });
249
+
250
+ if (!doc.enabled) {
251
+ doc.enabled = true;
252
+ doc.nextRunAt = calculateNextRun(doc.cronExpression, doc.timezone);
253
+ await doc.save();
254
+ await cronScheduler.scheduleJob(doc);
255
+ }
256
+
257
+ res.json({ item: doc.toObject() });
258
+ } catch (err) {
259
+ const safe = toSafeJsonError(err);
260
+ res.status(safe.status).json(safe.body);
261
+ }
262
+ };
263
+
264
+ exports.disableCronJob = async (req, res) => {
265
+ try {
266
+ const doc = await CronJob.findById(req.params.id);
267
+ if (!doc) return res.status(404).json({ error: 'Not found' });
268
+
269
+ if (doc.enabled) {
270
+ doc.enabled = false;
271
+ doc.nextRunAt = null;
272
+ await doc.save();
273
+ await cronScheduler.unscheduleJob(doc._id);
274
+ }
275
+
276
+ res.json({ item: doc.toObject() });
277
+ } catch (err) {
278
+ const safe = toSafeJsonError(err);
279
+ res.status(safe.status).json(safe.body);
280
+ }
281
+ };
282
+
283
+ exports.triggerCronJob = async (req, res) => {
284
+ try {
285
+ const doc = await CronJob.findById(req.params.id);
286
+ if (!doc) return res.status(404).json({ error: 'Not found' });
287
+
288
+ // Execute immediately
289
+ const execution = await cronScheduler.executeJob(doc);
290
+
291
+ res.json({ executionId: execution._id });
292
+ } catch (err) {
293
+ const safe = toSafeJsonError(err);
294
+ res.status(safe.status).json(safe.body);
295
+ }
296
+ };
297
+
298
+ exports.getExecutionHistory = async (req, res) => {
299
+ try {
300
+ const { page = 1, limit = 50 } = req.query;
301
+ const skip = (page - 1) * limit;
302
+
303
+ const items = await CronExecution.find({ cronJobId: req.params.id })
304
+ .sort({ startedAt: -1 })
305
+ .skip(skip)
306
+ .limit(parseInt(limit))
307
+ .lean();
308
+
309
+ const total = await CronExecution.countDocuments({ cronJobId: req.params.id });
310
+
311
+ res.json({
312
+ items,
313
+ pagination: {
314
+ page: parseInt(page),
315
+ limit: parseInt(limit),
316
+ total,
317
+ pages: Math.ceil(total / limit),
318
+ },
319
+ });
320
+ } catch (err) {
321
+ const safe = toSafeJsonError(err);
322
+ res.status(safe.status).json(safe.body);
323
+ }
324
+ };
325
+
326
+ exports.getExecution = async (req, res) => {
327
+ try {
328
+ const doc = await CronExecution.findById(req.params.eid)
329
+ .populate('cronJobId', 'name taskType')
330
+ .lean();
331
+ if (!doc) return res.status(404).json({ error: 'Not found' });
332
+ res.json({ item: doc });
333
+ } catch (err) {
334
+ const safe = toSafeJsonError(err);
335
+ res.status(safe.status).json(safe.body);
336
+ }
337
+ };
338
+
339
+ exports.getCronPresets = async (req, res) => {
340
+ try {
341
+ const presets = [
342
+ { label: 'Every minute', expression: '* * * * *' },
343
+ { label: 'Every 5 minutes', expression: '*/5 * * * *' },
344
+ { label: 'Every 15 minutes', expression: '*/15 * * * *' },
345
+ { label: 'Every 30 minutes', expression: '*/30 * * * *' },
346
+ { label: 'Every hour', expression: '0 * * * *' },
347
+ { label: 'Every 2 hours', expression: '0 */2 * * *' },
348
+ { label: 'Every 6 hours', expression: '0 */6 * * *' },
349
+ { label: 'Every day at midnight', expression: '0 0 * * *' },
350
+ { label: 'Every day at 9 AM', expression: '0 9 * * *' },
351
+ { label: 'Every Monday at 9 AM', expression: '0 9 * * 1' },
352
+ { label: 'First day of month', expression: '0 0 1 * *' },
353
+ { label: 'Weekdays only', expression: '0 9 * * 1-5' },
354
+ ];
355
+ res.json({ presets });
356
+ } catch (err) {
357
+ const safe = toSafeJsonError(err);
358
+ res.status(safe.status).json(safe.body);
359
+ }
360
+ };
361
+
362
+ exports.previewNextRuns = async (req, res) => {
363
+ try {
364
+ const { cronExpression, timezone = 'UTC', count = 5 } = req.body;
365
+
366
+ if (!cronExpression) {
367
+ return res.status(400).json({ error: 'cronExpression is required' });
368
+ }
369
+
370
+ const runs = [];
371
+ const interval = parser.parseExpression(cronExpression, {
372
+ tz: timezone,
373
+ });
374
+
375
+ for (let i = 0; i < parseInt(count); i++) {
376
+ try {
377
+ runs.push(interval.next().toDate());
378
+ } catch (err) {
379
+ break;
380
+ }
381
+ }
382
+
383
+ res.json({ runs });
384
+ } catch (err) {
385
+ const safe = toSafeJsonError(err);
386
+ res.status(safe.status).json(safe.body);
387
+ }
388
+ };
@@ -0,0 +1,124 @@
1
+ const dbBrowser = require('../services/dbBrowser.service');
2
+
3
+ exports.listConnections = async (req, res) => {
4
+ try {
5
+ const items = await dbBrowser.listConnections();
6
+ res.json({ items });
7
+ } catch (err) {
8
+ const safe = dbBrowser.toSafeJsonError(err);
9
+ res.status(safe.status).json(safe.body);
10
+ }
11
+ };
12
+
13
+ exports.getConnection = async (req, res) => {
14
+ try {
15
+ const item = await dbBrowser.getConnection(req.params.id);
16
+ res.json({ item });
17
+ } catch (err) {
18
+ const safe = dbBrowser.toSafeJsonError(err);
19
+ res.status(safe.status).json(safe.body);
20
+ }
21
+ };
22
+
23
+ exports.createConnection = async (req, res) => {
24
+ try {
25
+ const item = await dbBrowser.createConnection(req.body || {});
26
+ res.status(201).json({ item });
27
+ } catch (err) {
28
+ if (err?.code === 11000) {
29
+ return res.status(400).json({ error: 'Connection name must be unique' });
30
+ }
31
+ const safe = dbBrowser.toSafeJsonError(err);
32
+ res.status(safe.status).json(safe.body);
33
+ }
34
+ };
35
+
36
+ exports.updateConnection = async (req, res) => {
37
+ try {
38
+ const item = await dbBrowser.updateConnection(req.params.id, req.body || {});
39
+ res.json({ item });
40
+ } catch (err) {
41
+ if (err?.code === 11000) {
42
+ return res.status(400).json({ error: 'Connection name must be unique' });
43
+ }
44
+ const safe = dbBrowser.toSafeJsonError(err);
45
+ res.status(safe.status).json(safe.body);
46
+ }
47
+ };
48
+
49
+ exports.deleteConnection = async (req, res) => {
50
+ try {
51
+ const out = await dbBrowser.deleteConnection(req.params.id);
52
+ res.json(out);
53
+ } catch (err) {
54
+ const safe = dbBrowser.toSafeJsonError(err);
55
+ res.status(safe.status).json(safe.body);
56
+ }
57
+ };
58
+
59
+ exports.testConnection = async (req, res) => {
60
+ try {
61
+ const out = await dbBrowser.testConnection(req.params.id);
62
+ res.json(out);
63
+ } catch (err) {
64
+ const safe = dbBrowser.toSafeJsonError(err);
65
+ res.status(safe.status).json(safe.body);
66
+ }
67
+ };
68
+
69
+ exports.listDatabases = async (req, res) => {
70
+ try {
71
+ const items = await dbBrowser.listDatabases(req.params.id);
72
+ res.json({ items });
73
+ } catch (err) {
74
+ const safe = dbBrowser.toSafeJsonError(err);
75
+ res.status(safe.status).json(safe.body);
76
+ }
77
+ };
78
+
79
+ exports.listNamespaces = async (req, res) => {
80
+ try {
81
+ const items = await dbBrowser.listNamespaces(req.params.id, req.params.database);
82
+ res.json({ items });
83
+ } catch (err) {
84
+ const safe = dbBrowser.toSafeJsonError(err);
85
+ res.status(safe.status).json(safe.body);
86
+ }
87
+ };
88
+
89
+ exports.getSchema = async (req, res) => {
90
+ try {
91
+ const schema = await dbBrowser.getSchema(req.params.id, req.params.database, req.params.namespace);
92
+ res.json({ schema });
93
+ } catch (err) {
94
+ const safe = dbBrowser.toSafeJsonError(err);
95
+ res.status(safe.status).json(safe.body);
96
+ }
97
+ };
98
+
99
+ exports.listRecords = async (req, res) => {
100
+ try {
101
+ const out = await dbBrowser.listRecords(req.params.id, req.params.database, req.params.namespace, {
102
+ page: req.query.page,
103
+ pageSize: req.query.pageSize,
104
+ filterField: req.query.filterField,
105
+ filterValue: req.query.filterValue,
106
+ sortField: req.query.sortField,
107
+ sortOrder: req.query.sortOrder,
108
+ });
109
+ res.json(out);
110
+ } catch (err) {
111
+ const safe = dbBrowser.toSafeJsonError(err);
112
+ res.status(safe.status).json(safe.body);
113
+ }
114
+ };
115
+
116
+ exports.getRecord = async (req, res) => {
117
+ try {
118
+ const item = await dbBrowser.getRecord(req.params.id, req.params.database, req.params.namespace, req.params.recordId);
119
+ res.json({ item });
120
+ } catch (err) {
121
+ const safe = dbBrowser.toSafeJsonError(err);
122
+ res.status(safe.status).json(safe.body);
123
+ }
124
+ };
@@ -83,10 +83,21 @@ exports.getFile = async (req, res) => {
83
83
  const viewsRoot = normalizeViewsRoot(req);
84
84
  const relPath = ejsVirtualService.normalizeRelPath(String(req.query.path || '').trim());
85
85
 
86
- const fsContent = await ejsVirtualService.readFsView(viewsRoot, relPath);
86
+ let fsContent = '';
87
+ try {
88
+ fsContent = await ejsVirtualService.readFsView(viewsRoot, relPath);
89
+ } catch (_) {
90
+ fsContent = '';
91
+ }
92
+
87
93
  const override = await VirtualEjsFile.findOne({ path: relPath }).lean();
88
94
 
89
- const effective = await ejsVirtualService.resolveTemplateSource({ viewsRoot, relPath, allowDb: true });
95
+ let effective;
96
+ try {
97
+ effective = await ejsVirtualService.resolveTemplateSource({ viewsRoot, relPath, allowDb: true });
98
+ } catch (_) {
99
+ effective = { source: override ? 'db' : 'none', content: override?.content || '' };
100
+ }
90
101
 
91
102
  res.json({
92
103
  path: relPath,
@@ -97,7 +108,6 @@ exports.getFile = async (req, res) => {
97
108
  } catch (err) {
98
109
  const code = err.code;
99
110
  if (code === 'VALIDATION') return res.status(400).json({ error: err.message });
100
- if (code === 'NOT_FOUND') return res.status(404).json({ error: err.message });
101
111
  console.error('[adminEjsVirtual] getFile error', err);
102
112
  res.status(500).json({ error: 'Failed to load file' });
103
113
  }
@@ -15,6 +15,7 @@ const {
15
15
 
16
16
  const llmService = require('../services/llm.service');
17
17
  const { getSettingValue } = require('../services/globalSettings.service');
18
+ const { resolveLlmProviderModel } = require('../services/llmDefaults.service');
18
19
  const { createAuditEvent, getBasicAuthActor } = require('../services/audit.service');
19
20
  const axios = require('axios');
20
21
  const { logAudit, scrubObject } = require('../services/auditLogger');
@@ -698,8 +699,14 @@ exports.aiModelBuilderChat = async (req, res) => {
698
699
  { role: 'user', content: message },
699
700
  ];
700
701
 
701
- const providerKey = (await getSettingValue('headless.aiProviderKey')) || process.env.HEADLESS_AI_PROVIDER_KEY || 'openrouter';
702
- const model = (await getSettingValue('headless.aiModel')) || process.env.HEADLESS_AI_MODEL || 'google/gemini-2.5-flash-lite';
702
+ const resolved = await resolveLlmProviderModel({
703
+ systemKey: 'headless.modelBuilder.chat',
704
+ providerKey: await getSettingValue('headless.aiProviderKey'),
705
+ model: await getSettingValue('headless.aiModel'),
706
+ });
707
+
708
+ const providerKey = resolved.providerKey;
709
+ const model = resolved.model;
703
710
 
704
711
  console.log('[headless aiModelBuilder] Resolved providerKey:', providerKey);
705
712
  console.log('[headless aiModelBuilder] Resolved model:', model);