@intranefr/superbackend 1.5.1 → 1.5.3

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 (73) hide show
  1. package/.env.example +10 -0
  2. package/index.js +2 -0
  3. package/manage.js +745 -0
  4. package/package.json +5 -2
  5. package/src/controllers/admin.controller.js +79 -6
  6. package/src/controllers/adminAgents.controller.js +37 -0
  7. package/src/controllers/adminExperiments.controller.js +200 -0
  8. package/src/controllers/adminLlm.controller.js +19 -0
  9. package/src/controllers/adminMarkdowns.controller.js +157 -0
  10. package/src/controllers/adminScripts.controller.js +243 -74
  11. package/src/controllers/adminTelegram.controller.js +72 -0
  12. package/src/controllers/experiments.controller.js +85 -0
  13. package/src/controllers/internalExperiments.controller.js +17 -0
  14. package/src/controllers/markdowns.controller.js +42 -0
  15. package/src/helpers/mongooseHelper.js +258 -0
  16. package/src/helpers/scriptBase.js +230 -0
  17. package/src/helpers/scriptRunner.js +335 -0
  18. package/src/middleware.js +195 -34
  19. package/src/models/Agent.js +105 -0
  20. package/src/models/AgentMessage.js +82 -0
  21. package/src/models/CacheEntry.js +1 -1
  22. package/src/models/ConsoleLog.js +1 -1
  23. package/src/models/Experiment.js +75 -0
  24. package/src/models/ExperimentAssignment.js +23 -0
  25. package/src/models/ExperimentEvent.js +26 -0
  26. package/src/models/ExperimentMetricBucket.js +30 -0
  27. package/src/models/GlobalSetting.js +1 -2
  28. package/src/models/Markdown.js +75 -0
  29. package/src/models/RateLimitCounter.js +1 -1
  30. package/src/models/ScriptDefinition.js +1 -0
  31. package/src/models/ScriptRun.js +8 -0
  32. package/src/models/TelegramBot.js +42 -0
  33. package/src/models/Webhook.js +2 -0
  34. package/src/routes/admin.routes.js +2 -0
  35. package/src/routes/adminAgents.routes.js +13 -0
  36. package/src/routes/adminConsoleManager.routes.js +1 -1
  37. package/src/routes/adminExperiments.routes.js +29 -0
  38. package/src/routes/adminLlm.routes.js +1 -0
  39. package/src/routes/adminMarkdowns.routes.js +16 -0
  40. package/src/routes/adminScripts.routes.js +4 -1
  41. package/src/routes/adminTelegram.routes.js +14 -0
  42. package/src/routes/blogInternal.routes.js +2 -2
  43. package/src/routes/experiments.routes.js +30 -0
  44. package/src/routes/internalExperiments.routes.js +15 -0
  45. package/src/routes/markdowns.routes.js +16 -0
  46. package/src/services/agent.service.js +546 -0
  47. package/src/services/agentHistory.service.js +345 -0
  48. package/src/services/agentTools.service.js +578 -0
  49. package/src/services/blogCronsBootstrap.service.js +7 -6
  50. package/src/services/consoleManager.service.js +56 -18
  51. package/src/services/consoleOverride.service.js +1 -0
  52. package/src/services/experiments.service.js +273 -0
  53. package/src/services/experimentsAggregation.service.js +308 -0
  54. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  55. package/src/services/experimentsRetention.service.js +43 -0
  56. package/src/services/experimentsWs.service.js +134 -0
  57. package/src/services/globalSettings.service.js +15 -0
  58. package/src/services/jsonConfigs.service.js +24 -12
  59. package/src/services/llm.service.js +219 -6
  60. package/src/services/markdowns.service.js +522 -0
  61. package/src/services/scriptsRunner.service.js +514 -23
  62. package/src/services/telegram.service.js +130 -0
  63. package/src/utils/rbac/rightsRegistry.js +4 -0
  64. package/views/admin-agents.ejs +273 -0
  65. package/views/admin-coolify-deploy.ejs +8 -8
  66. package/views/admin-dashboard.ejs +63 -12
  67. package/views/admin-experiments.ejs +91 -0
  68. package/views/admin-markdowns.ejs +905 -0
  69. package/views/admin-scripts.ejs +817 -6
  70. package/views/admin-telegram.ejs +269 -0
  71. package/views/partials/dashboard/nav-items.ejs +4 -0
  72. package/views/partials/dashboard/palette.ejs +5 -3
  73. package/src/middleware/internalCronAuth.js +0 -29
@@ -1,5 +1,6 @@
1
1
  const ScriptDefinition = require('../models/ScriptDefinition');
2
2
  const ScriptRun = require('../models/ScriptRun');
3
+ const { basicAuth } = require('../middleware/auth');
3
4
  const { startRun, getRunBus } = require('../services/scriptsRunner.service');
4
5
  const { logAuditSync } = require('../services/auditLogger');
5
6
 
@@ -12,19 +13,6 @@ function toSafeJsonError(error) {
12
13
  return { status: 500, body: { error: msg } };
13
14
  }
14
15
 
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
16
  function normalizeEnv(env) {
29
17
  const items = Array.isArray(env) ? env : [];
30
18
  const out = [];
@@ -37,6 +25,35 @@ function normalizeEnv(env) {
37
25
  return out;
38
26
  }
39
27
 
28
+ // Helper functions for base64 handling
29
+ function isBase64(str) {
30
+ try {
31
+ return Buffer.from(str, 'base64').toString('base64') === str;
32
+ } catch (err) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function isValidBase64(str) {
38
+ try {
39
+ Buffer.from(str, 'base64');
40
+ return true;
41
+ } catch (err) {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function decodeScriptContent(script, format) {
47
+ if (format === 'base64') {
48
+ try {
49
+ return Buffer.from(script, 'base64').toString('utf8');
50
+ } catch (err) {
51
+ throw new Error('Failed to decode base64 script content');
52
+ }
53
+ }
54
+ return script;
55
+ }
56
+
40
57
  exports.listScripts = async (req, res) => {
41
58
  try {
42
59
  const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
@@ -61,41 +78,74 @@ exports.getScript = async (req, res) => {
61
78
  exports.createScript = async (req, res) => {
62
79
  let created = null;
63
80
  try {
81
+ console.log('[createScript] Starting script creation...');
82
+ console.log('[createScript] Request body keys:', Object.keys(req.body || {}));
83
+
64
84
  const payload = req.body || {};
85
+ console.log('[createScript] Payload name:', payload.name);
86
+ console.log('[createScript] Payload type:', payload.type);
87
+ console.log('[createScript] Payload runner:', payload.runner);
88
+ console.log('[createScript] Script length:', (payload.script || '').length);
89
+ console.log('[createScript] Script format:', payload.scriptFormat);
90
+
91
+ // Handle script content encoding
92
+ let scriptContent = String(payload.script || '');
93
+ let scriptFormat = payload.scriptFormat || 'string';
94
+
95
+ // Auto-detect base64 if not specified and content looks like base64
96
+ if (scriptFormat === 'string' && isBase64(scriptContent)) {
97
+ scriptFormat = 'base64';
98
+ console.log('[createScript] Auto-detected base64 format');
99
+ }
65
100
 
101
+ // Validate base64 content if format is base64
102
+ if (scriptFormat === 'base64' && !isValidBase64(scriptContent)) {
103
+ console.log('[createScript] Invalid base64 content detected');
104
+ throw new Error('Invalid base64 script content');
105
+ }
106
+
107
+ console.log('[createScript] About to create ScriptDefinition...');
66
108
  const doc = await ScriptDefinition.create({
67
109
  name: String(payload.name || '').trim(),
68
110
  codeIdentifier: String(payload.codeIdentifier || '').trim(),
69
111
  description: String(payload.description || ''),
70
112
  type: String(payload.type || '').trim(),
71
113
  runner: String(payload.runner || '').trim(),
72
- script: String(payload.script || ''),
114
+ script: scriptContent,
115
+ scriptFormat: scriptFormat,
73
116
  defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
74
117
  env: normalizeEnv(payload.env),
75
118
  timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
76
119
  enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
77
120
  });
121
+
122
+ console.log('[createScript] ScriptDefinition created successfully');
78
123
 
79
124
  created = doc.toObject();
80
- audit(req, {
125
+ console.log('[createScript] About to create audit entry...');
126
+ console.log('[createScript] Script created successfully:', { name: created.name, id: created._id });
127
+
128
+ logAuditSync({
129
+ req,
81
130
  action: 'scripts.create',
82
131
  outcome: 'success',
83
- entityId: doc._id,
84
- before: null,
85
- after: created,
132
+ entityType: 'ScriptDefinition',
133
+ entityId: created._id,
134
+ data: { name: created.name },
86
135
  });
87
-
136
+
137
+ console.log('[createScript] About to send response...');
88
138
  res.status(201).json({ item: doc.toObject() });
139
+ console.log('[createScript] Response sent successfully');
89
140
  } 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
- });
141
+ console.log('[createScript] ERROR occurred:', err);
142
+ console.log('[createScript] ERROR stack:', err.stack);
143
+ console.log('[createScript] ERROR message:', err.message);
144
+ console.log('[createScript] ERROR code:', err.code);
145
+
146
+ console.log('[createScript] Script creation failed:', { error: err?.message || 'Operation failed' });
98
147
  const safe = toSafeJsonError(err);
148
+ console.log('[createScript] Safe error:', safe);
99
149
  res.status(safe.status).json(safe.body);
100
150
  }
101
151
  };
@@ -111,12 +161,30 @@ exports.updateScript = async (req, res) => {
111
161
 
112
162
  before = doc.toObject();
113
163
 
164
+ // Handle script content encoding
165
+ if (payload.script !== undefined) {
166
+ let scriptContent = String(payload.script || '');
167
+ let scriptFormat = payload.scriptFormat || doc.scriptFormat || 'string';
168
+
169
+ // Auto-detect base64 if not specified and content looks like base64
170
+ if (scriptFormat === 'string' && isBase64(scriptContent)) {
171
+ scriptFormat = 'base64';
172
+ }
173
+
174
+ // Validate base64 content if format is base64
175
+ if (scriptFormat === 'base64' && !isValidBase64(scriptContent)) {
176
+ throw new Error('Invalid base64 script content');
177
+ }
178
+
179
+ doc.script = scriptContent;
180
+ doc.scriptFormat = scriptFormat;
181
+ }
182
+
114
183
  if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
115
184
  if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
116
185
  if (payload.description !== undefined) doc.description = String(payload.description || '');
117
186
  if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
118
187
  if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
119
- if (payload.script !== undefined) doc.script = String(payload.script || '');
120
188
  if (payload.defaultWorkingDirectory !== undefined) {
121
189
  doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
122
190
  }
@@ -126,23 +194,11 @@ exports.updateScript = async (req, res) => {
126
194
 
127
195
  await doc.save();
128
196
  after = doc.toObject();
129
- audit(req, {
130
- action: 'scripts.update',
131
- outcome: 'success',
132
- entityId: doc._id,
133
- before,
134
- after,
135
- });
197
+ console.log('[updateScript] Script updated successfully:', { name: after.name, id: after._id });
136
198
  res.json({ item: doc.toObject() });
137
199
  } 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
- });
200
+ console.log('[updateScript] ERROR occurred:', err);
201
+ console.log('[updateScript] ERROR message:', err.message);
146
202
  const safe = toSafeJsonError(err);
147
203
  res.status(safe.status).json(safe.body);
148
204
  }
@@ -156,23 +212,11 @@ exports.deleteScript = async (req, res) => {
156
212
  before = doc.toObject();
157
213
  await doc.deleteOne();
158
214
 
159
- audit(req, {
160
- action: 'scripts.delete',
161
- outcome: 'success',
162
- entityId: doc._id,
163
- before,
164
- after: null,
165
- });
215
+ console.log('[deleteScript] Script deleted successfully:', { name: before.name, id: before._id });
166
216
  res.json({ ok: true });
167
217
  } 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
- });
218
+ console.log('[deleteScript] ERROR occurred:', err);
219
+ console.log('[deleteScript] ERROR message:', err.message);
176
220
  const safe = toSafeJsonError(err);
177
221
  res.status(safe.status).json(safe.body);
178
222
  }
@@ -189,25 +233,12 @@ exports.runScript = async (req, res) => {
189
233
 
190
234
  const runDoc = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
191
235
 
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
- });
236
+ console.log('[runScript] Script executed successfully:', { name: script.name, runId: runDoc._id });
200
237
 
201
238
  res.json({ runId: String(runDoc._id) });
202
239
  } 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
- });
240
+ console.log('[runScript] ERROR occurred:', err);
241
+ console.log('[runScript] ERROR message:', err.message);
211
242
  const safe = toSafeJsonError(err);
212
243
  res.status(safe.status).json(safe.body);
213
244
  }
@@ -318,3 +349,141 @@ exports.streamRun = async (req, res) => {
318
349
  return res.end();
319
350
  }
320
351
  };
352
+
353
+ // Get programmatic output (clean result for API consumption)
354
+ async function getProgrammaticOutput(req, res) {
355
+ try {
356
+ const { runId } = req.params;
357
+ if (!runId) {
358
+ return res.status(400).json({ error: 'runId is required' });
359
+ }
360
+
361
+ const run = await ScriptRun.findById(runId).lean();
362
+ if (!run) {
363
+ return res.status(404).json({ error: 'Script run not found' });
364
+ }
365
+
366
+ // Parse programmatic output if it's JSON
367
+ let parsedResult = null;
368
+ let isJson = false;
369
+
370
+ if (run.programmaticOutput) {
371
+ try {
372
+ parsedResult = JSON.parse(run.programmaticOutput);
373
+ isJson = true;
374
+ } catch {
375
+ // Not JSON, keep as string
376
+ parsedResult = null;
377
+ isJson = false;
378
+ }
379
+ }
380
+
381
+ res.json({
382
+ runId: run._id,
383
+ status: run.status,
384
+ exitCode: run.exitCode,
385
+ programmaticOutput: run.programmaticOutput || 'No output',
386
+ outputType: run.outputType || 'none',
387
+ isJson: isJson,
388
+ parsedResult: parsedResult,
389
+ returnResult: run.returnResult,
390
+ lastConsoleLog: run.lastConsoleLog,
391
+ createdAt: run.createdAt,
392
+ updatedAt: run.updatedAt,
393
+ startedAt: run.startedAt,
394
+ finishedAt: run.finishedAt
395
+ });
396
+ } catch (err) {
397
+ console.error('Error getting programmatic output:', err);
398
+ res.status(500).json({ error: err?.message || 'Internal server error' });
399
+ }
400
+ }
401
+
402
+ // Get full script output
403
+ async function getFullOutput(req, res) {
404
+ try {
405
+ const { runId } = req.params;
406
+ if (!runId) {
407
+ return res.status(400).json({ error: 'runId is required' });
408
+ }
409
+
410
+ const run = await ScriptRun.findById(runId).lean();
411
+ if (!run) {
412
+ return res.status(404).json({ error: 'Script run not found' });
413
+ }
414
+
415
+ res.json({
416
+ runId: run._id,
417
+ status: run.status,
418
+ exitCode: run.exitCode,
419
+ fullOutput: run.fullOutput || '',
420
+ outputSize: run.outputSize || 0,
421
+ lineCount: run.lineCount || 0,
422
+ lastOutputUpdate: run.lastOutputUpdate,
423
+ createdAt: run.createdAt,
424
+ updatedAt: run.updatedAt,
425
+ startedAt: run.startedAt,
426
+ finishedAt: run.finishedAt
427
+ });
428
+ } catch (err) {
429
+ console.error('Error getting full output:', err);
430
+ res.status(500).json({ error: err?.message || 'Internal server error' });
431
+ }
432
+ }
433
+
434
+ // Download script output as file
435
+ async function downloadOutput(req, res) {
436
+ try {
437
+ const { runId } = req.params;
438
+ if (!runId) {
439
+ return res.status(400).json({ error: 'runId is required' });
440
+ }
441
+
442
+ const run = await ScriptRun.findById(runId).lean();
443
+ if (!run) {
444
+ return res.status(404).json({ error: 'Script run not found' });
445
+ }
446
+
447
+ const filename = `script-output-${runId}-${run.createdAt.toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
448
+
449
+ res.setHeader('Content-Type', 'text/plain');
450
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
451
+
452
+ // Include metadata at the top
453
+ const metadata = [
454
+ `Script Run ID: ${runId}`,
455
+ `Status: ${run.status}`,
456
+ `Exit Code: ${run.exitCode || 'N/A'}`,
457
+ `Started: ${run.startedAt || 'N/A'}`,
458
+ `Finished: ${run.finishedAt || 'N/A'}`,
459
+ `Output Size: ${run.outputSize || 0} characters`,
460
+ `Line Count: ${run.lineCount || 0}`,
461
+ `Created: ${run.createdAt}`,
462
+ '=' .repeat(50),
463
+ ''
464
+ ].join('\n');
465
+
466
+ res.send(metadata + (run.fullOutput || run.outputTail || 'No output available'));
467
+ } catch (err) {
468
+ console.error('Error downloading output:', err);
469
+ res.status(500).json({ error: err?.message || 'Internal server error' });
470
+ }
471
+ }
472
+
473
+ exports.getFullOutput = getFullOutput;
474
+ exports.downloadOutput = downloadOutput;
475
+
476
+ module.exports = {
477
+ listScripts: exports.listScripts,
478
+ getScript: exports.getScript,
479
+ createScript: exports.createScript,
480
+ updateScript: exports.updateScript,
481
+ deleteScript: exports.deleteScript,
482
+ runScript: exports.runScript,
483
+ listRuns: exports.listRuns,
484
+ getRun: exports.getRun,
485
+ streamRunLogs: exports.streamRun,
486
+ getProgrammaticOutput: getProgrammaticOutput,
487
+ getFullOutput: exports.getFullOutput,
488
+ downloadOutput: exports.downloadOutput,
489
+ };
@@ -0,0 +1,72 @@
1
+ const TelegramBot = require('../models/TelegramBot');
2
+ const Agent = require('../models/Agent');
3
+ const telegramService = require('../services/telegram.service');
4
+
5
+ exports.listBots = async (req, res) => {
6
+ try {
7
+ const bots = await TelegramBot.find().populate('defaultAgentId', 'name').lean();
8
+ return res.json({ items: bots });
9
+ } catch (error) {
10
+ return res.status(500).json({ error: error.message });
11
+ }
12
+ };
13
+
14
+ exports.createBot = async (req, res) => {
15
+ try {
16
+ const data = { ...req.body };
17
+ if (data.defaultAgentId === '') delete data.defaultAgentId;
18
+
19
+ const bot = await TelegramBot.create(data);
20
+ if (bot.isActive) {
21
+ await telegramService.startBot(bot._id);
22
+ }
23
+ return res.json(bot);
24
+ } catch (error) {
25
+ return res.status(500).json({ error: error.message });
26
+ }
27
+ };
28
+
29
+ exports.updateBot = async (req, res) => {
30
+ try {
31
+ const data = { ...req.body };
32
+ if (data.defaultAgentId === '') data.defaultAgentId = null;
33
+
34
+ const bot = await TelegramBot.findByIdAndUpdate(req.params.id, data, { new: true });
35
+ if (bot.isActive) {
36
+ await telegramService.startBot(bot._id);
37
+ } else {
38
+ await telegramService.stopBot(bot._id);
39
+ }
40
+ return res.json(bot);
41
+ } catch (error) {
42
+ return res.status(500).json({ error: error.message });
43
+ }
44
+ };
45
+
46
+ exports.deleteBot = async (req, res) => {
47
+ try {
48
+ await telegramService.stopBot(req.params.id);
49
+ await TelegramBot.findByIdAndDelete(req.params.id);
50
+ return res.json({ success: true });
51
+ } catch (error) {
52
+ return res.status(500).json({ error: error.message });
53
+ }
54
+ };
55
+
56
+ exports.toggleBot = async (req, res) => {
57
+ try {
58
+ const bot = await TelegramBot.findById(req.params.id);
59
+ bot.isActive = !bot.isActive;
60
+ await bot.save();
61
+
62
+ if (bot.isActive) {
63
+ await telegramService.startBot(bot._id);
64
+ } else {
65
+ await telegramService.stopBot(bot._id);
66
+ }
67
+
68
+ return res.json(bot);
69
+ } catch (error) {
70
+ return res.status(500).json({ error: error.message });
71
+ }
72
+ };
@@ -0,0 +1,85 @@
1
+ const experimentsService = require('../services/experiments.service');
2
+
3
+ function toSafeJsonError(error) {
4
+ const msg = error?.message || 'Operation failed';
5
+ const code = error?.code;
6
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
7
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
8
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
9
+ return { status: 500, body: { error: msg } };
10
+ }
11
+
12
+ exports.getAssignment = async (req, res) => {
13
+ try {
14
+ const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
15
+ const subjectId = req.query.subjectId || req.body?.subjectId;
16
+
17
+ const context = req.body?.context && typeof req.body.context === 'object' ? req.body.context : {};
18
+
19
+ const { experiment, assignment } = await experimentsService.getOrCreateAssignment({
20
+ orgId,
21
+ experimentCode: req.params.code,
22
+ subjectId,
23
+ context,
24
+ });
25
+
26
+ const variant = (experiment.variants || []).find((v) => String(v?.key || '') === String(assignment.variantKey));
27
+ const config = await experimentsService.resolveVariantConfig(variant);
28
+
29
+ const { snapshot } = await experimentsService.getWinnerSnapshot({ orgId, experimentCode: req.params.code });
30
+
31
+ return res.json({
32
+ experimentCode: experiment.code,
33
+ variantKey: assignment.variantKey,
34
+ assignedAt: assignment.assignedAt,
35
+ config,
36
+ winner: {
37
+ winnerVariantKey: snapshot.winnerVariantKey,
38
+ decidedAt: snapshot.winnerDecidedAt,
39
+ reason: snapshot.winnerReason,
40
+ status: snapshot.status,
41
+ },
42
+ });
43
+ } catch (err) {
44
+ const safe = toSafeJsonError(err);
45
+ return res.status(safe.status).json(safe.body);
46
+ }
47
+ };
48
+
49
+ exports.postEvents = async (req, res) => {
50
+ try {
51
+ const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
52
+ const subjectId = req.query.subjectId || req.body?.subjectId;
53
+
54
+ const payload = req.body || {};
55
+ const events = Array.isArray(payload.events) ? payload.events : [payload];
56
+
57
+ const result = await experimentsService.ingestEvents({
58
+ orgId,
59
+ experimentCode: req.params.code,
60
+ subjectId,
61
+ events,
62
+ });
63
+
64
+ return res.status(201).json(result);
65
+ } catch (err) {
66
+ const safe = toSafeJsonError(err);
67
+ return res.status(safe.status).json(safe.body);
68
+ }
69
+ };
70
+
71
+ exports.getWinner = async (req, res) => {
72
+ try {
73
+ const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
74
+ const { snapshot } = await experimentsService.getWinnerSnapshot({ orgId, experimentCode: req.params.code });
75
+ return res.json({
76
+ status: snapshot.status,
77
+ winnerVariantKey: snapshot.winnerVariantKey,
78
+ decidedAt: snapshot.winnerDecidedAt,
79
+ reason: snapshot.winnerReason,
80
+ });
81
+ } catch (err) {
82
+ const safe = toSafeJsonError(err);
83
+ return res.status(safe.status).json(safe.body);
84
+ }
85
+ };
@@ -0,0 +1,17 @@
1
+ const experimentsAggregation = require('../services/experimentsAggregation.service');
2
+ const experimentsRetention = require('../services/experimentsRetention.service');
3
+
4
+ exports.runAggregation = async (req, res) => {
5
+ const body = req.body || {};
6
+ const bucketMs = body.bucketMs;
7
+ const start = body.start;
8
+ const end = body.end;
9
+
10
+ const data = await experimentsAggregation.runAggregationAndWinner({ bucketMs, start, end });
11
+ return res.json(data);
12
+ };
13
+
14
+ exports.runRetention = async (_req, res) => {
15
+ const data = await experimentsRetention.runRetentionCleanup();
16
+ return res.json(data);
17
+ };
@@ -0,0 +1,42 @@
1
+ const { getMarkdownByPath, searchMarkdowns } = require('../services/markdowns.service');
2
+
3
+ exports.getByPath = async (req, res) => {
4
+ try {
5
+ const { category, group_code, slug } = req.params;
6
+ // Check if JSON is requested via query or if we are on a .json route
7
+ const isJson = req.query?.json === 'true' || req.query?.json === '1' || req.path.endsWith('/json');
8
+
9
+ const doc = await getMarkdownByPath(category, group_code, slug);
10
+
11
+ if (isJson) {
12
+ return res.json({ item: doc });
13
+ }
14
+
15
+ // Serve raw markdown with correct MIME type
16
+ return res.type('text/markdown').send(doc.markdownRaw);
17
+ } catch (error) {
18
+ const code = error?.code;
19
+ if (code === 'NOT_FOUND') {
20
+ return res.status(404).json({ error: 'Markdown not found' });
21
+ }
22
+
23
+ console.error('Error fetching markdown:', error);
24
+ return res.status(500).json({ error: error?.message || 'Failed to fetch markdown' });
25
+ }
26
+ };
27
+
28
+ exports.search = async (req, res) => {
29
+ try {
30
+ const { q: query, category, group_code, limit = 50 } = req.query;
31
+
32
+ if (!query) {
33
+ return res.status(400).json({ error: 'Search query (q) is required' });
34
+ }
35
+
36
+ const results = await searchMarkdowns(query, { category, group_code, limit: Number(limit) });
37
+ return res.json({ results });
38
+ } catch (error) {
39
+ console.error('Error searching markdowns:', error);
40
+ return res.status(500).json({ error: error?.message || 'Failed to search markdowns' });
41
+ }
42
+ };