@intranefr/superbackend 1.4.4 → 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 (195) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +39 -1
  4. package/package.json +11 -3
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/ui-components/browser/src/index.js +228 -0
  7. package/src/admin/endpointRegistry.js +120 -0
  8. package/src/controllers/admin.controller.js +111 -5
  9. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  10. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  11. package/src/controllers/adminCache.controller.js +342 -0
  12. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  13. package/src/controllers/adminCrons.controller.js +388 -0
  14. package/src/controllers/adminDbBrowser.controller.js +124 -0
  15. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  16. package/src/controllers/adminHeadless.controller.js +91 -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 +320 -0
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/adminTerminals.controller.js +39 -0
  28. package/src/controllers/adminUiComponents.controller.js +315 -0
  29. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  30. package/src/controllers/blogAdmin.controller.js +279 -0
  31. package/src/controllers/blogAiAdmin.controller.js +224 -0
  32. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  33. package/src/controllers/blogInternal.controller.js +26 -0
  34. package/src/controllers/blogPublic.controller.js +89 -0
  35. package/src/controllers/fileManager.controller.js +190 -0
  36. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  37. package/src/controllers/healthChecksPublic.controller.js +196 -0
  38. package/src/controllers/metrics.controller.js +64 -4
  39. package/src/controllers/orgAdmin.controller.js +366 -0
  40. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  41. package/src/middleware/auth.js +7 -0
  42. package/src/middleware/internalCronAuth.js +29 -0
  43. package/src/middleware/rbac.js +62 -0
  44. package/src/middleware.js +879 -56
  45. package/src/models/BlockDefinition.js +27 -0
  46. package/src/models/BlogAutomationLock.js +14 -0
  47. package/src/models/BlogAutomationRun.js +39 -0
  48. package/src/models/BlogPost.js +42 -0
  49. package/src/models/CacheEntry.js +26 -0
  50. package/src/models/ConsoleEntry.js +32 -0
  51. package/src/models/ConsoleLog.js +23 -0
  52. package/src/models/ContextBlockDefinition.js +33 -0
  53. package/src/models/CronExecution.js +47 -0
  54. package/src/models/CronJob.js +70 -0
  55. package/src/models/ExternalDbConnection.js +49 -0
  56. package/src/models/FileEntry.js +22 -0
  57. package/src/models/HeadlessModelDefinition.js +10 -0
  58. package/src/models/HealthAutoHealAttempt.js +57 -0
  59. package/src/models/HealthCheck.js +132 -0
  60. package/src/models/HealthCheckRun.js +51 -0
  61. package/src/models/HealthIncident.js +49 -0
  62. package/src/models/Page.js +95 -0
  63. package/src/models/PageCollection.js +42 -0
  64. package/src/models/ProxyEntry.js +66 -0
  65. package/src/models/RateLimitCounter.js +19 -0
  66. package/src/models/RateLimitMetricBucket.js +20 -0
  67. package/src/models/RbacGrant.js +25 -0
  68. package/src/models/RbacGroup.js +16 -0
  69. package/src/models/RbacGroupMember.js +13 -0
  70. package/src/models/RbacGroupRole.js +13 -0
  71. package/src/models/RbacRole.js +25 -0
  72. package/src/models/RbacUserRole.js +13 -0
  73. package/src/models/ScriptDefinition.js +42 -0
  74. package/src/models/ScriptRun.js +22 -0
  75. package/src/models/UiComponent.js +29 -0
  76. package/src/models/UiComponentProject.js +26 -0
  77. package/src/models/UiComponentProjectComponent.js +18 -0
  78. package/src/routes/admin.routes.js +1 -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/adminHeadless.routes.js +8 -1
  88. package/src/routes/adminHealthChecks.routes.js +28 -0
  89. package/src/routes/adminI18n.routes.js +4 -3
  90. package/src/routes/adminLlm.routes.js +4 -2
  91. package/src/routes/adminPages.routes.js +55 -0
  92. package/src/routes/adminProxy.routes.js +15 -0
  93. package/src/routes/adminRateLimits.routes.js +17 -0
  94. package/src/routes/adminRbac.routes.js +38 -0
  95. package/src/routes/adminScripts.routes.js +21 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminTerminals.routes.js +13 -0
  98. package/src/routes/adminUiComponents.routes.js +30 -0
  99. package/src/routes/blogInternal.routes.js +14 -0
  100. package/src/routes/blogPublic.routes.js +9 -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/log.routes.js +43 -60
  105. package/src/routes/metrics.routes.js +4 -2
  106. package/src/routes/orgAdmin.routes.js +6 -0
  107. package/src/routes/pages.routes.js +123 -0
  108. package/src/routes/proxy.routes.js +46 -0
  109. package/src/routes/rbac.routes.js +47 -0
  110. package/src/routes/uiComponentsPublic.routes.js +9 -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 +184 -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 +700 -0
  120. package/src/services/consoleOverride.service.js +6 -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/fileManager.service.js +475 -0
  125. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  126. package/src/services/headlessExternalModels.service.js +292 -0
  127. package/src/services/headlessModels.service.js +26 -6
  128. package/src/services/healthChecks.service.js +650 -0
  129. package/src/services/healthChecksBootstrap.service.js +109 -0
  130. package/src/services/healthChecksScheduler.service.js +106 -0
  131. package/src/services/llmDefaults.service.js +190 -0
  132. package/src/services/migrationAssets/s3.js +2 -2
  133. package/src/services/pages.service.js +602 -0
  134. package/src/services/pagesContext.service.js +331 -0
  135. package/src/services/pagesContextBlocksAi.service.js +349 -0
  136. package/src/services/proxy.service.js +535 -0
  137. package/src/services/rateLimiter.service.js +623 -0
  138. package/src/services/rbac.service.js +212 -0
  139. package/src/services/scriptsRunner.service.js +259 -0
  140. package/src/services/terminals.service.js +152 -0
  141. package/src/services/terminalsWs.service.js +100 -0
  142. package/src/services/uiComponentsAi.service.js +299 -0
  143. package/src/services/uiComponentsCrypto.service.js +39 -0
  144. package/src/services/workflow.service.js +23 -8
  145. package/src/utils/orgRoles.js +14 -0
  146. package/src/utils/rbac/engine.js +60 -0
  147. package/src/utils/rbac/rightsRegistry.js +29 -0
  148. package/views/admin-blog-automation.ejs +877 -0
  149. package/views/admin-blog-edit.ejs +542 -0
  150. package/views/admin-blog.ejs +399 -0
  151. package/views/admin-cache.ejs +681 -0
  152. package/views/admin-console-manager.ejs +680 -0
  153. package/views/admin-crons.ejs +645 -0
  154. package/views/admin-db-browser.ejs +445 -0
  155. package/views/admin-ejs-virtual.ejs +16 -10
  156. package/views/admin-file-manager.ejs +942 -0
  157. package/views/admin-headless.ejs +294 -24
  158. package/views/admin-health-checks.ejs +725 -0
  159. package/views/admin-i18n.ejs +59 -5
  160. package/views/admin-llm.ejs +99 -1
  161. package/views/admin-organizations.ejs +528 -10
  162. package/views/admin-pages.ejs +2424 -0
  163. package/views/admin-proxy.ejs +491 -0
  164. package/views/admin-rate-limiter.ejs +625 -0
  165. package/views/admin-rbac.ejs +1331 -0
  166. package/views/admin-scripts.ejs +497 -0
  167. package/views/admin-seo-config.ejs +61 -7
  168. package/views/admin-terminals.ejs +328 -0
  169. package/views/admin-ui-components.ejs +741 -0
  170. package/views/admin-users.ejs +261 -4
  171. package/views/admin-workflows.ejs +7 -7
  172. package/views/file-manager.ejs +866 -0
  173. package/views/pages/blocks/contact.ejs +27 -0
  174. package/views/pages/blocks/cta.ejs +18 -0
  175. package/views/pages/blocks/faq.ejs +20 -0
  176. package/views/pages/blocks/features.ejs +19 -0
  177. package/views/pages/blocks/hero.ejs +13 -0
  178. package/views/pages/blocks/html.ejs +5 -0
  179. package/views/pages/blocks/image.ejs +14 -0
  180. package/views/pages/blocks/testimonials.ejs +26 -0
  181. package/views/pages/blocks/text.ejs +10 -0
  182. package/views/pages/layouts/default.ejs +51 -0
  183. package/views/pages/layouts/minimal.ejs +42 -0
  184. package/views/pages/layouts/sidebar.ejs +54 -0
  185. package/views/pages/partials/footer.ejs +13 -0
  186. package/views/pages/partials/header.ejs +12 -0
  187. package/views/pages/partials/sidebar.ejs +8 -0
  188. package/views/pages/runtime/page.ejs +10 -0
  189. package/views/pages/templates/article.ejs +20 -0
  190. package/views/pages/templates/default.ejs +12 -0
  191. package/views/pages/templates/landing.ejs +14 -0
  192. package/views/pages/templates/listing.ejs +15 -0
  193. package/views/partials/admin-image-upload-modal.ejs +221 -0
  194. package/views/partials/dashboard/nav-items.ejs +14 -0
  195. package/views/partials/llm-provider-model-picker.ejs +183 -0
@@ -0,0 +1,320 @@
1
+ const ScriptDefinition = require('../models/ScriptDefinition');
2
+ const ScriptRun = require('../models/ScriptRun');
3
+ const { startRun, getRunBus } = require('../services/scriptsRunner.service');
4
+ const { logAuditSync } = require('../services/auditLogger');
5
+
6
+ function toSafeJsonError(error) {
7
+ const msg = error?.message || 'Operation failed';
8
+ const code = error?.code;
9
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
10
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
11
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
12
+ return { status: 500, body: { error: msg } };
13
+ }
14
+
15
+ function audit(req, event) {
16
+ logAuditSync({
17
+ req,
18
+ action: event.action,
19
+ outcome: event.outcome,
20
+ entityType: 'ScriptDefinition',
21
+ entityId: event.entityId ? String(event.entityId) : null,
22
+ before: event.before || null,
23
+ after: event.after || null,
24
+ details: event.details || undefined,
25
+ });
26
+ }
27
+
28
+ function normalizeEnv(env) {
29
+ const items = Array.isArray(env) ? env : [];
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
+ exports.listScripts = async (req, res) => {
41
+ try {
42
+ const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
43
+ res.json({ items });
44
+ } catch (err) {
45
+ const safe = toSafeJsonError(err);
46
+ res.status(safe.status).json(safe.body);
47
+ }
48
+ };
49
+
50
+ exports.getScript = async (req, res) => {
51
+ try {
52
+ const doc = await ScriptDefinition.findById(req.params.id).lean();
53
+ if (!doc) return res.status(404).json({ error: 'Not found' });
54
+ res.json({ item: doc });
55
+ } catch (err) {
56
+ const safe = toSafeJsonError(err);
57
+ res.status(safe.status).json(safe.body);
58
+ }
59
+ };
60
+
61
+ exports.createScript = async (req, res) => {
62
+ let created = null;
63
+ try {
64
+ const payload = req.body || {};
65
+
66
+ const doc = await ScriptDefinition.create({
67
+ name: String(payload.name || '').trim(),
68
+ codeIdentifier: String(payload.codeIdentifier || '').trim(),
69
+ description: String(payload.description || ''),
70
+ type: String(payload.type || '').trim(),
71
+ runner: String(payload.runner || '').trim(),
72
+ script: String(payload.script || ''),
73
+ defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
74
+ env: normalizeEnv(payload.env),
75
+ timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
76
+ enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
77
+ });
78
+
79
+ created = doc.toObject();
80
+ audit(req, {
81
+ action: 'scripts.create',
82
+ outcome: 'success',
83
+ entityId: doc._id,
84
+ before: null,
85
+ after: created,
86
+ });
87
+
88
+ res.status(201).json({ item: doc.toObject() });
89
+ } catch (err) {
90
+ audit(req, {
91
+ action: 'scripts.create',
92
+ outcome: 'failure',
93
+ entityId: created?._id,
94
+ before: null,
95
+ after: created,
96
+ details: { error: err?.message || 'Operation failed' },
97
+ });
98
+ const safe = toSafeJsonError(err);
99
+ res.status(safe.status).json(safe.body);
100
+ }
101
+ };
102
+
103
+ exports.updateScript = async (req, res) => {
104
+ let before = null;
105
+ let after = null;
106
+ try {
107
+ const payload = req.body || {};
108
+
109
+ const doc = await ScriptDefinition.findById(req.params.id);
110
+ if (!doc) return res.status(404).json({ error: 'Not found' });
111
+
112
+ before = doc.toObject();
113
+
114
+ if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
115
+ if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
116
+ if (payload.description !== undefined) doc.description = String(payload.description || '');
117
+ if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
118
+ if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
119
+ if (payload.script !== undefined) doc.script = String(payload.script || '');
120
+ if (payload.defaultWorkingDirectory !== undefined) {
121
+ doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
122
+ }
123
+ if (payload.env !== undefined) doc.env = normalizeEnv(payload.env);
124
+ if (payload.timeoutMs !== undefined) doc.timeoutMs = Number(payload.timeoutMs || 0);
125
+ if (payload.enabled !== undefined) doc.enabled = Boolean(payload.enabled);
126
+
127
+ await doc.save();
128
+ after = doc.toObject();
129
+ audit(req, {
130
+ action: 'scripts.update',
131
+ outcome: 'success',
132
+ entityId: doc._id,
133
+ before,
134
+ after,
135
+ });
136
+ res.json({ item: doc.toObject() });
137
+ } catch (err) {
138
+ audit(req, {
139
+ action: 'scripts.update',
140
+ outcome: 'failure',
141
+ entityId: req.params?.id,
142
+ before,
143
+ after,
144
+ details: { error: err?.message || 'Operation failed' },
145
+ });
146
+ const safe = toSafeJsonError(err);
147
+ res.status(safe.status).json(safe.body);
148
+ }
149
+ };
150
+
151
+ exports.deleteScript = async (req, res) => {
152
+ let before = null;
153
+ try {
154
+ const doc = await ScriptDefinition.findById(req.params.id);
155
+ if (!doc) return res.status(404).json({ error: 'Not found' });
156
+ before = doc.toObject();
157
+ await doc.deleteOne();
158
+
159
+ audit(req, {
160
+ action: 'scripts.delete',
161
+ outcome: 'success',
162
+ entityId: doc._id,
163
+ before,
164
+ after: null,
165
+ });
166
+ res.json({ ok: true });
167
+ } catch (err) {
168
+ audit(req, {
169
+ action: 'scripts.delete',
170
+ outcome: 'failure',
171
+ entityId: req.params?.id,
172
+ before,
173
+ after: null,
174
+ details: { error: err?.message || 'Operation failed' },
175
+ });
176
+ const safe = toSafeJsonError(err);
177
+ res.status(safe.status).json(safe.body);
178
+ }
179
+ };
180
+
181
+ exports.runScript = async (req, res) => {
182
+ let script = null;
183
+ try {
184
+ const doc = await ScriptDefinition.findById(req.params.id);
185
+ if (!doc) return res.status(404).json({ error: 'Not found' });
186
+ if (!doc.enabled) return res.status(400).json({ error: 'Script is disabled' });
187
+
188
+ script = doc.toObject();
189
+
190
+ const runDoc = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
191
+
192
+ audit(req, {
193
+ action: 'scripts.run',
194
+ outcome: 'success',
195
+ entityId: doc._id,
196
+ before: null,
197
+ after: null,
198
+ details: { runId: String(runDoc._id) },
199
+ });
200
+
201
+ res.json({ runId: String(runDoc._id) });
202
+ } catch (err) {
203
+ audit(req, {
204
+ action: 'scripts.run',
205
+ outcome: 'failure',
206
+ entityId: req.params?.id,
207
+ before: script,
208
+ after: null,
209
+ details: { error: err?.message || 'Operation failed' },
210
+ });
211
+ const safe = toSafeJsonError(err);
212
+ res.status(safe.status).json(safe.body);
213
+ }
214
+ };
215
+
216
+ exports.getRun = async (req, res) => {
217
+ try {
218
+ const run = await ScriptRun.findById(req.params.runId).lean();
219
+ if (!run) return res.status(404).json({ error: 'Not found' });
220
+ res.json({ item: run });
221
+ } catch (err) {
222
+ const safe = toSafeJsonError(err);
223
+ res.status(safe.status).json(safe.body);
224
+ }
225
+ };
226
+
227
+ exports.listRuns = async (req, res) => {
228
+ try {
229
+ const filter = {};
230
+ if (req.query.scriptId) filter.scriptId = req.query.scriptId;
231
+
232
+ const items = await ScriptRun.find(filter)
233
+ .sort({ createdAt: -1 })
234
+ .limit(50)
235
+ .lean();
236
+
237
+ res.json({ items });
238
+ } catch (err) {
239
+ const safe = toSafeJsonError(err);
240
+ res.status(safe.status).json(safe.body);
241
+ }
242
+ };
243
+
244
+ exports.streamRun = async (req, res) => {
245
+ try {
246
+ const runId = String(req.params.runId);
247
+
248
+ res.status(200);
249
+ res.setHeader('Content-Type', 'text/event-stream');
250
+ res.setHeader('Cache-Control', 'no-cache');
251
+ res.setHeader('Connection', 'keep-alive');
252
+
253
+ const bus = getRunBus(runId);
254
+
255
+ const since = Number(req.query.since || 0);
256
+ if (bus) {
257
+ const existing = bus.snapshot(since);
258
+ for (const e of existing) {
259
+ res.write(`event: ${e.type}\n`);
260
+ res.write(`data: ${JSON.stringify(e)}\n\n`);
261
+ }
262
+
263
+ const onEvent = (e) => {
264
+ res.write(`event: ${e.type}\n`);
265
+ res.write(`data: ${JSON.stringify(e)}\n\n`);
266
+ };
267
+ const cleanup = () => {
268
+ clearInterval(heartbeat);
269
+ bus.emitter.off('event', onEvent);
270
+ bus.emitter.off('close', onClose);
271
+ };
272
+
273
+ const onClose = () => {
274
+ cleanup();
275
+ res.end();
276
+ };
277
+
278
+ const heartbeat = setInterval(() => {
279
+ res.write(`: ping\n\n`);
280
+ }, 15000);
281
+ heartbeat.unref();
282
+
283
+ bus.emitter.on('event', onEvent);
284
+ bus.emitter.once('close', onClose);
285
+
286
+ req.on('close', () => {
287
+ cleanup();
288
+ });
289
+
290
+ return;
291
+ }
292
+
293
+ const run = await ScriptRun.findById(runId).lean();
294
+ if (!run) {
295
+ res.write(`event: error\n`);
296
+ res.write(`data: ${JSON.stringify({ error: 'Not found' })}\n\n`);
297
+ return res.end();
298
+ }
299
+
300
+ if (run.outputTail) {
301
+ res.write(`event: log\n`);
302
+ res.write(
303
+ `data: ${JSON.stringify({ seq: 1, type: 'log', ts: new Date().toISOString(), stream: 'stdout', line: run.outputTail })}\n\n`,
304
+ );
305
+ }
306
+ res.write(`event: status\n`);
307
+ res.write(
308
+ `data: ${JSON.stringify({ seq: 2, type: 'status', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
309
+ );
310
+ res.write(`event: done\n`);
311
+ res.write(
312
+ `data: ${JSON.stringify({ seq: 3, type: 'done', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
313
+ );
314
+ return res.end();
315
+ } catch (err) {
316
+ res.write(`event: error\n`);
317
+ res.write(`data: ${JSON.stringify({ error: err?.message || 'Stream error' })}\n\n`);
318
+ return res.end();
319
+ }
320
+ };
@@ -1,4 +1,3 @@
1
- const OpenAI = require('openai');
2
1
  const fs = require('fs');
3
2
  const path = require('path');
4
3
 
@@ -15,6 +14,9 @@ const {
15
14
  DEFAULT_OG_PNG_OUTPUT_PATH,
16
15
  } = require('../services/seoConfig.service');
17
16
 
17
+ const llmService = require('../services/llm.service');
18
+ const { resolveLlmProviderModel } = require('../services/llmDefaults.service');
19
+
18
20
  function handleServiceError(res, error) {
19
21
  const msg = error?.message || 'Operation failed';
20
22
  const code = error?.code;
@@ -231,6 +233,7 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
231
233
  const viewPath = String(req.body?.viewPath || '').trim();
232
234
  const routePath = validateRoutePathOrThrow(req.body?.routePath);
233
235
  const modelOverride = req.body?.model;
236
+ const providerKeyOverride = req.body?.providerKey;
234
237
 
235
238
  if (!viewPath || !viewPath.endsWith('.ejs')) {
236
239
  return res.status(400).json({ error: 'viewPath is required and must end with .ejs' });
@@ -250,12 +253,18 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
250
253
  return res.status(400).json({ error: 'view file is too large' });
251
254
  }
252
255
 
253
- const apiKey = await getSeoconfigOpenRouterApiKey();
254
- if (!apiKey) {
255
- return res.status(400).json({ error: 'AI is disabled (missing OpenRouter API key)' });
256
- }
256
+ const resolved = await resolveLlmProviderModel({
257
+ systemKey: 'seoConfig.entry.generate',
258
+ providerKey: providerKeyOverride,
259
+ model: modelOverride,
260
+ });
257
261
 
258
- const model = modelOverride || (await getSeoconfigOpenRouterModel());
262
+ const legacyApiKey = await getSeoconfigOpenRouterApiKey();
263
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
264
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
265
+ : {};
266
+
267
+ const model = resolved.model || (await getSeoconfigOpenRouterModel());
259
268
 
260
269
  const { data } = await getSeoConfigData();
261
270
  const siteName = data?.siteName || '';
@@ -263,11 +272,6 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
263
272
 
264
273
  const ejsSource = await fs.promises.readFile(abs, 'utf8');
265
274
 
266
- const client = new OpenAI({
267
- apiKey,
268
- baseURL: 'https://openrouter.ai/api/v1',
269
- });
270
-
271
275
  const prompt = buildSeoEntryPromptFromEjs({
272
276
  routePath,
273
277
  viewRelPath: viewPath,
@@ -276,15 +280,20 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
276
280
  baseUrl,
277
281
  });
278
282
 
279
- const resp = await client.chat.completions.create({
280
- model,
281
- messages: [{ role: 'user', content: prompt }],
282
- });
283
+ const resp = await llmService.callAdhoc(
284
+ {
285
+ providerKey: resolved.providerKey,
286
+ model,
287
+ messages: [{ role: 'user', content: prompt }],
288
+ promptKeyForAudit: 'seoConfig.entry.generate',
289
+ },
290
+ runtimeOptions,
291
+ );
283
292
 
284
- const out = resp.choices?.[0]?.message?.content || '';
293
+ const out = resp.content || '';
285
294
  const entry = parseAiJsonObjectOrThrow(out);
286
295
 
287
- return res.json({ routePath, entry, model });
296
+ return res.json({ routePath, entry, model, providerKey: resolved.providerKey });
288
297
  } catch (error) {
289
298
  const code = error?.code;
290
299
  if (code === 'VALIDATION') {
@@ -323,6 +332,7 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
323
332
  const routePath = validateRoutePathOrThrow(req.body?.routePath);
324
333
  const instruction = String(req.body?.instruction || '').trim();
325
334
  const modelOverride = req.body?.model;
335
+ const providerKeyOverride = req.body?.providerKey;
326
336
 
327
337
  if (!instruction) {
328
338
  return res.status(400).json({ error: 'instruction is required' });
@@ -331,12 +341,18 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
331
341
  return res.status(400).json({ error: 'instruction is too large' });
332
342
  }
333
343
 
334
- const apiKey = await getSeoconfigOpenRouterApiKey();
335
- if (!apiKey) {
336
- return res.status(400).json({ error: 'AI is disabled (missing OpenRouter API key)' });
337
- }
344
+ const resolved = await resolveLlmProviderModel({
345
+ systemKey: 'seoConfig.entry.improve',
346
+ providerKey: providerKeyOverride,
347
+ model: modelOverride,
348
+ });
338
349
 
339
- const model = modelOverride || (await getSeoconfigOpenRouterModel());
350
+ const legacyApiKey = await getSeoconfigOpenRouterApiKey();
351
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
352
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
353
+ : {};
354
+
355
+ const model = resolved.model || (await getSeoconfigOpenRouterModel());
340
356
 
341
357
  const { data } = await getSeoConfigData();
342
358
  const siteName = data?.siteName || '';
@@ -346,11 +362,6 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
346
362
  return res.status(404).json({ error: `No existing entry for ${routePath}` });
347
363
  }
348
364
 
349
- const client = new OpenAI({
350
- apiKey,
351
- baseURL: 'https://openrouter.ai/api/v1',
352
- });
353
-
354
365
  const prompt = buildSeoEntryPromptImprove({
355
366
  routePath,
356
367
  existingEntry,
@@ -359,15 +370,20 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
359
370
  baseUrl,
360
371
  });
361
372
 
362
- const resp = await client.chat.completions.create({
363
- model,
364
- messages: [{ role: 'user', content: prompt }],
365
- });
373
+ const resp = await llmService.callAdhoc(
374
+ {
375
+ providerKey: resolved.providerKey,
376
+ model,
377
+ messages: [{ role: 'user', content: prompt }],
378
+ promptKeyForAudit: 'seoConfig.entry.improve',
379
+ },
380
+ runtimeOptions,
381
+ );
366
382
 
367
- const out = resp.choices?.[0]?.message?.content || '';
383
+ const out = resp.content || '';
368
384
  const entry = parseAiJsonObjectOrThrow(out);
369
385
 
370
- return res.json({ routePath, entry, model });
386
+ return res.json({ routePath, entry, model, providerKey: resolved.providerKey });
371
387
  } catch (error) {
372
388
  const code = error?.code;
373
389
  if (code === 'VALIDATION') {
@@ -469,6 +485,7 @@ exports.aiEditSvg = async (req, res) => {
469
485
  const svgRaw = req.body?.svgRaw;
470
486
  const instruction = req.body?.instruction;
471
487
  const modelOverride = req.body?.model;
488
+ const providerKeyOverride = req.body?.providerKey;
472
489
 
473
490
  if (typeof svgRaw !== 'string' || svgRaw.trim() === '') {
474
491
  return res.status(400).json({ error: 'svgRaw is required' });
@@ -484,30 +501,36 @@ exports.aiEditSvg = async (req, res) => {
484
501
  return res.status(400).json({ error: 'instruction is too large' });
485
502
  }
486
503
 
487
- const apiKey = await getSeoconfigOpenRouterApiKey();
488
- if (!apiKey) {
489
- return res.status(400).json({ error: 'AI is disabled (missing OpenRouter API key)' });
490
- }
504
+ const resolved = await resolveLlmProviderModel({
505
+ systemKey: 'seoConfig.ogSvg.edit',
506
+ providerKey: providerKeyOverride,
507
+ model: modelOverride,
508
+ });
491
509
 
492
- const model = modelOverride || (await getSeoconfigOpenRouterModel());
510
+ const legacyApiKey = await getSeoconfigOpenRouterApiKey();
511
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
512
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
513
+ : {};
493
514
 
494
- const client = new OpenAI({
495
- apiKey,
496
- baseURL: 'https://openrouter.ai/api/v1',
497
- });
515
+ const model = resolved.model || (await getSeoconfigOpenRouterModel());
498
516
 
499
517
  const prompt = buildSvgAiPrompt({ svg: svgRaw, instruction });
500
- const resp = await client.chat.completions.create({
501
- model,
502
- messages: [{ role: 'user', content: prompt }],
503
- });
518
+ const resp = await llmService.callAdhoc(
519
+ {
520
+ providerKey: resolved.providerKey,
521
+ model,
522
+ messages: [{ role: 'user', content: prompt }],
523
+ promptKeyForAudit: 'seoConfig.ogSvg.edit',
524
+ },
525
+ runtimeOptions,
526
+ );
504
527
 
505
- const out = resp.choices?.[0]?.message?.content?.trim() || '';
528
+ const out = String(resp.content || '').trim();
506
529
  if (!out.startsWith('<svg') || !out.includes('</svg>')) {
507
530
  return res.status(500).json({ error: 'AI returned invalid SVG' });
508
531
  }
509
532
 
510
- return res.json({ svgRaw: out, model });
533
+ return res.json({ svgRaw: out, model, providerKey: resolved.providerKey });
511
534
  } catch (error) {
512
535
  console.error('Error editing SVG with AI:', error);
513
536
  return res.status(500).json({ error: error?.message || 'Failed to edit SVG' });
@@ -0,0 +1,39 @@
1
+ const {
2
+ createSession,
3
+ listSessions,
4
+ killSession,
5
+ } = require('../services/terminals.service');
6
+
7
+ function handleError(res, err) {
8
+ const msg = err?.message || 'Operation failed';
9
+ const code = err?.code;
10
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: msg });
11
+ if (code === 'LIMIT') return res.status(429).json({ error: msg });
12
+ return res.status(500).json({ error: msg });
13
+ }
14
+
15
+ exports.createSession = async (req, res) => {
16
+ try {
17
+ const { cols, rows } = req.body || {};
18
+ const result = createSession({ cols, rows });
19
+ res.json(result);
20
+ } catch (err) {
21
+ handleError(res, err);
22
+ }
23
+ };
24
+
25
+ exports.listSessions = async (req, res) => {
26
+ try {
27
+ res.json({ items: listSessions() });
28
+ } catch (err) {
29
+ handleError(res, err);
30
+ }
31
+ };
32
+
33
+ exports.killSession = async (req, res) => {
34
+ try {
35
+ res.json(killSession(req.params.sessionId));
36
+ } catch (err) {
37
+ handleError(res, err);
38
+ }
39
+ };