@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
@@ -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">
@@ -114,11 +549,44 @@
114
549
  </div>
115
550
 
116
551
  <div class="mt-4 bg-white border border-gray-200 rounded-lg">
117
- <div class="p-3 border-b border-gray-200 flex items-center justify-between">
118
- <div class="text-sm font-medium text-gray-800">Output</div>
119
- <button id="btn-clear-output" class="text-sm text-gray-600 hover:underline">Clear</button>
552
+ <div class="p-3 border-b border-gray-200">
553
+ <div class="flex items-center justify-between">
554
+ <nav class="flex space-x-4">
555
+ <button class="output-tab active" data-tab="output">Output</button>
556
+ <button class="output-tab" data-tab="full-logs">Full Console Logs</button>
557
+ </nav>
558
+ <div class="flex items-center gap-2">
559
+ <button id="btn-download-logs" class="text-sm text-gray-600 hover:underline">Download</button>
560
+ <button id="btn-clear-output" class="text-sm text-gray-600 hover:underline">Clear</button>
561
+ </div>
562
+ </div>
563
+ </div>
564
+
565
+ <!-- Output Tab (JSON Result) -->
566
+ <div id="output-tab-content" class="output-tab-content">
567
+ <pre id="output" class="p-3 text-xs font-mono whitespace-pre-wrap max-h-[40vh] overflow-auto"></pre>
568
+ </div>
569
+
570
+ <!-- Full Console Logs Tab -->
571
+ <div id="full-logs-tab-content" class="output-tab-content hidden">
572
+ <!-- Search Bar -->
573
+ <div class="p-3 border-b border-gray-200">
574
+ <div class="flex items-center gap-2">
575
+ <input type="text" id="log-search" placeholder="Search logs..." class="flex-1 px-2 py-1 border rounded text-sm">
576
+ <select id="log-filter" class="px-2 py-1 border rounded text-sm">
577
+ <option value="all">All Logs</option>
578
+ <option value="stdout">Stdout</option>
579
+ <option value="stderr">Stderr</option>
580
+ </select>
581
+ <button id="btn-auto-scroll" class="px-2 py-1 bg-gray-100 rounded text-sm">Auto-scroll</button>
582
+ </div>
583
+ </div>
584
+
585
+ <!-- Full Logs Display -->
586
+ <div class="relative">
587
+ <pre id="full-logs-content" class="p-3 text-xs font-mono whitespace-pre-wrap" style="height: 60vh; overflow: auto;"></pre>
588
+ </div>
120
589
  </div>
121
- <pre id="output" class="p-3 text-xs font-mono whitespace-pre-wrap max-h-[40vh] overflow-auto"></pre>
122
590
  </div>
123
591
  </div>
124
592
  </div>
@@ -136,6 +604,8 @@
136
604
  es: null,
137
605
  };
138
606
 
607
+ let currentRunId = null;
608
+
139
609
  function qs(id) {
140
610
  return document.getElementById(id);
141
611
  }
@@ -214,6 +684,10 @@
214
684
  env.push({ key, value });
215
685
  });
216
686
 
687
+ // Encode script content as base64 to avoid JSON parsing issues
688
+ const scriptContent = qs('f-script').value;
689
+ const scriptBase64 = btoa(unescape(encodeURIComponent(scriptContent)));
690
+
217
691
  return {
218
692
  name: qs('f-name').value.trim(),
219
693
  codeIdentifier: qs('f-code').value.trim(),
@@ -224,7 +698,8 @@
224
698
  defaultWorkingDirectory: qs('f-cwd').value,
225
699
  enabled: !!qs('f-enabled').checked,
226
700
  env,
227
- script: qs('f-script').value,
701
+ script: scriptBase64,
702
+ scriptFormat: 'base64',
228
703
  };
229
704
  }
230
705
 
@@ -274,7 +749,19 @@
274
749
  qs('f-timeout').value = String(s.timeoutMs || 300000);
275
750
  qs('f-cwd').value = s.defaultWorkingDirectory || '';
276
751
  qs('f-enabled').checked = !!s.enabled;
277
- qs('f-script').value = s.script || '';
752
+
753
+ // Decode script content if it's base64 encoded
754
+ let scriptContent = s.script || '';
755
+ if (s.scriptFormat === 'base64') {
756
+ try {
757
+ scriptContent = decodeURIComponent(escape(atob(scriptContent)));
758
+ } catch (err) {
759
+ console.error('Failed to decode base64 script content:', err);
760
+ scriptContent = s.script || ''; // Fallback to raw content
761
+ }
762
+ }
763
+ qs('f-script').value = scriptContent;
764
+
278
765
  renderEnv(s.env || []);
279
766
  loadRuns();
280
767
  }
@@ -320,8 +807,12 @@
320
807
  <div class="text-xs text-gray-500 font-mono">${String(r._id || '').slice(0, 10)} · ${String(r.createdAt || '').replace(/</g,'&lt;')}</div>
321
808
  `;
322
809
  btn.addEventListener('click', () => {
810
+ currentRunId = r._id; // Set the current run ID when clicking on a previous run
323
811
  setOutput('');
324
812
  if (r.outputTail) setOutput(r.outputTail, true);
813
+
814
+ // Load programmatic output for the selected run
815
+ loadProgrammaticOutput(r._id);
325
816
  });
326
817
  list.appendChild(btn);
327
818
  });
@@ -442,8 +933,16 @@
442
933
 
443
934
  setOutput('');
444
935
  const res = await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId) + '/run', { method: 'POST' });
936
+ currentRunId = res.runId; // Set the current run ID
445
937
  startSse(res.runId);
446
938
  await loadRuns();
939
+
940
+ // Load programmatic output after script starts
941
+ setTimeout(() => {
942
+ if (currentRunId) {
943
+ loadProgrammaticOutput(currentRunId);
944
+ }
945
+ }, 1000); // Wait a bit for the script to start producing output
447
946
  }
448
947
 
449
948
  qs('btn-refresh').addEventListener('click', loadScripts);
@@ -487,9 +986,321 @@
487
986
  });
488
987
  qs('f-runner').addEventListener('change', setRunnerWarning);
489
988
 
989
+ // Documentation section functionality
990
+ function initDocumentation() {
991
+ const docsToggle = qs('docs-toggle');
992
+ const docsContent = qs('docs-content');
993
+ const docsChevron = qs('docs-chevron');
994
+ const searchInput = qs('docs-search');
995
+ const clearSearchBtn = qs('clear-search');
996
+
997
+ // Load saved state from localStorage
998
+ const docsExpanded = localStorage.getItem('admin-scripts-docs-expanded') === 'true';
999
+ if (docsExpanded) {
1000
+ docsContent.classList.remove('hidden');
1001
+ docsChevron.style.transform = 'rotate(180deg)';
1002
+ }
1003
+
1004
+ // Toggle documentation section
1005
+ docsToggle.addEventListener('click', () => {
1006
+ const isHidden = docsContent.classList.contains('hidden');
1007
+ if (isHidden) {
1008
+ docsContent.classList.remove('hidden');
1009
+ docsChevron.style.transform = 'rotate(180deg)';
1010
+ localStorage.setItem('admin-scripts-docs-expanded', 'true');
1011
+ } else {
1012
+ docsContent.classList.add('hidden');
1013
+ docsChevron.style.transform = 'rotate(0deg)';
1014
+ localStorage.setItem('admin-scripts-docs-expanded', 'false');
1015
+ }
1016
+ });
1017
+
1018
+ // Tab switching functionality
1019
+ const tabs = document.querySelectorAll('.docs-tab');
1020
+ const tabContents = document.querySelectorAll('.docs-tab-content');
1021
+
1022
+ tabs.forEach(tab => {
1023
+ tab.addEventListener('click', () => {
1024
+ const targetTab = tab.dataset.tab;
1025
+
1026
+ // Update tab styles
1027
+ tabs.forEach(t => {
1028
+ t.classList.remove('border-blue-500', 'text-blue-600');
1029
+ t.classList.add('border-transparent', 'text-gray-500');
1030
+ });
1031
+ tab.classList.remove('border-transparent', 'text-gray-500');
1032
+ tab.classList.add('border-blue-500', 'text-blue-600');
1033
+
1034
+ // Show/hide content
1035
+ tabContents.forEach(content => {
1036
+ if (content.id === `tab-${targetTab}`) {
1037
+ content.classList.remove('hidden');
1038
+ } else {
1039
+ content.classList.add('hidden');
1040
+ }
1041
+ });
1042
+ });
1043
+ });
1044
+
1045
+ // Search functionality
1046
+ function performSearch(query) {
1047
+ const searchTerm = query.toLowerCase().trim();
1048
+ const allContent = document.querySelectorAll('.docs-tab-content');
1049
+
1050
+ if (!searchTerm) {
1051
+ // Reset all content visibility
1052
+ allContent.forEach(content => {
1053
+ const elements = content.querySelectorAll('h3, h4, h5, p, li, div, pre');
1054
+ elements.forEach(el => {
1055
+ el.style.display = '';
1056
+ el.classList.remove('search-highlight');
1057
+ });
1058
+ });
1059
+ return;
1060
+ }
1061
+
1062
+ allContent.forEach(content => {
1063
+ const elements = content.querySelectorAll('h3, h4, h5, p, li, div, pre');
1064
+ let hasVisibleContent = false;
1065
+
1066
+ elements.forEach(el => {
1067
+ const text = el.textContent.toLowerCase();
1068
+ if (text.includes(searchTerm)) {
1069
+ el.style.display = '';
1070
+ el.classList.add('search-highlight');
1071
+ hasVisibleContent = true;
1072
+ } else {
1073
+ el.style.display = 'none';
1074
+ el.classList.remove('search-highlight');
1075
+ }
1076
+ });
1077
+ });
1078
+ }
1079
+
1080
+ searchInput.addEventListener('input', (e) => {
1081
+ performSearch(e.target.value);
1082
+ });
1083
+
1084
+ clearSearchBtn.addEventListener('click', () => {
1085
+ searchInput.value = '';
1086
+ performSearch('');
1087
+ });
1088
+
1089
+ // Copy to clipboard functionality
1090
+ const copyButtons = document.querySelectorAll('.copy-btn');
1091
+
1092
+ copyButtons.forEach(btn => {
1093
+ btn.addEventListener('click', async () => {
1094
+ const targetId = btn.dataset.copy;
1095
+ const targetElement = document.getElementById(targetId);
1096
+
1097
+ if (targetElement) {
1098
+ try {
1099
+ await navigator.clipboard.writeText(targetElement.textContent);
1100
+ const originalText = btn.textContent;
1101
+ btn.textContent = '✅ Copied!';
1102
+ btn.classList.add('bg-green-600');
1103
+ btn.classList.remove('bg-gray-700');
1104
+
1105
+ setTimeout(() => {
1106
+ btn.textContent = originalText;
1107
+ btn.classList.remove('bg-green-600');
1108
+ btn.classList.add('bg-gray-700');
1109
+ }, 2000);
1110
+ } catch (err) {
1111
+ // Fallback for older browsers
1112
+ const textArea = document.createElement('textarea');
1113
+ textArea.value = targetElement.textContent;
1114
+ document.body.appendChild(textArea);
1115
+ textArea.select();
1116
+ document.execCommand('copy');
1117
+ document.body.removeChild(textArea);
1118
+
1119
+ const originalText = btn.textContent;
1120
+ btn.textContent = '✅ Copied!';
1121
+ setTimeout(() => {
1122
+ btn.textContent = originalText;
1123
+ }, 2000);
1124
+ }
1125
+ }
1126
+ });
1127
+ });
1128
+ }
1129
+
1130
+ // Tab switching functionality
1131
+ function switchOutputTab(tabName) {
1132
+ // Hide all tab contents
1133
+ document.querySelectorAll('.output-tab-content').forEach(content => {
1134
+ content.classList.add('hidden');
1135
+ });
1136
+
1137
+ // Remove active class from all tabs
1138
+ document.querySelectorAll('.output-tab').forEach(tab => {
1139
+ tab.classList.remove('active', 'border-blue-500', 'text-blue-600');
1140
+ tab.classList.add('border-transparent', 'text-gray-500');
1141
+ });
1142
+
1143
+ // Show selected tab content
1144
+ document.getElementById(`${tabName}-tab-content`).classList.remove('hidden');
1145
+
1146
+ // Activate selected tab
1147
+ const activeTab = document.querySelector(`[data-tab="${tabName}"]`);
1148
+ activeTab.classList.add('active', 'border-blue-500', 'text-blue-600');
1149
+ activeTab.classList.remove('border-transparent', 'text-gray-500');
1150
+
1151
+ // Load full logs if switching to full-logs tab
1152
+ if (tabName === 'full-logs' && currentRunId) {
1153
+ loadFullLogs(currentRunId);
1154
+ }
1155
+
1156
+ // Load programmatic output if switching to output tab
1157
+ if (tabName === 'output' && currentRunId) {
1158
+ loadProgrammaticOutput(currentRunId);
1159
+ }
1160
+ }
1161
+
1162
+ // Load programmatic output from API
1163
+ async function loadProgrammaticOutput(runId) {
1164
+ try {
1165
+ const response = await fetch(`${window.BASE_URL}/api/admin/scripts/runs/${runId}/programmatic-output`);
1166
+ const data = await response.json();
1167
+
1168
+ displayProgrammaticOutput(data);
1169
+ } catch (error) {
1170
+ console.error('Failed to load programmatic output:', error);
1171
+ document.getElementById('output').textContent = 'Error loading programmatic output';
1172
+ }
1173
+ }
1174
+
1175
+ // Display programmatic output with smart formatting
1176
+ function displayProgrammaticOutput(data) {
1177
+ const outputElement = document.getElementById('output');
1178
+
1179
+ if (data.isJson && data.parsedResult) {
1180
+ // Format JSON for clean display
1181
+ outputElement.textContent = JSON.stringify(data.parsedResult, null, 2);
1182
+ outputElement.className = 'p-3 text-xs font-mono whitespace-pre-wrap max-h-[40vh] overflow-auto json-output';
1183
+ } else {
1184
+ // Display as plain text
1185
+ outputElement.textContent = data.programmaticOutput;
1186
+ outputElement.className = 'p-3 text-xs font-mono whitespace-pre-wrap max-h-[40vh] overflow-auto';
1187
+ }
1188
+ }
1189
+
1190
+ // Load full logs from API
1191
+ async function loadFullLogs(runId) {
1192
+ try {
1193
+ const response = await fetch(`${window.BASE_URL}/api/admin/scripts/runs/${runId}/full-output`);
1194
+ const data = await response.json();
1195
+
1196
+ if (data.fullOutput) {
1197
+ displayFullLogs(data.fullOutput);
1198
+ } else {
1199
+ document.getElementById('full-logs-content').textContent = 'No full logs available';
1200
+ }
1201
+ } catch (error) {
1202
+ console.error('Failed to load full logs:', error);
1203
+ document.getElementById('full-logs-content').textContent = 'Error loading full logs';
1204
+ }
1205
+ }
1206
+
1207
+ // Display full logs with filtering
1208
+ function displayFullLogs(logs) {
1209
+ const content = document.getElementById('full-logs-content');
1210
+ const searchTerm = document.getElementById('log-search').value.toLowerCase();
1211
+ const filterType = document.getElementById('log-filter').value;
1212
+
1213
+ let filteredLogs = logs;
1214
+
1215
+ // Apply search filter
1216
+ if (searchTerm) {
1217
+ filteredLogs = logs.split('\n').filter(line =>
1218
+ line.toLowerCase().includes(searchTerm)
1219
+ ).join('\n');
1220
+ }
1221
+
1222
+ // Apply type filter
1223
+ if (filterType !== 'all') {
1224
+ const lines = filteredLogs.split('\n');
1225
+ filteredLogs = lines.filter(line => {
1226
+ // Simple heuristic to determine log type
1227
+ if (filterType === 'stderr') {
1228
+ return line.includes('❌') || line.includes('🚨') || line.includes('Error') || line.includes('error');
1229
+ }
1230
+ return true; // stdout
1231
+ }).join('\n');
1232
+ }
1233
+
1234
+ content.textContent = filteredLogs || 'No logs match the current filters';
1235
+
1236
+ // Auto-scroll if enabled
1237
+ if (document.getElementById('btn-auto-scroll').classList.contains('bg-blue-500')) {
1238
+ content.scrollTop = content.scrollHeight;
1239
+ }
1240
+ }
1241
+
1242
+ // Download logs functionality
1243
+ async function downloadLogs() {
1244
+ if (!currentRunId) {
1245
+ alert('No script run selected');
1246
+ return;
1247
+ }
1248
+
1249
+ try {
1250
+ const response = await fetch(`${window.BASE_URL}/api/admin/scripts/runs/${currentRunId}/download`);
1251
+ const blob = await response.blob();
1252
+
1253
+ const url = window.URL.createObjectURL(blob);
1254
+ const a = document.createElement('a');
1255
+ a.href = url;
1256
+ a.download = response.headers.get('Content-Disposition')?.split('filename=')[1]?.replace(/"/g, '') || 'script-output.txt';
1257
+ document.body.appendChild(a);
1258
+ a.click();
1259
+ document.body.removeChild(a);
1260
+ window.URL.revokeObjectURL(url);
1261
+ } catch (error) {
1262
+ console.error('Failed to download logs:', error);
1263
+ alert('Failed to download logs');
1264
+ }
1265
+ }
1266
+
1267
+ // Add event listeners for new functionality
1268
+ document.querySelectorAll('.output-tab').forEach(tab => {
1269
+ tab.addEventListener('click', () => {
1270
+ switchOutputTab(tab.dataset.tab);
1271
+ });
1272
+ });
1273
+
1274
+ document.getElementById('btn-download-logs').addEventListener('click', downloadLogs);
1275
+
1276
+ document.getElementById('log-search').addEventListener('input', () => {
1277
+ if (document.getElementById('full-logs-tab-content').classList.contains('hidden') === false) {
1278
+ const content = document.getElementById('full-logs-content');
1279
+ const currentLogs = content.dataset.originalLogs || content.textContent;
1280
+ content.dataset.originalLogs = currentLogs;
1281
+ displayFullLogs(currentLogs);
1282
+ }
1283
+ });
1284
+
1285
+ document.getElementById('log-filter').addEventListener('change', () => {
1286
+ if (document.getElementById('full-logs-tab-content').classList.contains('hidden') === false) {
1287
+ const content = document.getElementById('full-logs-content');
1288
+ const currentLogs = content.dataset.originalLogs || content.textContent;
1289
+ content.dataset.originalLogs = currentLogs;
1290
+ displayFullLogs(currentLogs);
1291
+ }
1292
+ });
1293
+
1294
+ document.getElementById('btn-auto-scroll').addEventListener('click', function() {
1295
+ this.classList.toggle('bg-blue-500');
1296
+ this.classList.toggle('bg-gray-100');
1297
+ this.textContent = this.classList.contains('bg-blue-500') ? 'Auto-scroll ON' : 'Auto-scroll';
1298
+ });
1299
+
490
1300
  (async function init() {
491
1301
  clearForm();
492
1302
  normalizeRunnerOptions();
1303
+ initDocumentation();
493
1304
  await loadScripts();
494
1305
  })();
495
1306
  </script>