@intranefr/superbackend 1.5.1 → 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 (46) hide show
  1. package/.env.example +10 -0
  2. package/analysis-only.skill +0 -0
  3. package/package.json +2 -1
  4. package/src/controllers/admin.controller.js +68 -1
  5. package/src/controllers/adminExperiments.controller.js +200 -0
  6. package/src/controllers/adminScripts.controller.js +105 -74
  7. package/src/controllers/experiments.controller.js +85 -0
  8. package/src/controllers/internalExperiments.controller.js +17 -0
  9. package/src/helpers/mongooseHelper.js +258 -0
  10. package/src/helpers/scriptBase.js +230 -0
  11. package/src/helpers/scriptRunner.js +335 -0
  12. package/src/middleware.js +65 -11
  13. package/src/models/CacheEntry.js +1 -1
  14. package/src/models/ConsoleLog.js +1 -1
  15. package/src/models/Experiment.js +75 -0
  16. package/src/models/ExperimentAssignment.js +23 -0
  17. package/src/models/ExperimentEvent.js +26 -0
  18. package/src/models/ExperimentMetricBucket.js +30 -0
  19. package/src/models/GlobalSetting.js +1 -2
  20. package/src/models/RateLimitCounter.js +1 -1
  21. package/src/models/ScriptDefinition.js +1 -0
  22. package/src/models/Webhook.js +2 -0
  23. package/src/routes/admin.routes.js +2 -0
  24. package/src/routes/adminConsoleManager.routes.js +1 -1
  25. package/src/routes/adminExperiments.routes.js +29 -0
  26. package/src/routes/blogInternal.routes.js +2 -2
  27. package/src/routes/experiments.routes.js +30 -0
  28. package/src/routes/internalExperiments.routes.js +15 -0
  29. package/src/services/blogCronsBootstrap.service.js +7 -6
  30. package/src/services/consoleManager.service.js +56 -18
  31. package/src/services/consoleOverride.service.js +1 -0
  32. package/src/services/experiments.service.js +273 -0
  33. package/src/services/experimentsAggregation.service.js +308 -0
  34. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  35. package/src/services/experimentsRetention.service.js +43 -0
  36. package/src/services/experimentsWs.service.js +134 -0
  37. package/src/services/globalSettings.service.js +15 -0
  38. package/src/services/jsonConfigs.service.js +2 -2
  39. package/src/services/scriptsRunner.service.js +214 -14
  40. package/src/utils/rbac/rightsRegistry.js +4 -0
  41. package/views/admin-dashboard.ejs +28 -8
  42. package/views/admin-experiments.ejs +91 -0
  43. package/views/admin-scripts.ejs +596 -2
  44. package/views/partials/dashboard/nav-items.ejs +1 -0
  45. package/views/partials/dashboard/palette.ejs +5 -3
  46. package/src/middleware/internalCronAuth.js +0 -29
@@ -5,6 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Admin Scripts</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .search-highlight {
10
+ background-color: #fef3c7;
11
+ padding: 2px 4px;
12
+ border-radius: 3px;
13
+ }
14
+ </style>
8
15
  </head>
9
16
  <body class="bg-gray-50">
10
17
  <div class="max-w-7xl mx-auto px-6 py-6">
@@ -21,6 +28,434 @@
21
28
  </div>
22
29
  </div>
23
30
 
31
+ <!-- Documentation Section -->
32
+ <div class="mb-6">
33
+ <div class="bg-white border border-gray-200 rounded-lg">
34
+ <div class="p-3 border-b border-gray-200 flex items-center justify-between cursor-pointer hover:bg-gray-50" id="docs-toggle">
35
+ <div class="text-sm font-medium text-gray-800">📚 Documentation</div>
36
+ <div id="docs-chevron" class="text-gray-400 transition-transform duration-200">▼</div>
37
+ </div>
38
+ <div id="docs-content" class="hidden">
39
+ <div class="border-b border-gray-200">
40
+ <div class="px-4 py-3 flex items-center justify-between">
41
+ <nav class="flex space-x-8" aria-label="Tabs">
42
+ <button class="docs-tab py-3 px-1 border-b-2 border-blue-500 font-medium text-sm text-blue-600" data-tab="quick-start">
43
+ Quick Start
44
+ </button>
45
+ <button class="docs-tab py-3 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-tab="script-types">
46
+ Script Types
47
+ </button>
48
+ <button class="docs-tab py-3 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-tab="api-reference">
49
+ API Reference
50
+ </button>
51
+ <button class="docs-tab py-3 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-tab="examples">
52
+ Examples
53
+ </button>
54
+ </nav>
55
+ <div class="flex items-center space-x-2">
56
+ <input type="text" id="docs-search" placeholder="Search documentation..." class="text-sm border rounded px-2 py-1 w-48" />
57
+ <button id="clear-search" class="text-xs text-gray-500 hover:text-gray-700">Clear</button>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <div class="p-4">
62
+ <!-- Quick Start Tab -->
63
+ <div id="tab-quick-start" class="docs-tab-content">
64
+ <div class="prose prose-sm max-w-none">
65
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Start Guide</h3>
66
+
67
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Creating Your First Script</h4>
68
+ <ol class="list-decimal list-inside space-y-2 text-sm text-gray-600">
69
+ <li>Click the "New" button to create a new script</li>
70
+ <li>Fill in the basic information:
71
+ <ul class="list-disc list-inside ml-4 mt-1">
72
+ <li><strong>Name</strong>: Human-readable name for your script</li>
73
+ <li><strong>Code</strong>: Unique identifier (auto-normalized)</li>
74
+ <li><strong>Type</strong>: Choose between bash, node, or browser</li>
75
+ <li><strong>Runner</strong>: Execution environment (host, vm2, browser)</li>
76
+ </ul>
77
+ </li>
78
+ <li>Write your script code in the script editor</li>
79
+ <li>Configure environment variables if needed</li>
80
+ <li>Set timeout and working directory options</li>
81
+ <li>Click "Save" to save your script</li>
82
+ <li>Click "Run" to execute the script</li>
83
+ </ol>
84
+
85
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Common Use Cases</h4>
86
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
87
+ <div class="bg-gray-50 p-3 rounded">
88
+ <h5 class="font-medium text-gray-800 mb-1">🔧 System Administration</h5>
89
+ <p class="text-sm text-gray-600">Deployments, backups, log rotation, system updates</p>
90
+ </div>
91
+ <div class="bg-gray-50 p-3 rounded">
92
+ <h5 class="font-medium text-gray-800 mb-1">📊 Data Processing</h5>
93
+ <p class="text-sm text-gray-600">Database migrations, data imports, report generation</p>
94
+ </div>
95
+ <div class="bg-gray-50 p-3 rounded">
96
+ <h5 class="font-medium text-gray-800 mb-1">🔍 Health Checks</h5>
97
+ <p class="text-sm text-gray-600">Service monitoring, connectivity tests, performance checks</p>
98
+ </div>
99
+ <div class="bg-gray-50 p-3 rounded">
100
+ <h5 class="font-medium text-gray-800 mb-1">🚀 Automation</h5>
101
+ <p class="text-sm text-gray-600">Scheduled tasks, cleanup operations, notifications</p>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="bg-amber-50 border border-amber-200 rounded p-3 mt-4">
106
+ <h5 class="font-medium text-amber-800 mb-1">⚠️ Security Considerations</h5>
107
+ <ul class="list-disc list-inside text-sm text-amber-700 space-y-1">
108
+ <li>Host runner scripts have full system access</li>
109
+ <li>VM2 runner provides sandboxing but has limitations</li>
110
+ <li>Never expose sensitive data in script code</li>
111
+ <li>Use environment variables for configuration</li>
112
+ <li>Set appropriate timeouts to prevent hanging</li>
113
+ </ul>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Script Types Tab -->
119
+ <div id="tab-script-types" class="docs-tab-content hidden">
120
+ <div class="prose prose-sm max-w-none">
121
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Script Types & Runners</h3>
122
+
123
+ <div class="space-y-4">
124
+ <div class="border border-gray-200 rounded-lg p-4">
125
+ <div class="flex items-center justify-between mb-2">
126
+ <h4 class="text-md font-medium text-gray-800">🐚 Bash + Host Runner</h4>
127
+ <span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">Full System Access</span>
128
+ </div>
129
+ <p class="text-sm text-gray-600 mb-2">Execute bash scripts directly on the server host with full system access.</p>
130
+ <div class="bg-gray-900 text-gray-100 p-3 rounded text-xs font-mono mb-2 overflow-x-auto">
131
+ <pre>#!/bin/bash
132
+ echo "System uptime: $(uptime)"
133
+ echo "Disk usage:"
134
+ df -h | grep -E '^/dev/'
135
+ echo "Memory usage:"
136
+ free -h</pre>
137
+ </div>
138
+ <div class="bg-gray-50 p-2 rounded text-xs font-mono">
139
+ <div><strong>Use Case:</strong> System administration, file operations, deployments</div>
140
+ <div><strong>Security:</strong> High - full system access</div>
141
+ <div><strong>Limitations:</strong> None - can execute any bash command</div>
142
+ </div>
143
+ </div>
144
+
145
+ <div class="border border-gray-200 rounded-lg p-4">
146
+ <div class="flex items-center justify-between mb-2">
147
+ <h4 class="text-md font-medium text-gray-800">🟢 Node.js + Host Runner</h4>
148
+ <span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">Full System Access</span>
149
+ </div>
150
+ <p class="text-sm text-gray-600 mb-2">Run Node.js scripts with full access to system resources and Node.js APIs.</p>
151
+ <div class="bg-gray-900 text-gray-100 p-3 rounded text-xs font-mono mb-2 overflow-x-auto">
152
+ <pre>const fs = require('fs');
153
+ const path = require('path');
154
+
155
+ // Read package.json and analyze dependencies
156
+ const packagePath = path.join(process.cwd(), 'package.json');
157
+ const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
158
+
159
+ const depCount = Object.keys(packageData.dependencies || {}).length;
160
+ const devDepCount = Object.keys(packageData.devDependencies || {}).length;
161
+
162
+ // Output results (captured in script output)
163
+ console.log(`Dependencies: ${depCount}`);
164
+ console.log(`Dev Dependencies: ${devDepCount}`);
165
+ console.log(`Total: ${depCount + devDepCount}`);
166
+
167
+ // Output structured data as JSON for parsing
168
+ const result = {
169
+ dependencies: depCount,
170
+ devDependencies: devDepCount,
171
+ total: depCount + devDepCount,
172
+ name: packageData.name,
173
+ version: packageData.version
174
+ };
175
+
176
+ console.log(JSON.stringify(result));</pre>
177
+ </div>
178
+ <div class="bg-gray-50 p-2 rounded text-xs font-mono">
179
+ <div><strong>Use Case:</strong> Complex data processing, API integrations, database operations</div>
180
+ <div><strong>Security:</strong> High - full system and Node.js API access</div>
181
+ <div><strong>Limitations:</strong> None - can use any Node.js module/API</div>
182
+ </div>
183
+ </div>
184
+
185
+ <div class="border border-gray-200 rounded-lg p-4">
186
+ <div class="flex items-center justify-between mb-2">
187
+ <h4 class="text-md font-medium text-gray-800">🔒 Node.js + VM2 Runner</h4>
188
+ <span class="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Sandboxed</span>
189
+ </div>
190
+ <p class="text-sm text-gray-600 mb-2">Run Node.js scripts in a secure VM2 sandbox with limited API access.</p>
191
+ <div class="bg-gray-900 text-gray-100 p-3 rounded text-xs font-mono mb-2 overflow-x-auto">
192
+ <pre>// Safe data processing in sandbox
193
+ const data = [
194
+ { name: 'Alice', score: 85 },
195
+ { name: 'Bob', score: 92 },
196
+ { name: 'Charlie', score: 78 }
197
+ ];
198
+
199
+ // Calculate statistics
200
+ const total = data.reduce((sum, item) => sum + item.score, 0);
201
+ const average = total / data.length;
202
+ const topStudent = data.reduce((max, item) =>
203
+ item.score > max.score ? item : max
204
+ );
205
+
206
+ // Output human-readable results
207
+ console.log(`Average score: ${average.toFixed(2)}`);
208
+ console.log(`Top student: ${topStudent.name} (${topStudent.score})`);
209
+
210
+ // Output structured data as JSON for parsing
211
+ const result = {
212
+ average: parseFloat(average.toFixed(2)),
213
+ topStudent: {
214
+ name: topStudent.name,
215
+ score: topStudent.score
216
+ },
217
+ totalStudents: data.length,
218
+ processed: new Date().toISOString()
219
+ };
220
+
221
+ console.log(JSON.stringify(result));</pre>
222
+ </div>
223
+ <div class="bg-gray-50 p-2 rounded text-xs font-mono">
224
+ <div><strong>Use Case:</strong> User-submitted code, untrusted scripts, testing</div>
225
+ <div><strong>Security:</strong> Medium - sandboxed environment</div>
226
+ <div><strong>Limitations:</strong> No file system, network, or most Node.js APIs</div>
227
+ </div>
228
+ </div>
229
+
230
+ <div class="border border-gray-200 rounded-lg p-4">
231
+ <div class="flex items-center justify-between mb-2">
232
+ <h4 class="text-md font-medium text-gray-800">🌐 Browser Scripts</h4>
233
+ <span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Client-side Only</span>
234
+ </div>
235
+ <p class="text-sm text-gray-600 mb-2">Execute JavaScript directly in the browser for UI interactions and client-side tasks.</p>
236
+ <div class="bg-gray-900 text-gray-100 p-3 rounded text-xs font-mono mb-2 overflow-x-auto">
237
+ <pre>// UI automation and form validation
238
+ const forms = document.querySelectorAll('form');
239
+ let totalForms = forms.length;
240
+
241
+ // Count input fields across all forms
242
+ let totalInputs = 0;
243
+ forms.forEach(form => {
244
+ const inputs = form.querySelectorAll('input, select, textarea');
245
+ totalInputs += inputs.length;
246
+ });
247
+
248
+ // Check for required fields and validation
249
+ let requiredFields = 0;
250
+ let emptyRequiredFields = [];
251
+ document.querySelectorAll('[required]').forEach(field => {
252
+ requiredFields++;
253
+ if (!field.value.trim()) {
254
+ emptyRequiredFields.push({
255
+ name: field.name || field.type || 'unnamed',
256
+ type: field.type,
257
+ id: field.id
258
+ });
259
+ field.style.borderColor = '#ef4444';
260
+ }
261
+ });
262
+
263
+ console.log(`Forms found: ${totalForms}`);
264
+ console.log(`Total input fields: ${totalInputs}`);
265
+ console.log(`Required fields: ${requiredFields}`);
266
+ console.log(`Empty required fields: ${emptyRequiredFields.length}`);
267
+
268
+ // Output structured data as JSON for parsing
269
+ const result = {
270
+ forms: totalForms,
271
+ totalInputs: totalInputs,
272
+ requiredFields: requiredFields,
273
+ emptyRequiredFields: emptyRequiredFields,
274
+ validationPassed: emptyRequiredFields.length === 0,
275
+ timestamp: new Date().toISOString()
276
+ };
277
+
278
+ console.log(JSON.stringify(result));</pre>
279
+ </div>
280
+ <div class="bg-gray-50 p-2 rounded text-xs font-mono">
281
+ <div><strong>Use Case:</strong> UI automation, form manipulation, client-side validation</div>
282
+ <div><strong>Security:</strong> Low - browser sandbox</div>
283
+ <div><strong>Limitations:</strong> No server access, browser APIs only</div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ <div class="bg-blue-50 border border-blue-200 rounded p-3 mt-4">
289
+ <h5 class="font-medium text-blue-800 mb-1">💡 Recommendation</h5>
290
+ <p class="text-sm text-blue-700">Use Host Runner for trusted system scripts, VM2 for untrusted code, and Browser for UI tasks.</p>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- API Reference Tab -->
296
+ <div id="tab-api-reference" class="docs-tab-content hidden">
297
+ <div class="prose prose-sm max-w-none">
298
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">API Reference</h3>
299
+
300
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Script Management</h4>
301
+ <div class="space-y-2">
302
+ <div class="bg-gray-50 p-3 rounded">
303
+ <div class="font-mono text-sm font-medium text-gray-800">GET /api/admin/scripts</div>
304
+ <p class="text-sm text-gray-600 mt-1">List all script definitions</p>
305
+ </div>
306
+ <div class="bg-gray-50 p-3 rounded">
307
+ <div class="font-mono text-sm font-medium text-gray-800">POST /api/admin/scripts</div>
308
+ <p class="text-sm text-gray-600 mt-1">Create a new script definition</p>
309
+ </div>
310
+ <div class="bg-gray-50 p-3 rounded">
311
+ <div class="font-mono text-sm font-medium text-gray-800">GET /api/admin/scripts/:id</div>
312
+ <p class="text-sm text-gray-600 mt-1">Get a specific script definition</p>
313
+ </div>
314
+ <div class="bg-gray-50 p-3 rounded">
315
+ <div class="font-mono text-sm font-medium text-gray-800">PUT /api/admin/scripts/:id</div>
316
+ <p class="text-sm text-gray-600 mt-1">Update a script definition</p>
317
+ </div>
318
+ <div class="bg-gray-50 p-3 rounded">
319
+ <div class="font-mono text-sm font-medium text-gray-800">DELETE /api/admin/scripts/:id</div>
320
+ <p class="text-sm text-gray-600 mt-1">Delete a script definition</p>
321
+ </div>
322
+ </div>
323
+
324
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Execution Control</h4>
325
+ <div class="space-y-2">
326
+ <div class="bg-gray-50 p-3 rounded">
327
+ <div class="font-mono text-sm font-medium text-gray-800">POST /api/admin/scripts/:id/run</div>
328
+ <p class="text-sm text-gray-600 mt-1">Execute a script and return run ID</p>
329
+ </div>
330
+ <div class="bg-gray-50 p-3 rounded">
331
+ <div class="font-mono text-sm font-medium text-gray-800">GET /api/admin/scripts/runs/:runId</div>
332
+ <p class="text-sm text-gray-600 mt-1">Get execution results and status</p>
333
+ </div>
334
+ <div class="bg-gray-50 p-3 rounded">
335
+ <div class="font-mono text-sm font-medium text-gray-800">GET /api/admin/scripts/runs</div>
336
+ <p class="text-sm text-gray-600 mt-1">List all script runs (optional scriptId filter)</p>
337
+ </div>
338
+ </div>
339
+
340
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Real-time Streaming</h4>
341
+ <div class="bg-gray-50 p-3 rounded">
342
+ <div class="font-mono text-sm font-medium text-gray-800">GET /api/admin/scripts/runs/:runId/stream</div>
343
+ <p class="text-sm text-gray-600 mt-1">Server-Sent Events stream for live output</p>
344
+ <div class="mt-2 text-xs text-gray-500">
345
+ <div>Events: log, status, done, error</div>
346
+ <div>Query: ?since=N to get events after sequence N</div>
347
+ </div>
348
+ </div>
349
+
350
+ <div class="bg-green-50 border border-green-200 rounded p-3 mt-4">
351
+ <h5 class="font-medium text-green-800 mb-1">✅ Authentication</h5>
352
+ <p class="text-sm text-green-700">All endpoints require basic authentication with admin credentials.</p>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <!-- Examples Tab -->
358
+ <div id="tab-examples" class="docs-tab-content hidden">
359
+ <div class="prose prose-sm max-w-none">
360
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Programmatic Examples</h3>
361
+
362
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Direct Service Call</h4>
363
+ <div class="relative">
364
+ <button class="copy-btn absolute top-2 right-2 bg-gray-700 text-white text-xs px-2 py-1 rounded hover:bg-gray-600" data-copy="service-call">📋 Copy</button>
365
+ <div class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
366
+ <pre class="text-sm"><code id="service-call">const { startRun } = require('./services/scriptsRunner.service');
367
+ const ScriptDefinition = require('./models/ScriptDefinition');
368
+
369
+ // Get script definition
370
+ const script = await ScriptDefinition.findById(scriptId);
371
+ if (!script || !script.enabled) throw new Error('Script not found or disabled');
372
+
373
+ // Run script programmatically
374
+ const runDoc = await startRun(script, {
375
+ trigger: 'api',
376
+ meta: { actorType: 'system', customData: '...' }
377
+ });
378
+
379
+ console.log('Run ID:', runDoc._id);</code></pre>
380
+ </div>
381
+ </div>
382
+
383
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">HTTP API Call</h4>
384
+ <div class="relative">
385
+ <button class="copy-btn absolute top-2 right-2 bg-gray-700 text-white text-xs px-2 py-1 rounded hover:bg-gray-600" data-copy="http-api">📋 Copy</button>
386
+ <div class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
387
+ <pre class="text-sm"><code id="http-api">// Execute script via HTTP API
388
+ const response = await fetch('/api/admin/scripts/scriptId/run', {
389
+ method: 'POST',
390
+ headers: {
391
+ 'Authorization': 'Basic ' + btoa('username:password'),
392
+ 'Content-Type': 'application/json'
393
+ }
394
+ });
395
+ const { runId } = await response.json();
396
+
397
+ // Get results
398
+ const runResult = await fetch('/api/admin/scripts/runs/' + runId);
399
+ const run = await runResult.json();
400
+ console.log('Status:', run.status, 'Exit Code:', run.exitCode);</code></pre>
401
+ </div>
402
+ </div>
403
+
404
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Real-time Streaming</h4>
405
+ <div class="relative">
406
+ <button class="copy-btn absolute top-2 right-2 bg-gray-700 text-white text-xs px-2 py-1 rounded hover:bg-gray-600" data-copy="streaming">📋 Copy</button>
407
+ <div class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
408
+ <pre class="text-sm"><code id="streaming">const eventSource = new EventSource('/api/admin/scripts/runs/' + runId + '/stream');
409
+
410
+ eventSource.addEventListener('log', (e) => {
411
+ const data = JSON.parse(e.data);
412
+ console.log('Output:', data.line);
413
+ });
414
+
415
+ eventSource.addEventListener('status', (e) => {
416
+ const data = JSON.parse(e.data);
417
+ console.log('Status changed:', data.status);
418
+ });
419
+
420
+ eventSource.addEventListener('done', (e) => {
421
+ console.log('Execution completed');
422
+ eventSource.close();
423
+ });</code></pre>
424
+ </div>
425
+ </div>
426
+
427
+ <h4 class="text-md font-medium text-gray-800 mt-4 mb-2">Database Query</h4>
428
+ <div class="relative">
429
+ <button class="copy-btn absolute top-2 right-2 bg-gray-700 text-white text-xs px-2 py-1 rounded hover:bg-gray-600" data-copy="database-query">📋 Copy</button>
430
+ <div class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
431
+ <pre class="text-sm"><code id="database-query">const ScriptRun = require('./models/ScriptRun');
432
+
433
+ // Get run status and results
434
+ const run = await ScriptRun.findById(runId);
435
+ console.log('Status:', run.status);
436
+ console.log('Exit code:', run.exitCode);
437
+ console.log('Output tail:', run.outputTail);
438
+
439
+ // List recent runs
440
+ const recentRuns = await ScriptRun.find()
441
+ .sort({ createdAt: -1 })
442
+ .limit(10)
443
+ .populate('scriptId')
444
+ .lean();</code></pre>
445
+ </div>
446
+ </div>
447
+
448
+ <div class="bg-purple-50 border border-purple-200 rounded p-3 mt-4">
449
+ <h5 class="font-medium text-purple-800 mb-1">🔗 Integration Examples</h5>
450
+ <p class="text-sm text-purple-700">Scripts are used by Cron Scheduler, Health Checks, and can be integrated into any system component.</p>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+ </div>
456
+ </div>
457
+ </div>
458
+
24
459
  <div class="grid grid-cols-12 gap-6">
25
460
  <div class="col-span-4">
26
461
  <div class="bg-white border border-gray-200 rounded-lg">
@@ -214,6 +649,10 @@
214
649
  env.push({ key, value });
215
650
  });
216
651
 
652
+ // Encode script content as base64 to avoid JSON parsing issues
653
+ const scriptContent = qs('f-script').value;
654
+ const scriptBase64 = btoa(unescape(encodeURIComponent(scriptContent)));
655
+
217
656
  return {
218
657
  name: qs('f-name').value.trim(),
219
658
  codeIdentifier: qs('f-code').value.trim(),
@@ -224,7 +663,8 @@
224
663
  defaultWorkingDirectory: qs('f-cwd').value,
225
664
  enabled: !!qs('f-enabled').checked,
226
665
  env,
227
- script: qs('f-script').value,
666
+ script: scriptBase64,
667
+ scriptFormat: 'base64',
228
668
  };
229
669
  }
230
670
 
@@ -274,7 +714,19 @@
274
714
  qs('f-timeout').value = String(s.timeoutMs || 300000);
275
715
  qs('f-cwd').value = s.defaultWorkingDirectory || '';
276
716
  qs('f-enabled').checked = !!s.enabled;
277
- qs('f-script').value = s.script || '';
717
+
718
+ // Decode script content if it's base64 encoded
719
+ let scriptContent = s.script || '';
720
+ if (s.scriptFormat === 'base64') {
721
+ try {
722
+ scriptContent = decodeURIComponent(escape(atob(scriptContent)));
723
+ } catch (err) {
724
+ console.error('Failed to decode base64 script content:', err);
725
+ scriptContent = s.script || ''; // Fallback to raw content
726
+ }
727
+ }
728
+ qs('f-script').value = scriptContent;
729
+
278
730
  renderEnv(s.env || []);
279
731
  loadRuns();
280
732
  }
@@ -487,9 +939,151 @@
487
939
  });
488
940
  qs('f-runner').addEventListener('change', setRunnerWarning);
489
941
 
942
+ // Documentation section functionality
943
+ function initDocumentation() {
944
+ const docsToggle = qs('docs-toggle');
945
+ const docsContent = qs('docs-content');
946
+ const docsChevron = qs('docs-chevron');
947
+ const searchInput = qs('docs-search');
948
+ const clearSearchBtn = qs('clear-search');
949
+
950
+ // Load saved state from localStorage
951
+ const docsExpanded = localStorage.getItem('admin-scripts-docs-expanded') === 'true';
952
+ if (docsExpanded) {
953
+ docsContent.classList.remove('hidden');
954
+ docsChevron.style.transform = 'rotate(180deg)';
955
+ }
956
+
957
+ // Toggle documentation section
958
+ docsToggle.addEventListener('click', () => {
959
+ const isHidden = docsContent.classList.contains('hidden');
960
+ if (isHidden) {
961
+ docsContent.classList.remove('hidden');
962
+ docsChevron.style.transform = 'rotate(180deg)';
963
+ localStorage.setItem('admin-scripts-docs-expanded', 'true');
964
+ } else {
965
+ docsContent.classList.add('hidden');
966
+ docsChevron.style.transform = 'rotate(0deg)';
967
+ localStorage.setItem('admin-scripts-docs-expanded', 'false');
968
+ }
969
+ });
970
+
971
+ // Tab switching functionality
972
+ const tabs = document.querySelectorAll('.docs-tab');
973
+ const tabContents = document.querySelectorAll('.docs-tab-content');
974
+
975
+ tabs.forEach(tab => {
976
+ tab.addEventListener('click', () => {
977
+ const targetTab = tab.dataset.tab;
978
+
979
+ // Update tab styles
980
+ tabs.forEach(t => {
981
+ t.classList.remove('border-blue-500', 'text-blue-600');
982
+ t.classList.add('border-transparent', 'text-gray-500');
983
+ });
984
+ tab.classList.remove('border-transparent', 'text-gray-500');
985
+ tab.classList.add('border-blue-500', 'text-blue-600');
986
+
987
+ // Show/hide content
988
+ tabContents.forEach(content => {
989
+ if (content.id === `tab-${targetTab}`) {
990
+ content.classList.remove('hidden');
991
+ } else {
992
+ content.classList.add('hidden');
993
+ }
994
+ });
995
+ });
996
+ });
997
+
998
+ // Search functionality
999
+ function performSearch(query) {
1000
+ const searchTerm = query.toLowerCase().trim();
1001
+ const allContent = document.querySelectorAll('.docs-tab-content');
1002
+
1003
+ if (!searchTerm) {
1004
+ // Reset all content visibility
1005
+ allContent.forEach(content => {
1006
+ const elements = content.querySelectorAll('h3, h4, h5, p, li, div, pre');
1007
+ elements.forEach(el => {
1008
+ el.style.display = '';
1009
+ el.classList.remove('search-highlight');
1010
+ });
1011
+ });
1012
+ return;
1013
+ }
1014
+
1015
+ allContent.forEach(content => {
1016
+ const elements = content.querySelectorAll('h3, h4, h5, p, li, div, pre');
1017
+ let hasVisibleContent = false;
1018
+
1019
+ elements.forEach(el => {
1020
+ const text = el.textContent.toLowerCase();
1021
+ if (text.includes(searchTerm)) {
1022
+ el.style.display = '';
1023
+ el.classList.add('search-highlight');
1024
+ hasVisibleContent = true;
1025
+ } else {
1026
+ el.style.display = 'none';
1027
+ el.classList.remove('search-highlight');
1028
+ }
1029
+ });
1030
+ });
1031
+ }
1032
+
1033
+ searchInput.addEventListener('input', (e) => {
1034
+ performSearch(e.target.value);
1035
+ });
1036
+
1037
+ clearSearchBtn.addEventListener('click', () => {
1038
+ searchInput.value = '';
1039
+ performSearch('');
1040
+ });
1041
+
1042
+ // Copy to clipboard functionality
1043
+ const copyButtons = document.querySelectorAll('.copy-btn');
1044
+
1045
+ copyButtons.forEach(btn => {
1046
+ btn.addEventListener('click', async () => {
1047
+ const targetId = btn.dataset.copy;
1048
+ const targetElement = document.getElementById(targetId);
1049
+
1050
+ if (targetElement) {
1051
+ try {
1052
+ await navigator.clipboard.writeText(targetElement.textContent);
1053
+ const originalText = btn.textContent;
1054
+ btn.textContent = '✅ Copied!';
1055
+ btn.classList.add('bg-green-600');
1056
+ btn.classList.remove('bg-gray-700');
1057
+
1058
+ setTimeout(() => {
1059
+ btn.textContent = originalText;
1060
+ btn.classList.remove('bg-green-600');
1061
+ btn.classList.add('bg-gray-700');
1062
+ }, 2000);
1063
+ } catch (err) {
1064
+ // Fallback for older browsers
1065
+ const textArea = document.createElement('textarea');
1066
+ textArea.value = targetElement.textContent;
1067
+ document.body.appendChild(textArea);
1068
+ textArea.select();
1069
+ document.execCommand('copy');
1070
+ document.body.removeChild(textArea);
1071
+
1072
+ const originalText = btn.textContent;
1073
+ btn.textContent = '✅ Copied!';
1074
+ setTimeout(() => {
1075
+ btn.textContent = originalText;
1076
+ }, 2000);
1077
+ }
1078
+ }
1079
+ });
1080
+ });
1081
+ }
1082
+
490
1083
  (async function init() {
491
1084
  clearForm();
492
1085
  normalizeRunnerOptions();
1086
+ initDocumentation();
493
1087
  await loadScripts();
494
1088
  })();
495
1089
  </script>
@@ -53,6 +53,7 @@
53
53
  items: [
54
54
  { id: 'audit', label: 'Audit Logs', path: adminPath + '/audit', icon: 'ti-history' },
55
55
  { id: 'errors', label: 'Error Tracking', path: adminPath + '/errors', icon: 'ti-bug' },
56
+ { id: 'experiments', label: 'Experiments', path: adminPath + '/experiments', icon: 'ti-flask-2' },
56
57
  { id: 'console-manager', label: 'Console Manager', path: adminPath + '/console-manager', icon: 'ti-terminal-2' },
57
58
  { id: 'health-checks', label: 'Health Checks', path: adminPath + '/health-checks', icon: 'ti-heartbeat' },
58
59
  { id: 'metrics', label: 'Metrics', path: adminPath + '/metrics', icon: 'ti-chart-bar' },