@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
@@ -0,0 +1,200 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const Experiment = require('../models/Experiment');
4
+ const ExperimentMetricBucket = require('../models/ExperimentMetricBucket');
5
+
6
+ const experimentsService = require('../services/experiments.service');
7
+
8
+ function toSafeJsonError(error) {
9
+ const msg = error?.message || 'Operation failed';
10
+ const code = error?.code;
11
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
12
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
13
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
14
+ return { status: 500, body: { error: msg } };
15
+ }
16
+
17
+ function isValidObjectId(id) {
18
+ return id && mongoose.Types.ObjectId.isValid(String(id));
19
+ }
20
+
21
+ function normalizeVariant(v) {
22
+ const key = String(v?.key || '').trim();
23
+ const weight = Number(v?.weight || 0) || 0;
24
+ const configSlug = String(v?.configSlug || '').trim();
25
+ if (!key) return null;
26
+ return { key, weight, configSlug };
27
+ }
28
+
29
+ function normalizeMetric(d) {
30
+ const key = String(d?.key || '').trim();
31
+ const kind = String(d?.kind || '').trim() || 'count';
32
+ if (!key) return null;
33
+ return {
34
+ key,
35
+ kind,
36
+ numeratorEventKey: String(d?.numeratorEventKey || '').trim(),
37
+ denominatorEventKey: String(d?.denominatorEventKey || '').trim(),
38
+ objective: String(d?.objective || 'maximize').trim() === 'minimize' ? 'minimize' : 'maximize',
39
+ };
40
+ }
41
+
42
+ exports.list = async (req, res) => {
43
+ try {
44
+ const orgId = req.query.orgId || null;
45
+ const q = {};
46
+ if (orgId) q.organizationId = orgId;
47
+
48
+ const items = await Experiment.find(q).sort({ updatedAt: -1 }).lean();
49
+ return res.json({ items });
50
+ } catch (err) {
51
+ const safe = toSafeJsonError(err);
52
+ return res.status(safe.status).json(safe.body);
53
+ }
54
+ };
55
+
56
+ exports.get = async (req, res) => {
57
+ try {
58
+ const id = req.params.id;
59
+ const doc = await Experiment.findById(id).lean();
60
+ if (!doc) return res.status(404).json({ error: 'Not found' });
61
+ return res.json({ item: doc });
62
+ } catch (err) {
63
+ const safe = toSafeJsonError(err);
64
+ return res.status(safe.status).json(safe.body);
65
+ }
66
+ };
67
+
68
+ exports.create = async (req, res) => {
69
+ try {
70
+ const p = req.body || {};
71
+
72
+ const orgId = p.organizationId === null || p.organizationId === '' ? null : p.organizationId;
73
+ if (orgId && !isValidObjectId(orgId)) {
74
+ return res.status(400).json({ error: 'Invalid organizationId' });
75
+ }
76
+
77
+ const code = String(p.code || '').trim();
78
+ if (!code) return res.status(400).json({ error: 'code is required' });
79
+
80
+ const variants = (Array.isArray(p.variants) ? p.variants : []).map(normalizeVariant).filter(Boolean);
81
+ const primaryMetric = normalizeMetric(p.primaryMetric);
82
+ if (!primaryMetric) return res.status(400).json({ error: 'primaryMetric is required' });
83
+
84
+ const doc = await Experiment.create({
85
+ organizationId: orgId || null,
86
+ code,
87
+ name: String(p.name || '').trim(),
88
+ description: String(p.description || '').trim(),
89
+ status: String(p.status || 'draft'),
90
+ startedAt: p.startedAt ? new Date(p.startedAt) : null,
91
+ endsAt: p.endsAt ? new Date(p.endsAt) : null,
92
+ assignment: { unit: 'subjectId', sticky: p.assignment?.sticky !== false, salt: String(p.assignment?.salt || '').trim() },
93
+ variants,
94
+ primaryMetric,
95
+ secondaryMetrics: (Array.isArray(p.secondaryMetrics) ? p.secondaryMetrics : []).map(normalizeMetric).filter(Boolean),
96
+ winnerPolicy: {
97
+ mode: String(p.winnerPolicy?.mode || 'manual') === 'automatic' ? 'automatic' : 'manual',
98
+ pickAfterMs: Number(p.winnerPolicy?.pickAfterMs || 0) || 0,
99
+ minAssignments: Number(p.winnerPolicy?.minAssignments || 0) || 0,
100
+ minExposures: Number(p.winnerPolicy?.minExposures || 0) || 0,
101
+ minConversions: Number(p.winnerPolicy?.minConversions || 0) || 0,
102
+ statMethod: String(p.winnerPolicy?.statMethod || 'simple_rate'),
103
+ overrideWinnerVariantKey: String(p.winnerPolicy?.overrideWinnerVariantKey || '').trim(),
104
+ },
105
+ createdByUserId: req.user?._id || null,
106
+ updatedByUserId: req.user?._id || null,
107
+ });
108
+
109
+ await experimentsService.clearExperimentCaches(doc._id);
110
+
111
+ return res.status(201).json({ item: doc.toObject() });
112
+ } catch (err) {
113
+ const safe = toSafeJsonError(err);
114
+ return res.status(safe.status).json(safe.body);
115
+ }
116
+ };
117
+
118
+ exports.update = async (req, res) => {
119
+ try {
120
+ const id = req.params.id;
121
+ const p = req.body || {};
122
+
123
+ const doc = await Experiment.findById(id);
124
+ if (!doc) return res.status(404).json({ error: 'Not found' });
125
+
126
+ if (p.name !== undefined) doc.name = String(p.name || '').trim();
127
+ if (p.description !== undefined) doc.description = String(p.description || '').trim();
128
+ if (p.status !== undefined) doc.status = String(p.status);
129
+ if (p.startedAt !== undefined) doc.startedAt = p.startedAt ? new Date(p.startedAt) : null;
130
+ if (p.endsAt !== undefined) doc.endsAt = p.endsAt ? new Date(p.endsAt) : null;
131
+
132
+ if (p.variants !== undefined) {
133
+ doc.variants = (Array.isArray(p.variants) ? p.variants : []).map(normalizeVariant).filter(Boolean);
134
+ }
135
+
136
+ if (p.primaryMetric !== undefined) {
137
+ const m = normalizeMetric(p.primaryMetric);
138
+ if (!m) return res.status(400).json({ error: 'primaryMetric is required' });
139
+ doc.primaryMetric = m;
140
+ }
141
+
142
+ if (p.secondaryMetrics !== undefined) {
143
+ doc.secondaryMetrics = (Array.isArray(p.secondaryMetrics) ? p.secondaryMetrics : []).map(normalizeMetric).filter(Boolean);
144
+ }
145
+
146
+ if (p.winnerPolicy !== undefined) {
147
+ doc.winnerPolicy = {
148
+ ...(doc.winnerPolicy?.toObject ? doc.winnerPolicy.toObject() : doc.winnerPolicy),
149
+ ...(p.winnerPolicy || {}),
150
+ };
151
+ }
152
+
153
+ doc.updatedByUserId = req.user?._id || null;
154
+
155
+ await doc.save();
156
+ await experimentsService.clearExperimentCaches(doc._id);
157
+
158
+ return res.json({ item: doc.toObject() });
159
+ } catch (err) {
160
+ const safe = toSafeJsonError(err);
161
+ return res.status(safe.status).json(safe.body);
162
+ }
163
+ };
164
+
165
+ exports.remove = async (req, res) => {
166
+ try {
167
+ const id = req.params.id;
168
+ const doc = await Experiment.findById(id);
169
+ if (!doc) return res.status(404).json({ error: 'Not found' });
170
+
171
+ await doc.deleteOne();
172
+ await experimentsService.clearExperimentCaches(id);
173
+
174
+ return res.json({ success: true });
175
+ } catch (err) {
176
+ const safe = toSafeJsonError(err);
177
+ return res.status(safe.status).json(safe.body);
178
+ }
179
+ };
180
+
181
+ exports.getMetrics = async (req, res) => {
182
+ try {
183
+ const experimentId = req.params.id;
184
+ const start = req.query.start ? new Date(req.query.start) : null;
185
+ const end = req.query.end ? new Date(req.query.end) : null;
186
+
187
+ const q = { experimentId };
188
+ if (start || end) {
189
+ q.bucketStart = {};
190
+ if (start) q.bucketStart.$gte = start;
191
+ if (end) q.bucketStart.$lte = end;
192
+ }
193
+
194
+ const buckets = await ExperimentMetricBucket.find(q).sort({ bucketStart: 1 }).lean();
195
+ return res.json({ buckets });
196
+ } catch (err) {
197
+ const safe = toSafeJsonError(err);
198
+ return res.status(safe.status).json(safe.body);
199
+ }
200
+ };
@@ -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);