@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,279 @@
1
+ const BlogPost = require('../models/BlogPost');
2
+ const {
3
+ extractExcerptFromMarkdown,
4
+ generateUniqueBlogSlug,
5
+ normalizeTags,
6
+ slugify,
7
+ parsePagination,
8
+ } = require('../services/blog.service');
9
+
10
+ function normalizeStringField(value) {
11
+ if (value === undefined) return undefined;
12
+ return String(value || '').trim();
13
+ }
14
+
15
+ exports.list = async (req, res) => {
16
+ try {
17
+ const { page, limit, skip } = parsePagination({
18
+ page: req.query.page,
19
+ limit: req.query.limit,
20
+ maxLimit: 200,
21
+ defaultLimit: 50,
22
+ });
23
+
24
+ const filter = {};
25
+
26
+ const status = String(req.query.status || '').trim();
27
+ if (status) filter.status = status;
28
+
29
+ const tag = String(req.query.tag || '').trim();
30
+ if (tag) filter.tags = tag;
31
+
32
+ const category = String(req.query.category || '').trim();
33
+ if (category) filter.category = category;
34
+
35
+ const q = String(req.query.q || '').trim();
36
+ if (q) {
37
+ const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
38
+ filter.$or = [{ title: re }, { excerpt: re }, { slug: re }];
39
+ }
40
+
41
+ const statsBaseFilter = { ...filter };
42
+ delete statsBaseFilter.status;
43
+
44
+ const [items, total, statsTotal, statsDraft, statsScheduled, statsPublished, statsArchived] = await Promise.all([
45
+ BlogPost.find(filter)
46
+ .sort({ updatedAt: -1, createdAt: -1 })
47
+ .select('title slug status excerpt category tags authorName publishedAt scheduledAt updatedAt createdAt')
48
+ .skip(skip)
49
+ .limit(limit)
50
+ .lean(),
51
+ BlogPost.countDocuments(filter),
52
+ BlogPost.countDocuments(statsBaseFilter),
53
+ BlogPost.countDocuments({ ...statsBaseFilter, status: 'draft' }),
54
+ BlogPost.countDocuments({ ...statsBaseFilter, status: 'scheduled' }),
55
+ BlogPost.countDocuments({ ...statsBaseFilter, status: 'published' }),
56
+ BlogPost.countDocuments({ ...statsBaseFilter, status: 'archived' }),
57
+ ]);
58
+
59
+ res.json({
60
+ items,
61
+ pagination: { page, limit, total, pages: Math.ceil(total / limit) },
62
+ stats: {
63
+ total: statsTotal,
64
+ draft: statsDraft,
65
+ scheduled: statsScheduled,
66
+ published: statsPublished,
67
+ archived: statsArchived,
68
+ },
69
+ });
70
+ } catch (error) {
71
+ console.error('Error listing admin blog posts:', error);
72
+ res.status(500).json({ error: 'Failed to list blog posts' });
73
+ }
74
+ };
75
+
76
+ exports.suggestions = async (req, res) => {
77
+ try {
78
+ const [categories, authorNames, tagsAgg] = await Promise.all([
79
+ BlogPost.distinct('category', { category: { $ne: '' } }),
80
+ BlogPost.distinct('authorName', { authorName: { $ne: '' } }),
81
+ BlogPost.aggregate([
82
+ { $unwind: '$tags' },
83
+ { $match: { tags: { $ne: '' } } },
84
+ { $group: { _id: '$tags' } },
85
+ { $project: { _id: 0, tag: '$_id' } },
86
+ ]),
87
+ ]);
88
+
89
+ const tags = (tagsAgg || []).map((t) => String(t.tag || '')).filter(Boolean);
90
+
91
+ res.json({
92
+ categories: (categories || []).map((x) => String(x || '')).filter(Boolean).sort(),
93
+ tags: tags.sort(),
94
+ authorNames: (authorNames || []).map((x) => String(x || '')).filter(Boolean).sort(),
95
+ });
96
+ } catch (error) {
97
+ console.error('Error building blog suggestions:', error);
98
+ res.status(500).json({ error: 'Failed to load suggestions' });
99
+ }
100
+ };
101
+
102
+ exports.create = async (req, res) => {
103
+ try {
104
+ const payload = req.body || {};
105
+ const title = normalizeStringField(payload.title);
106
+ if (!title) return res.status(400).json({ error: 'title is required' });
107
+
108
+ const markdown = String(payload.markdown || '');
109
+ if (!markdown.trim()) return res.status(400).json({ error: 'markdown is required' });
110
+
111
+ const html = String(payload.html || payload.markdown || '');
112
+ const excerpt =
113
+ normalizeStringField(payload.excerpt) || extractExcerptFromMarkdown(markdown);
114
+
115
+ const desiredSlug = normalizeStringField(payload.slug);
116
+ const slug = desiredSlug ? slugify(desiredSlug) : await generateUniqueBlogSlug(title);
117
+
118
+ const post = await BlogPost.create({
119
+ title,
120
+ slug,
121
+ status: 'draft',
122
+ excerpt,
123
+ markdown,
124
+ html,
125
+ coverImageUrl: normalizeStringField(payload.coverImageUrl) || '',
126
+ category: normalizeStringField(payload.category) || '',
127
+ tags: normalizeTags(payload.tags),
128
+ authorName: normalizeStringField(payload.authorName) || '',
129
+ seoTitle: normalizeStringField(payload.seoTitle) || '',
130
+ seoDescription: normalizeStringField(payload.seoDescription) || '',
131
+ scheduledAt: null,
132
+ publishedAt: null,
133
+ });
134
+
135
+ res.status(201).json({ item: post.toObject() });
136
+ } catch (error) {
137
+ console.error('Error creating blog post:', error);
138
+ res.status(500).json({ error: 'Failed to create blog post' });
139
+ }
140
+ };
141
+
142
+ exports.get = async (req, res) => {
143
+ try {
144
+ const post = await BlogPost.findById(req.params.id).lean();
145
+ if (!post) return res.status(404).json({ error: 'Not found' });
146
+ res.json({ item: post });
147
+ } catch (error) {
148
+ console.error('Error getting blog post:', error);
149
+ res.status(500).json({ error: 'Failed to get blog post' });
150
+ }
151
+ };
152
+
153
+ exports.update = async (req, res) => {
154
+ try {
155
+ const post = await BlogPost.findById(req.params.id);
156
+ if (!post) return res.status(404).json({ error: 'Not found' });
157
+
158
+ const payload = req.body || {};
159
+
160
+ if (payload.title !== undefined) post.title = normalizeStringField(payload.title) || '';
161
+ if (!post.title) return res.status(400).json({ error: 'title is required' });
162
+
163
+ if (payload.slug !== undefined) {
164
+ const desired = slugify(payload.slug);
165
+ post.slug = desired || (await generateUniqueBlogSlug(post.title, { excludeId: post._id }));
166
+ }
167
+ if (!post.slug) {
168
+ post.slug = await generateUniqueBlogSlug(post.title, { excludeId: post._id });
169
+ }
170
+
171
+ if (payload.markdown !== undefined) post.markdown = String(payload.markdown || '');
172
+ if (payload.html !== undefined) post.html = String(payload.html || '');
173
+
174
+ if (payload.excerpt !== undefined) {
175
+ post.excerpt = normalizeStringField(payload.excerpt) || '';
176
+ }
177
+ if (!post.excerpt) {
178
+ post.excerpt = extractExcerptFromMarkdown(post.markdown);
179
+ }
180
+
181
+ if (payload.coverImageUrl !== undefined) post.coverImageUrl = normalizeStringField(payload.coverImageUrl) || '';
182
+ if (payload.category !== undefined) post.category = normalizeStringField(payload.category) || '';
183
+ if (payload.authorName !== undefined) post.authorName = normalizeStringField(payload.authorName) || '';
184
+ if (payload.seoTitle !== undefined) post.seoTitle = normalizeStringField(payload.seoTitle) || '';
185
+ if (payload.seoDescription !== undefined) post.seoDescription = normalizeStringField(payload.seoDescription) || '';
186
+ if (payload.tags !== undefined) post.tags = normalizeTags(payload.tags);
187
+
188
+ await post.save();
189
+
190
+ res.json({ item: post.toObject() });
191
+ } catch (error) {
192
+ console.error('Error updating blog post:', error);
193
+ res.status(500).json({ error: 'Failed to update blog post' });
194
+ }
195
+ };
196
+
197
+ exports.publish = async (req, res) => {
198
+ try {
199
+ const post = await BlogPost.findById(req.params.id);
200
+ if (!post) return res.status(404).json({ error: 'Not found' });
201
+
202
+ post.status = 'published';
203
+ post.scheduledAt = null;
204
+ if (!post.publishedAt) post.publishedAt = new Date();
205
+ await post.save();
206
+
207
+ res.json({ item: post.toObject() });
208
+ } catch (error) {
209
+ console.error('Error publishing blog post:', error);
210
+ res.status(500).json({ error: 'Failed to publish blog post' });
211
+ }
212
+ };
213
+
214
+ exports.unpublish = async (req, res) => {
215
+ try {
216
+ const post = await BlogPost.findById(req.params.id);
217
+ if (!post) return res.status(404).json({ error: 'Not found' });
218
+
219
+ post.status = 'draft';
220
+ post.scheduledAt = null;
221
+ await post.save();
222
+
223
+ res.json({ item: post.toObject() });
224
+ } catch (error) {
225
+ console.error('Error unpublishing blog post:', error);
226
+ res.status(500).json({ error: 'Failed to unpublish blog post' });
227
+ }
228
+ };
229
+
230
+ exports.schedule = async (req, res) => {
231
+ try {
232
+ const post = await BlogPost.findById(req.params.id);
233
+ if (!post) return res.status(404).json({ error: 'Not found' });
234
+
235
+ const scheduledAtRaw = req.body?.scheduledAt;
236
+ const scheduledAt = scheduledAtRaw ? new Date(scheduledAtRaw) : null;
237
+ if (!scheduledAt || Number.isNaN(scheduledAt.getTime())) {
238
+ return res.status(400).json({ error: 'scheduledAt is required and must be a valid date' });
239
+ }
240
+
241
+ post.status = 'scheduled';
242
+ post.scheduledAt = scheduledAt;
243
+ await post.save();
244
+
245
+ res.json({ item: post.toObject() });
246
+ } catch (error) {
247
+ console.error('Error scheduling blog post:', error);
248
+ res.status(500).json({ error: 'Failed to schedule blog post' });
249
+ }
250
+ };
251
+
252
+ exports.archive = async (req, res) => {
253
+ try {
254
+ const post = await BlogPost.findById(req.params.id);
255
+ if (!post) return res.status(404).json({ error: 'Not found' });
256
+
257
+ post.status = 'archived';
258
+ post.scheduledAt = null;
259
+ await post.save();
260
+
261
+ res.json({ item: post.toObject() });
262
+ } catch (error) {
263
+ console.error('Error archiving blog post:', error);
264
+ res.status(500).json({ error: 'Failed to archive blog post' });
265
+ }
266
+ };
267
+
268
+ exports.remove = async (req, res) => {
269
+ try {
270
+ const post = await BlogPost.findById(req.params.id);
271
+ if (!post) return res.status(404).json({ error: 'Not found' });
272
+
273
+ await BlogPost.deleteOne({ _id: post._id });
274
+ res.json({ deleted: true });
275
+ } catch (error) {
276
+ console.error('Error deleting blog post:', error);
277
+ res.status(500).json({ error: 'Failed to delete blog post' });
278
+ }
279
+ };
@@ -0,0 +1,224 @@
1
+ const llmService = require('../services/llm.service');
2
+
3
+ function safeJsonExtract(raw) {
4
+ const text = String(raw || '').trim();
5
+ if (!text) return null;
6
+
7
+ let cleaned = text;
8
+ if (cleaned.startsWith('```')) {
9
+ cleaned = cleaned.replace(/^```[a-zA-Z]*\s*/m, '').replace(/```\s*$/m, '').trim();
10
+ }
11
+
12
+ const firstObj = cleaned.indexOf('{');
13
+ const lastObj = cleaned.lastIndexOf('}');
14
+ const firstArr = cleaned.indexOf('[');
15
+ const lastArr = cleaned.lastIndexOf(']');
16
+
17
+ let candidate = cleaned;
18
+ if (firstObj !== -1 && lastObj !== -1 && lastObj > firstObj) {
19
+ candidate = cleaned.slice(firstObj, lastObj + 1);
20
+ } else if (firstArr !== -1 && lastArr !== -1 && lastArr > firstArr) {
21
+ candidate = cleaned.slice(firstArr, lastArr + 1);
22
+ }
23
+
24
+ try {
25
+ return JSON.parse(candidate);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ exports.generateField = async (req, res) => {
32
+ try {
33
+ const { field, context, providerKey, model } = req.body || {};
34
+ const f = String(field || '').trim();
35
+ if (!f) return res.status(400).json({ error: 'field is required' });
36
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
37
+
38
+ const ctx = context && typeof context === 'object' ? context : {};
39
+
40
+ const system =
41
+ 'You are an expert blog editor. You write clear, concise, SEO-friendly copy. ' +
42
+ 'Return ONLY the requested output with no extra commentary.';
43
+
44
+ const instructionsByField = {
45
+ title: 'Generate a compelling blog post title. Keep it SEO-friendly. Max 70 characters. Return only the title.',
46
+ excerpt: 'Generate a short excerpt (1-2 sentences). SEO-friendly, no markdown, no quotes. Return only the excerpt.',
47
+ category: 'Suggest a single category (2-4 words). Return only the category.',
48
+ tags: 'Generate 5-10 relevant tags. Return as a comma-separated list, lowercase where appropriate.',
49
+ seoTitle: 'Generate an SEO title. Max 60 characters. Return only the SEO title.',
50
+ seoDescription: 'Generate an SEO meta description. Max 155 characters. Return only the description.',
51
+ };
52
+
53
+ const instruction =
54
+ instructionsByField[f] || `Generate a value for field "${f}". Return only the value.`;
55
+
56
+ const result = await llmService.callAdhoc(
57
+ {
58
+ providerKey,
59
+ model,
60
+ promptKeyForAudit: `blog.ai.generate.${f}`,
61
+ messages: [
62
+ { role: 'system', content: system },
63
+ { role: 'user', content: instruction + '\n\nContext (JSON):\n' + JSON.stringify(ctx, null, 2) },
64
+ ],
65
+ },
66
+ { temperature: 0.6, max_tokens: 256 },
67
+ );
68
+
69
+ res.json({ value: String(result?.content || '').trim(), usage: result?.usage || null });
70
+ } catch (error) {
71
+ console.error('[blog-ai] generate-field error', error);
72
+ res.status(500).json({ error: error.message || 'Failed to generate field' });
73
+ }
74
+ };
75
+
76
+ exports.generateAll = async (req, res) => {
77
+ try {
78
+ const { context, providerKey, model } = req.body || {};
79
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
80
+
81
+ const ctx = context && typeof context === 'object' ? context : {};
82
+
83
+ const system = 'Return ONLY a valid JSON object, no markdown fences.';
84
+
85
+ const user =
86
+ 'Generate blog metadata fields based on the blog content and context.\n' +
87
+ 'Return a JSON object with keys: title, excerpt, category, tags, seoTitle, seoDescription.\n' +
88
+ 'tags must be an array of strings.\n\n' +
89
+ 'Context (JSON):\n' +
90
+ JSON.stringify(ctx, null, 2);
91
+
92
+ const result = await llmService.callAdhoc(
93
+ {
94
+ providerKey,
95
+ model,
96
+ promptKeyForAudit: 'blog.ai.generate_all',
97
+ messages: [
98
+ { role: 'system', content: system },
99
+ { role: 'user', content: user },
100
+ ],
101
+ },
102
+ { temperature: 0.6, max_tokens: 600 },
103
+ );
104
+
105
+ const parsed = safeJsonExtract(result?.content) || null;
106
+ if (!parsed || typeof parsed !== 'object') {
107
+ return res.status(500).json({ error: 'AI returned invalid JSON', raw: String(result?.content || '') });
108
+ }
109
+
110
+ if (typeof parsed.tags === 'string') {
111
+ parsed.tags = parsed.tags
112
+ .split(',')
113
+ .map((t) => String(t || '').trim())
114
+ .filter(Boolean);
115
+ }
116
+ if (parsed.tags !== undefined && !Array.isArray(parsed.tags)) {
117
+ parsed.tags = [];
118
+ }
119
+
120
+ res.json({ values: parsed, usage: result?.usage || null });
121
+ } catch (error) {
122
+ console.error('[blog-ai] generate-all error', error);
123
+ res.status(500).json({ error: error.message || 'Failed to generate all' });
124
+ }
125
+ };
126
+
127
+ exports.formatMarkdown = async (req, res) => {
128
+ try {
129
+ const { text, context, providerKey, model } = req.body || {};
130
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
131
+
132
+ const input = String(text || '');
133
+ if (!input.trim()) return res.status(400).json({ error: 'text is required' });
134
+
135
+ const ctx = context && typeof context === 'object' ? context : {};
136
+
137
+ const system = 'You are an expert blog editor. Output only markdown. Do not wrap in code fences.';
138
+
139
+ const user =
140
+ 'Convert the following content into clean, well-structured markdown with headings, lists, and short paragraphs.\n' +
141
+ 'Preserve meaning, improve readability.\n\n' +
142
+ 'Context (JSON):\n' +
143
+ JSON.stringify(ctx, null, 2) +
144
+ '\n\nInput:\n' +
145
+ input;
146
+
147
+ const result = await llmService.callAdhoc(
148
+ {
149
+ providerKey,
150
+ model,
151
+ promptKeyForAudit: 'blog.ai.format_markdown',
152
+ messages: [
153
+ { role: 'system', content: system },
154
+ { role: 'user', content: user },
155
+ ],
156
+ },
157
+ { temperature: 0.4, max_tokens: 2000 },
158
+ );
159
+
160
+ res.json({ markdown: String(result?.content || ''), usage: result?.usage || null });
161
+ } catch (error) {
162
+ console.error('[blog-ai] format-markdown error', error);
163
+ res.status(500).json({ error: error.message || 'Failed to format markdown' });
164
+ }
165
+ };
166
+
167
+ exports.refineMarkdown = async (req, res) => {
168
+ try {
169
+ const { markdown, instruction, selectionStart, selectionEnd, context, providerKey, model } = req.body || {};
170
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
171
+
172
+ const md = String(markdown || '');
173
+ if (!md.trim()) return res.status(400).json({ error: 'markdown is required' });
174
+
175
+ const instr = String(instruction || '').trim();
176
+ if (!instr) return res.status(400).json({ error: 'instruction is required' });
177
+
178
+ const ctx = context && typeof context === 'object' ? context : {};
179
+
180
+ const start = Number(selectionStart);
181
+ const end = Number(selectionEnd);
182
+ const hasSelection = Number.isFinite(start) && Number.isFinite(end) && end > start;
183
+ const selected = hasSelection ? md.slice(start, end) : '';
184
+
185
+ const system = 'You are an expert blog editor. Output only markdown. Do not wrap in code fences.';
186
+
187
+ const user = hasSelection
188
+ ? 'Refine ONLY the selected markdown according to the instruction. Return ONLY the replacement markdown for the selection.\n\n' +
189
+ 'Instruction:\n' + instr +
190
+ '\n\nContext (JSON):\n' + JSON.stringify(ctx, null, 2) +
191
+ '\n\nSelected markdown:\n' + selected
192
+ : 'Refine the full markdown according to the instruction. Return ONLY the full updated markdown.\n\n' +
193
+ 'Instruction:\n' + instr +
194
+ '\n\nContext (JSON):\n' + JSON.stringify(ctx, null, 2) +
195
+ '\n\nMarkdown:\n' + md;
196
+
197
+ const result = await llmService.callAdhoc(
198
+ {
199
+ providerKey,
200
+ model,
201
+ promptKeyForAudit: hasSelection ? 'blog.ai.refine.selection' : 'blog.ai.refine.full',
202
+ messages: [
203
+ { role: 'system', content: system },
204
+ { role: 'user', content: user },
205
+ ],
206
+ },
207
+ { temperature: 0.5, max_tokens: 2000 },
208
+ );
209
+
210
+ const replacement = String(result?.content || '');
211
+ const nextMarkdown = hasSelection ? md.slice(0, start) + replacement + md.slice(end) : replacement;
212
+
213
+ res.json({
214
+ markdown: nextMarkdown,
215
+ replaced: hasSelection,
216
+ selectionStart: hasSelection ? start : null,
217
+ selectionEnd: hasSelection ? start + replacement.length : null,
218
+ usage: result?.usage || null,
219
+ });
220
+ } catch (error) {
221
+ console.error('[blog-ai] refine-markdown error', error);
222
+ res.status(500).json({ error: error.message || 'Failed to refine markdown' });
223
+ }
224
+ };
@@ -0,0 +1,141 @@
1
+ const blogAutomationService = require('../services/blogAutomation.service');
2
+
3
+ exports.getConfig = async (req, res) => {
4
+ res.status(400).json({
5
+ error: 'Deprecated endpoint. Use /api/admin/blog-automation/configs instead.',
6
+ });
7
+ };
8
+
9
+ exports.saveConfig = async (req, res) => {
10
+ res.status(400).json({
11
+ error: 'Deprecated endpoint. Use /api/admin/blog-automation/configs instead.',
12
+ });
13
+ };
14
+
15
+ exports.listConfigs = async (req, res) => {
16
+ try {
17
+ const configs = await blogAutomationService.getBlogAutomationConfigs();
18
+ res.json({ configs });
19
+ } catch (error) {
20
+ console.error('Error listing blog automation configs:', error);
21
+ res.status(500).json({ error: 'Failed to load configs' });
22
+ }
23
+ };
24
+
25
+ exports.previewPromptsByConfigId = async (req, res) => {
26
+ try {
27
+ const configId = String(req.params.id || '').trim();
28
+ if (!configId) return res.status(400).json({ error: 'configId is required' });
29
+ const prompts = await blogAutomationService.previewPromptsByConfigId(configId);
30
+ res.json({ prompts });
31
+ } catch (error) {
32
+ const status = Number(error?.statusCode || 500);
33
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
34
+ console.error('Error previewing blog automation prompts:', error);
35
+ res.status(500).json({ error: 'Failed to preview prompts' });
36
+ }
37
+ };
38
+
39
+ exports.getConfigById = async (req, res) => {
40
+ try {
41
+ const cfg = await blogAutomationService.getBlogAutomationConfigById(req.params.id);
42
+ res.json({ config: cfg });
43
+ } catch (error) {
44
+ const status = Number(error?.statusCode || 500);
45
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
46
+ console.error('Error getting blog automation config:', error);
47
+ res.status(500).json({ error: 'Failed to load config' });
48
+ }
49
+ };
50
+
51
+ exports.createConfig = async (req, res) => {
52
+ try {
53
+ const name = String(req.body?.name || '').trim();
54
+ const created = await blogAutomationService.createAutomationConfig({ name });
55
+ const blogCronsBootstrap = require('../services/blogCronsBootstrap.service');
56
+ await blogCronsBootstrap.bootstrap();
57
+ res.json({ config: created });
58
+ } catch (error) {
59
+ console.error('Error creating blog automation config:', error);
60
+ res.status(500).json({ error: 'Failed to create config' });
61
+ }
62
+ };
63
+
64
+ exports.updateConfigById = async (req, res) => {
65
+ try {
66
+ const config = req.body?.config;
67
+ if (!config || typeof config !== 'object') {
68
+ return res.status(400).json({ error: 'config object is required' });
69
+ }
70
+ const updated = await blogAutomationService.updateAutomationConfig(req.params.id, config);
71
+ const blogCronsBootstrap = require('../services/blogCronsBootstrap.service');
72
+ await blogCronsBootstrap.bootstrap();
73
+ res.json({ config: updated });
74
+ } catch (error) {
75
+ const status = Number(error?.statusCode || 500);
76
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
77
+ console.error('Error updating blog automation config:', error);
78
+ res.status(500).json({ error: 'Failed to update config' });
79
+ }
80
+ };
81
+
82
+ exports.deleteConfigById = async (req, res) => {
83
+ try {
84
+ await blogAutomationService.deleteAutomationConfig(req.params.id);
85
+ const blogCronsBootstrap = require('../services/blogCronsBootstrap.service');
86
+ await blogCronsBootstrap.bootstrap();
87
+ res.json({ success: true });
88
+ } catch (error) {
89
+ const status = Number(error?.statusCode || 500);
90
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
91
+ console.error('Error deleting blog automation config:', error);
92
+ res.status(500).json({ error: 'Failed to delete config' });
93
+ }
94
+ };
95
+
96
+ exports.getStyleGuide = async (req, res) => {
97
+ try {
98
+ const styleGuide = await blogAutomationService.getBlogAutomationStyleGuide();
99
+ res.json({ styleGuide });
100
+ } catch (error) {
101
+ console.error('Error getting blog automation style guide:', error);
102
+ res.status(500).json({ error: 'Failed to load style guide' });
103
+ }
104
+ };
105
+
106
+ exports.saveStyleGuide = async (req, res) => {
107
+ try {
108
+ const styleGuide = String(req.body?.styleGuide ?? '');
109
+ await blogAutomationService.updateStyleGuide(styleGuide);
110
+ res.json({ success: true });
111
+ } catch (error) {
112
+ console.error('Error saving blog automation style guide:', error);
113
+ res.status(500).json({ error: 'Failed to save style guide' });
114
+ }
115
+ };
116
+
117
+ exports.listRuns = async (req, res) => {
118
+ try {
119
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit || 30) || 30));
120
+ const configId = String(req.query.configId || '').trim();
121
+ const runs = await blogAutomationService.listRuns({ limit, configId });
122
+ res.json({ runs });
123
+ } catch (error) {
124
+ console.error('Error listing blog automation runs:', error);
125
+ res.status(500).json({ error: 'Failed to load runs' });
126
+ }
127
+ };
128
+
129
+ exports.runNow = async (req, res) => {
130
+ try {
131
+ const configId = String(req.body?.configId || '').trim();
132
+ if (!configId) return res.status(400).json({ error: 'configId is required' });
133
+ const run = await blogAutomationService.runBlogAutomation({ trigger: 'manual', configId });
134
+ res.json({ run });
135
+ } catch (error) {
136
+ const status = Number(error?.statusCode || 500);
137
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
138
+ console.error('Error starting blog automation run:', error);
139
+ res.status(500).json({ error: 'Failed to run automation' });
140
+ }
141
+ };
@@ -0,0 +1,26 @@
1
+ const blogAutomationService = require('../services/blogAutomation.service');
2
+ const blogPublishingService = require('../services/blogPublishing.service');
3
+
4
+ exports.runAutomation = async (req, res) => {
5
+ try {
6
+ const trigger = req.body?.trigger === 'scheduled' ? 'scheduled' : 'manual';
7
+ const configId = String(req.body?.configId || '').trim();
8
+ if (!configId) return res.status(400).json({ error: 'configId is required' });
9
+ const run = await blogAutomationService.runBlogAutomation({ trigger, configId });
10
+ res.json({ run });
11
+ } catch (error) {
12
+ console.error('internal automation run error:', error);
13
+ res.status(500).json({ error: 'Failed to run automation' });
14
+ }
15
+ };
16
+
17
+ exports.publishScheduled = async (req, res) => {
18
+ try {
19
+ const limit = req.body?.limit;
20
+ const result = await blogPublishingService.publishScheduledDue({ limit });
21
+ res.json({ result });
22
+ } catch (error) {
23
+ console.error('internal publish scheduled error:', error);
24
+ res.status(500).json({ error: 'Failed to publish scheduled posts' });
25
+ }
26
+ };