@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,570 @@
1
+ const HealthCheck = require('../models/HealthCheck');
2
+ const HealthCheckRun = require('../models/HealthCheckRun');
3
+ const HealthIncident = require('../models/HealthIncident');
4
+
5
+ const GlobalSetting = require('../models/GlobalSetting');
6
+ const { encryptString } = require('../utils/encryption');
7
+ const globalSettingsService = require('../services/globalSettings.service');
8
+
9
+ const healthChecksService = require('../services/healthChecks.service');
10
+ const healthChecksScheduler = require('../services/healthChecksScheduler.service');
11
+
12
+ const PUBLIC_STATUS_SETTING_KEY = 'healthChecks.publicStatusEnabled';
13
+
14
+ function toSafeJsonError(error) {
15
+ const msg = error?.message || 'Operation failed';
16
+ const code = error?.code;
17
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
18
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
19
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
20
+ return { status: 500, body: { error: msg } };
21
+ }
22
+
23
+ function normalizeEnv(env) {
24
+ const items = Array.isArray(env) ? env : [];
25
+ const out = [];
26
+ for (const it of items) {
27
+ if (!it || typeof it !== 'object') continue;
28
+ const key = String(it.key || '').trim();
29
+ if (!key) continue;
30
+ out.push({ key, value: String(it.value || '') });
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function normalizeHeaders(headers) {
36
+ const items = Array.isArray(headers) ? headers : [];
37
+ const out = [];
38
+ for (const it of items) {
39
+ if (!it || typeof it !== 'object') continue;
40
+ const key = String(it.key || '').trim();
41
+ if (!key) continue;
42
+ out.push({ key, value: String(it.value || '') });
43
+ }
44
+ return out;
45
+ }
46
+
47
+ async function upsertEncryptedSetting({ key, description, value }) {
48
+ const storedValue = JSON.stringify(encryptString(String(value || '')));
49
+
50
+ let doc = await GlobalSetting.findOne({ key });
51
+ if (!doc) {
52
+ doc = await GlobalSetting.create({
53
+ key,
54
+ value: storedValue,
55
+ type: 'encrypted',
56
+ description,
57
+ templateVariables: [],
58
+ public: false,
59
+ });
60
+ } else {
61
+ doc.value = storedValue;
62
+ if (doc.description !== description) doc.description = description;
63
+ doc.type = 'encrypted';
64
+ doc.public = false;
65
+ await doc.save();
66
+ }
67
+
68
+ globalSettingsService.clearSettingsCache();
69
+ return doc;
70
+ }
71
+
72
+ async function ensurePublicStatusSettingExists() {
73
+ const existing = await GlobalSetting.findOne({ key: PUBLIC_STATUS_SETTING_KEY });
74
+ if (existing) return;
75
+
76
+ await GlobalSetting.create({
77
+ key: PUBLIC_STATUS_SETTING_KEY,
78
+ value: 'false',
79
+ type: 'boolean',
80
+ description: 'Enable the public health checks status summary endpoint (/api/health-checks/status).',
81
+ templateVariables: [],
82
+ public: false,
83
+ });
84
+
85
+ globalSettingsService.clearSettingsCache();
86
+ }
87
+
88
+ async function getPublicStatusEnabled() {
89
+ await ensurePublicStatusSettingExists();
90
+ const raw = await globalSettingsService.getSettingValue(PUBLIC_STATUS_SETTING_KEY, 'false');
91
+ return String(raw) === 'true';
92
+ }
93
+
94
+ function applyAuthSecretsToCheckDoc(doc, payload) {
95
+ const type = String(payload?.httpAuth?.type || doc.httpAuth?.type || 'none');
96
+ const next = {
97
+ type,
98
+ username: String(payload?.httpAuth?.username ?? doc.httpAuth?.username ?? ''),
99
+ tokenSettingKey: doc.httpAuth?.tokenSettingKey,
100
+ passwordSettingKey: doc.httpAuth?.passwordSettingKey,
101
+ };
102
+
103
+ if (type !== 'bearer') {
104
+ next.tokenSettingKey = undefined;
105
+ }
106
+ if (type !== 'basic') {
107
+ next.passwordSettingKey = undefined;
108
+ }
109
+
110
+ doc.httpAuth = next;
111
+ }
112
+
113
+ async function persistAuthSecrets(doc, payload) {
114
+ const type = String(payload?.httpAuth?.type || doc.httpAuth?.type || 'none');
115
+
116
+ if (type === 'bearer') {
117
+ const rawToken = typeof payload?.httpAuth?.token === 'string' ? payload.httpAuth.token.trim() : '';
118
+ if (rawToken) {
119
+ const key = `healthChecks.${doc._id}.httpAuth.bearerToken`;
120
+ await upsertEncryptedSetting({
121
+ key,
122
+ description: `Health check bearer token for ${doc.name}`,
123
+ value: rawToken,
124
+ });
125
+ doc.httpAuth.tokenSettingKey = key;
126
+ }
127
+ }
128
+
129
+ if (type === 'basic') {
130
+ const rawPassword = typeof payload?.httpAuth?.password === 'string' ? payload.httpAuth.password : '';
131
+ if (rawPassword) {
132
+ const key = `healthChecks.${doc._id}.httpAuth.basicPassword`;
133
+ await upsertEncryptedSetting({
134
+ key,
135
+ description: `Health check basic-auth password for ${doc.name}`,
136
+ value: rawPassword,
137
+ });
138
+ doc.httpAuth.passwordSettingKey = key;
139
+ }
140
+ }
141
+ }
142
+
143
+ exports.getConfig = async (req, res) => {
144
+ try {
145
+ const publicStatusEnabled = await getPublicStatusEnabled();
146
+ res.json({
147
+ publicStatusEnabled,
148
+ publicStatusPath: '/api/health-checks/status',
149
+ globalSettingKey: PUBLIC_STATUS_SETTING_KEY,
150
+ });
151
+ } catch (err) {
152
+ const safe = toSafeJsonError(err);
153
+ res.status(safe.status).json(safe.body);
154
+ }
155
+ };
156
+
157
+ exports.updateConfig = async (req, res) => {
158
+ try {
159
+ await ensurePublicStatusSettingExists();
160
+
161
+ const enabled = Boolean(req.body?.publicStatusEnabled);
162
+
163
+ const doc = await GlobalSetting.findOne({ key: PUBLIC_STATUS_SETTING_KEY });
164
+ doc.value = enabled ? 'true' : 'false';
165
+ doc.type = 'boolean';
166
+ doc.public = false;
167
+ await doc.save();
168
+
169
+ globalSettingsService.clearSettingsCache();
170
+
171
+ res.json({ publicStatusEnabled: enabled });
172
+ } catch (err) {
173
+ const safe = toSafeJsonError(err);
174
+ res.status(safe.status).json(safe.body);
175
+ }
176
+ };
177
+
178
+ exports.listHealthChecks = async (req, res) => {
179
+ try {
180
+ const items = await HealthCheck.find().sort({ updatedAt: -1 }).lean();
181
+ const publicStatusEnabled = await getPublicStatusEnabled();
182
+ res.json({ items, publicStatusEnabled });
183
+ } catch (err) {
184
+ const safe = toSafeJsonError(err);
185
+ res.status(safe.status).json(safe.body);
186
+ }
187
+ };
188
+
189
+ exports.getHealthCheck = async (req, res) => {
190
+ try {
191
+ const doc = await HealthCheck.findById(req.params.id).lean();
192
+ if (!doc) return res.status(404).json({ error: 'Not found' });
193
+ res.json({ item: doc });
194
+ } catch (err) {
195
+ const safe = toSafeJsonError(err);
196
+ res.status(safe.status).json(safe.body);
197
+ }
198
+ };
199
+
200
+ exports.createHealthCheck = async (req, res) => {
201
+ try {
202
+ const payload = req.body || {};
203
+
204
+ const checkType = String(payload.checkType || '').trim();
205
+ if (!['http', 'script', 'internal'].includes(checkType)) {
206
+ return res.status(400).json({ error: 'Invalid checkType' });
207
+ }
208
+
209
+ if (checkType === 'http' && !String(payload.httpUrl || '').trim()) {
210
+ return res.status(400).json({ error: 'httpUrl is required for http checks' });
211
+ }
212
+
213
+ if (checkType === 'script' && !payload.scriptId) {
214
+ return res.status(400).json({ error: 'scriptId is required for script checks' });
215
+ }
216
+
217
+ const cronExpression = String(payload.cronExpression || '').trim();
218
+ if (!cronExpression) {
219
+ return res.status(400).json({ error: 'cronExpression is required' });
220
+ }
221
+
222
+ const timezone = String(payload.timezone || 'UTC');
223
+ const nextRunAt = healthChecksService.calculateNextRun(cronExpression, timezone);
224
+
225
+ const doc = await HealthCheck.create({
226
+ name: String(payload.name || '').trim(),
227
+ description: String(payload.description || ''),
228
+ enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
229
+ cronExpression,
230
+ timezone,
231
+ nextRunAt,
232
+ checkType,
233
+ timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
234
+
235
+ httpMethod: String(payload.httpMethod || 'GET'),
236
+ httpUrl: String(payload.httpUrl || '').trim() || undefined,
237
+ httpHeaders: normalizeHeaders(payload.httpHeaders),
238
+ httpBody: String(payload.httpBody || ''),
239
+ httpBodyType: String(payload.httpBodyType || 'raw'),
240
+ httpAuth: {
241
+ type: String(payload.httpAuth?.type || 'none'),
242
+ username: String(payload.httpAuth?.username || ''),
243
+ },
244
+
245
+ scriptId: payload.scriptId || undefined,
246
+ scriptEnv: normalizeEnv(payload.scriptEnv),
247
+
248
+ expectedStatusCodes: Array.isArray(payload.expectedStatusCodes) ? payload.expectedStatusCodes : undefined,
249
+ maxLatencyMs: payload.maxLatencyMs === undefined ? undefined : Number(payload.maxLatencyMs),
250
+ bodyMustMatch: payload.bodyMustMatch === undefined ? undefined : String(payload.bodyMustMatch || ''),
251
+ bodyMustNotMatch: payload.bodyMustNotMatch === undefined ? undefined : String(payload.bodyMustNotMatch || ''),
252
+
253
+ consecutiveFailuresToOpen: payload.consecutiveFailuresToOpen === undefined ? undefined : Number(payload.consecutiveFailuresToOpen),
254
+ consecutiveSuccessesToResolve: payload.consecutiveSuccessesToResolve === undefined ? undefined : Number(payload.consecutiveSuccessesToResolve),
255
+
256
+ retries: payload.retries === undefined ? undefined : Number(payload.retries),
257
+ retryDelayMs: payload.retryDelayMs === undefined ? undefined : Number(payload.retryDelayMs),
258
+
259
+ notifyOnOpen: payload.notifyOnOpen === undefined ? undefined : Boolean(payload.notifyOnOpen),
260
+ notifyOnResolve: payload.notifyOnResolve === undefined ? undefined : Boolean(payload.notifyOnResolve),
261
+ notifyOnEscalation: payload.notifyOnEscalation === undefined ? undefined : Boolean(payload.notifyOnEscalation),
262
+ notificationChannel: String(payload.notificationChannel || 'in_app'),
263
+ notifyUserIds: Array.isArray(payload.notifyUserIds) ? payload.notifyUserIds : [],
264
+ suppressNotificationsWhenAcknowledged:
265
+ payload.suppressNotificationsWhenAcknowledged === undefined ? undefined : Boolean(payload.suppressNotificationsWhenAcknowledged),
266
+
267
+ autoHealEnabled: payload.autoHealEnabled === undefined ? undefined : Boolean(payload.autoHealEnabled),
268
+ autoHealWaitMs: payload.autoHealWaitMs === undefined ? undefined : Number(payload.autoHealWaitMs),
269
+ autoHealCooldownMs: payload.autoHealCooldownMs === undefined ? undefined : Number(payload.autoHealCooldownMs),
270
+ autoHealMaxAttemptsPerIncident:
271
+ payload.autoHealMaxAttemptsPerIncident === undefined ? undefined : Number(payload.autoHealMaxAttemptsPerIncident),
272
+ autoHealBackoffPolicy: payload.autoHealBackoffPolicy === undefined ? undefined : String(payload.autoHealBackoffPolicy || 'fixed'),
273
+ autoHealBackoffMs: payload.autoHealBackoffMs === undefined ? undefined : Number(payload.autoHealBackoffMs),
274
+ autoHealActions: Array.isArray(payload.autoHealActions) ? payload.autoHealActions : [],
275
+
276
+ createdBy: req.user?.username || 'admin',
277
+ });
278
+
279
+ // Apply secret refs and store secrets (if provided)
280
+ applyAuthSecretsToCheckDoc(doc, payload);
281
+ await persistAuthSecrets(doc, payload);
282
+ await doc.save();
283
+
284
+ if (doc.enabled) {
285
+ await healthChecksScheduler.scheduleCheck(doc);
286
+ }
287
+
288
+ res.status(201).json({ item: doc.toObject() });
289
+ } catch (err) {
290
+ const safe = toSafeJsonError(err);
291
+ res.status(safe.status).json(safe.body);
292
+ }
293
+ };
294
+
295
+ exports.updateHealthCheck = async (req, res) => {
296
+ try {
297
+ const payload = req.body || {};
298
+
299
+ const doc = await HealthCheck.findById(req.params.id);
300
+ if (!doc) return res.status(404).json({ error: 'Not found' });
301
+
302
+ const wasEnabled = doc.enabled;
303
+ let needsReschedule = false;
304
+
305
+ if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
306
+ if (payload.description !== undefined) doc.description = String(payload.description || '');
307
+
308
+ if (payload.enabled !== undefined) {
309
+ doc.enabled = Boolean(payload.enabled);
310
+ needsReschedule = true;
311
+ }
312
+
313
+ if (payload.cronExpression !== undefined) {
314
+ doc.cronExpression = String(payload.cronExpression || '').trim();
315
+ needsReschedule = true;
316
+ }
317
+
318
+ if (payload.timezone !== undefined) {
319
+ doc.timezone = String(payload.timezone || 'UTC');
320
+ needsReschedule = true;
321
+ }
322
+
323
+ if (payload.checkType !== undefined) {
324
+ doc.checkType = String(payload.checkType || '').trim();
325
+ }
326
+
327
+ if (payload.timeoutMs !== undefined) doc.timeoutMs = Number(payload.timeoutMs || 0);
328
+
329
+ if (payload.httpMethod !== undefined) doc.httpMethod = String(payload.httpMethod || 'GET');
330
+ if (payload.httpUrl !== undefined) doc.httpUrl = String(payload.httpUrl || '').trim();
331
+ if (payload.httpHeaders !== undefined) doc.httpHeaders = normalizeHeaders(payload.httpHeaders);
332
+ if (payload.httpBody !== undefined) doc.httpBody = String(payload.httpBody || '');
333
+ if (payload.httpBodyType !== undefined) doc.httpBodyType = String(payload.httpBodyType || 'raw');
334
+
335
+ if (payload.httpAuth !== undefined) {
336
+ applyAuthSecretsToCheckDoc(doc, payload);
337
+ await persistAuthSecrets(doc, payload);
338
+ }
339
+
340
+ if (payload.scriptId !== undefined) doc.scriptId = payload.scriptId || undefined;
341
+ if (payload.scriptEnv !== undefined) doc.scriptEnv = normalizeEnv(payload.scriptEnv);
342
+
343
+ if (payload.expectedStatusCodes !== undefined) {
344
+ doc.expectedStatusCodes = Array.isArray(payload.expectedStatusCodes) ? payload.expectedStatusCodes : doc.expectedStatusCodes;
345
+ }
346
+ if (payload.maxLatencyMs !== undefined) doc.maxLatencyMs = payload.maxLatencyMs === null ? undefined : Number(payload.maxLatencyMs);
347
+ if (payload.bodyMustMatch !== undefined) doc.bodyMustMatch = payload.bodyMustMatch ? String(payload.bodyMustMatch) : undefined;
348
+ if (payload.bodyMustNotMatch !== undefined) doc.bodyMustNotMatch = payload.bodyMustNotMatch ? String(payload.bodyMustNotMatch) : undefined;
349
+
350
+ if (payload.consecutiveFailuresToOpen !== undefined) doc.consecutiveFailuresToOpen = Number(payload.consecutiveFailuresToOpen);
351
+ if (payload.consecutiveSuccessesToResolve !== undefined) doc.consecutiveSuccessesToResolve = Number(payload.consecutiveSuccessesToResolve);
352
+
353
+ if (payload.retries !== undefined) doc.retries = Number(payload.retries);
354
+ if (payload.retryDelayMs !== undefined) doc.retryDelayMs = Number(payload.retryDelayMs);
355
+
356
+ if (payload.notifyOnOpen !== undefined) doc.notifyOnOpen = Boolean(payload.notifyOnOpen);
357
+ if (payload.notifyOnResolve !== undefined) doc.notifyOnResolve = Boolean(payload.notifyOnResolve);
358
+ if (payload.notifyOnEscalation !== undefined) doc.notifyOnEscalation = Boolean(payload.notifyOnEscalation);
359
+ if (payload.notificationChannel !== undefined) doc.notificationChannel = String(payload.notificationChannel || 'in_app');
360
+ if (payload.notifyUserIds !== undefined) doc.notifyUserIds = Array.isArray(payload.notifyUserIds) ? payload.notifyUserIds : [];
361
+ if (payload.suppressNotificationsWhenAcknowledged !== undefined) {
362
+ doc.suppressNotificationsWhenAcknowledged = Boolean(payload.suppressNotificationsWhenAcknowledged);
363
+ }
364
+
365
+ if (payload.autoHealEnabled !== undefined) doc.autoHealEnabled = Boolean(payload.autoHealEnabled);
366
+ if (payload.autoHealWaitMs !== undefined) doc.autoHealWaitMs = Number(payload.autoHealWaitMs);
367
+ if (payload.autoHealCooldownMs !== undefined) doc.autoHealCooldownMs = Number(payload.autoHealCooldownMs);
368
+ if (payload.autoHealMaxAttemptsPerIncident !== undefined) {
369
+ doc.autoHealMaxAttemptsPerIncident = Number(payload.autoHealMaxAttemptsPerIncident);
370
+ }
371
+ if (payload.autoHealBackoffPolicy !== undefined) doc.autoHealBackoffPolicy = String(payload.autoHealBackoffPolicy || 'fixed');
372
+ if (payload.autoHealBackoffMs !== undefined) doc.autoHealBackoffMs = Number(payload.autoHealBackoffMs);
373
+ if (payload.autoHealActions !== undefined) doc.autoHealActions = Array.isArray(payload.autoHealActions) ? payload.autoHealActions : [];
374
+
375
+ if (needsReschedule) {
376
+ if (doc.enabled) {
377
+ doc.nextRunAt = healthChecksService.calculateNextRun(doc.cronExpression, doc.timezone);
378
+ } else {
379
+ doc.nextRunAt = null;
380
+ }
381
+ }
382
+
383
+ await doc.save();
384
+
385
+ if (needsReschedule) {
386
+ if (wasEnabled && !doc.enabled) {
387
+ await healthChecksScheduler.unscheduleCheck(doc._id);
388
+ } else if (!wasEnabled && doc.enabled) {
389
+ await healthChecksScheduler.scheduleCheck(doc);
390
+ } else if (wasEnabled && doc.enabled) {
391
+ await healthChecksScheduler.unscheduleCheck(doc._id);
392
+ await healthChecksScheduler.scheduleCheck(doc);
393
+ }
394
+ }
395
+
396
+ res.json({ item: doc.toObject() });
397
+ } catch (err) {
398
+ const safe = toSafeJsonError(err);
399
+ res.status(safe.status).json(safe.body);
400
+ }
401
+ };
402
+
403
+ exports.deleteHealthCheck = async (req, res) => {
404
+ try {
405
+ const doc = await HealthCheck.findById(req.params.id);
406
+ if (!doc) return res.status(404).json({ error: 'Not found' });
407
+
408
+ if (doc.enabled) {
409
+ await healthChecksScheduler.unscheduleCheck(doc._id);
410
+ }
411
+
412
+ await HealthCheck.deleteOne({ _id: doc._id });
413
+ res.json({ deleted: true });
414
+ } catch (err) {
415
+ const safe = toSafeJsonError(err);
416
+ res.status(safe.status).json(safe.body);
417
+ }
418
+ };
419
+
420
+ exports.enableHealthCheck = async (req, res) => {
421
+ try {
422
+ const doc = await HealthCheck.findById(req.params.id);
423
+ if (!doc) return res.status(404).json({ error: 'Not found' });
424
+
425
+ if (!doc.enabled) {
426
+ doc.enabled = true;
427
+ doc.nextRunAt = healthChecksService.calculateNextRun(doc.cronExpression, doc.timezone);
428
+ await doc.save();
429
+ await healthChecksScheduler.scheduleCheck(doc);
430
+ }
431
+
432
+ res.json({ item: doc.toObject() });
433
+ } catch (err) {
434
+ const safe = toSafeJsonError(err);
435
+ res.status(safe.status).json(safe.body);
436
+ }
437
+ };
438
+
439
+ exports.disableHealthCheck = async (req, res) => {
440
+ try {
441
+ const doc = await HealthCheck.findById(req.params.id);
442
+ if (!doc) return res.status(404).json({ error: 'Not found' });
443
+
444
+ if (doc.enabled) {
445
+ doc.enabled = false;
446
+ doc.nextRunAt = null;
447
+ await doc.save();
448
+ await healthChecksScheduler.unscheduleCheck(doc._id);
449
+ }
450
+
451
+ res.json({ item: doc.toObject() });
452
+ } catch (err) {
453
+ const safe = toSafeJsonError(err);
454
+ res.status(safe.status).json(safe.body);
455
+ }
456
+ };
457
+
458
+ exports.triggerHealthCheck = async (req, res) => {
459
+ try {
460
+ const doc = await HealthCheck.findById(req.params.id);
461
+ if (!doc) return res.status(404).json({ error: 'Not found' });
462
+
463
+ const result = await healthChecksScheduler.trigger(doc._id);
464
+ res.json(result);
465
+ } catch (err) {
466
+ const safe = toSafeJsonError(err);
467
+ res.status(safe.status).json(safe.body);
468
+ }
469
+ };
470
+
471
+ exports.getRunHistory = async (req, res) => {
472
+ try {
473
+ const { page = 1, limit = 50 } = req.query;
474
+ const skip = (Number(page) - 1) * Number(limit);
475
+
476
+ const items = await HealthCheckRun.find({ healthCheckId: req.params.id })
477
+ .sort({ startedAt: -1 })
478
+ .skip(skip)
479
+ .limit(Number(limit))
480
+ .lean();
481
+
482
+ const total = await HealthCheckRun.countDocuments({ healthCheckId: req.params.id });
483
+
484
+ res.json({
485
+ items,
486
+ pagination: {
487
+ page: Number(page),
488
+ limit: Number(limit),
489
+ total,
490
+ pages: Math.ceil(total / Number(limit)),
491
+ },
492
+ });
493
+ } catch (err) {
494
+ const safe = toSafeJsonError(err);
495
+ res.status(safe.status).json(safe.body);
496
+ }
497
+ };
498
+
499
+ exports.getIncidents = async (req, res) => {
500
+ try {
501
+ const { page = 1, limit = 50 } = req.query;
502
+ const skip = (Number(page) - 1) * Number(limit);
503
+
504
+ const items = await HealthIncident.find({ healthCheckId: req.params.id })
505
+ .sort({ openedAt: -1 })
506
+ .skip(skip)
507
+ .limit(Number(limit))
508
+ .lean();
509
+
510
+ const total = await HealthIncident.countDocuments({ healthCheckId: req.params.id });
511
+
512
+ res.json({
513
+ items,
514
+ pagination: {
515
+ page: Number(page),
516
+ limit: Number(limit),
517
+ total,
518
+ pages: Math.ceil(total / Number(limit)),
519
+ },
520
+ });
521
+ } catch (err) {
522
+ const safe = toSafeJsonError(err);
523
+ res.status(safe.status).json(safe.body);
524
+ }
525
+ };
526
+
527
+ exports.acknowledgeIncident = async (req, res) => {
528
+ try {
529
+ const { id, incidentId } = req.params;
530
+
531
+ const incident = await HealthIncident.findOne({ _id: incidentId, healthCheckId: id });
532
+ if (!incident) return res.status(404).json({ error: 'Not found' });
533
+
534
+ if (incident.status === 'open') {
535
+ incident.status = 'acknowledged';
536
+ incident.acknowledgedAt = new Date();
537
+ await incident.save();
538
+ }
539
+
540
+ res.json({ item: incident.toObject() });
541
+ } catch (err) {
542
+ const safe = toSafeJsonError(err);
543
+ res.status(safe.status).json(safe.body);
544
+ }
545
+ };
546
+
547
+ exports.resolveIncident = async (req, res) => {
548
+ try {
549
+ const { id, incidentId } = req.params;
550
+
551
+ const incident = await HealthIncident.findOne({ _id: incidentId, healthCheckId: id });
552
+ if (!incident) return res.status(404).json({ error: 'Not found' });
553
+
554
+ if (incident.status !== 'resolved') {
555
+ incident.status = 'resolved';
556
+ incident.resolvedAt = new Date();
557
+ await incident.save();
558
+
559
+ await HealthCheck.updateOne(
560
+ { _id: id, currentIncidentId: incident._id },
561
+ { $set: { currentIncidentId: null, consecutiveFailureCount: 0, consecutiveSuccessCount: 0 } },
562
+ );
563
+ }
564
+
565
+ res.json({ item: incident.toObject() });
566
+ } catch (err) {
567
+ const safe = toSafeJsonError(err);
568
+ res.status(safe.status).json(safe.body);
569
+ }
570
+ };