@intranefr/superbackend 1.5.2 → 1.6.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 (134) hide show
  1. package/cookies.txt +6 -0
  2. package/cookies1.txt +6 -0
  3. package/cookies2.txt +6 -0
  4. package/cookies3.txt +6 -0
  5. package/cookies4.txt +5 -0
  6. package/cookies_old.txt +5 -0
  7. package/cookies_old_test.txt +6 -0
  8. package/cookies_super.txt +5 -0
  9. package/cookies_super_test.txt +6 -0
  10. package/cookies_test.txt +6 -0
  11. package/index.js +9 -0
  12. package/manage.js +745 -0
  13. package/package.json +6 -2
  14. package/plugins/core-waiting-list-migration/README.md +118 -0
  15. package/plugins/core-waiting-list-migration/index.js +438 -0
  16. package/plugins/global-settings-presets/index.js +20 -0
  17. package/plugins/hello-cli/index.js +17 -0
  18. package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
  19. package/plugins/ui-components-seeder/components/suiToast.js +186 -0
  20. package/plugins/ui-components-seeder/index.js +31 -0
  21. package/public/js/admin-ui-components-preview.js +281 -0
  22. package/public/js/admin-ui-components.js +408 -0
  23. package/public/js/llm-provider-model-picker.js +193 -0
  24. package/public/test-iframe-fix.html +63 -0
  25. package/public/test-iframe.html +14 -0
  26. package/src/admin/endpointRegistry.js +68 -0
  27. package/src/controllers/admin.controller.js +36 -10
  28. package/src/controllers/adminAgents.controller.js +37 -0
  29. package/src/controllers/adminDataCleanup.controller.js +45 -0
  30. package/src/controllers/adminLlm.controller.js +19 -8
  31. package/src/controllers/adminLogin.controller.js +269 -0
  32. package/src/controllers/adminMarkdowns.controller.js +157 -0
  33. package/src/controllers/adminPlugins.controller.js +55 -0
  34. package/src/controllers/adminRegistry.controller.js +106 -0
  35. package/src/controllers/adminScripts.controller.js +138 -0
  36. package/src/controllers/adminStats.controller.js +4 -4
  37. package/src/controllers/adminTelegram.controller.js +72 -0
  38. package/src/controllers/markdowns.controller.js +42 -0
  39. package/src/controllers/registry.controller.js +32 -0
  40. package/src/controllers/waitingList.controller.js +52 -74
  41. package/src/helpers/mongooseHelper.js +6 -6
  42. package/src/helpers/scriptBase.js +2 -2
  43. package/src/middleware/auth.js +71 -1
  44. package/src/middleware/rbac.js +62 -0
  45. package/src/middleware.js +584 -176
  46. package/src/models/Agent.js +105 -0
  47. package/src/models/AgentMessage.js +82 -0
  48. package/src/models/GlobalSetting.js +11 -1
  49. package/src/models/Markdown.js +75 -0
  50. package/src/models/ScriptRun.js +8 -0
  51. package/src/models/TelegramBot.js +42 -0
  52. package/src/models/UiComponent.js +2 -0
  53. package/src/models/User.js +1 -1
  54. package/src/routes/admin.routes.js +3 -3
  55. package/src/routes/adminAgents.routes.js +13 -0
  56. package/src/routes/adminAssets.routes.js +11 -11
  57. package/src/routes/adminBlog.routes.js +2 -2
  58. package/src/routes/adminBlogAi.routes.js +2 -2
  59. package/src/routes/adminBlogAutomation.routes.js +2 -2
  60. package/src/routes/adminCache.routes.js +2 -2
  61. package/src/routes/adminConsoleManager.routes.js +2 -2
  62. package/src/routes/adminCrons.routes.js +2 -2
  63. package/src/routes/adminDataCleanup.routes.js +26 -0
  64. package/src/routes/adminDbBrowser.routes.js +2 -2
  65. package/src/routes/adminEjsVirtual.routes.js +2 -2
  66. package/src/routes/adminFeatureFlags.routes.js +6 -6
  67. package/src/routes/adminHeadless.routes.js +2 -2
  68. package/src/routes/adminHealthChecks.routes.js +2 -2
  69. package/src/routes/adminI18n.routes.js +2 -2
  70. package/src/routes/adminJsonConfigs.routes.js +8 -8
  71. package/src/routes/adminLlm.routes.js +8 -7
  72. package/src/routes/adminLogin.routes.js +23 -0
  73. package/src/routes/adminMarkdowns.routes.js +10 -0
  74. package/src/routes/adminMigration.routes.js +12 -12
  75. package/src/routes/adminPages.routes.js +2 -2
  76. package/src/routes/adminPlugins.routes.js +15 -0
  77. package/src/routes/adminProxy.routes.js +2 -2
  78. package/src/routes/adminRateLimits.routes.js +8 -8
  79. package/src/routes/adminRbac.routes.js +2 -2
  80. package/src/routes/adminRegistry.routes.js +24 -0
  81. package/src/routes/adminScripts.routes.js +6 -3
  82. package/src/routes/adminSeoConfig.routes.js +10 -10
  83. package/src/routes/adminTelegram.routes.js +14 -0
  84. package/src/routes/adminTerminals.routes.js +2 -2
  85. package/src/routes/adminUiComponents.routes.js +2 -2
  86. package/src/routes/adminUploadNamespaces.routes.js +7 -7
  87. package/src/routes/blogInternal.routes.js +2 -2
  88. package/src/routes/experiments.routes.js +2 -2
  89. package/src/routes/formsAdmin.routes.js +6 -6
  90. package/src/routes/globalSettings.routes.js +8 -8
  91. package/src/routes/internalExperiments.routes.js +2 -2
  92. package/src/routes/markdowns.routes.js +16 -0
  93. package/src/routes/notificationAdmin.routes.js +7 -7
  94. package/src/routes/orgAdmin.routes.js +16 -16
  95. package/src/routes/pages.routes.js +3 -3
  96. package/src/routes/registry.routes.js +11 -0
  97. package/src/routes/stripeAdmin.routes.js +12 -12
  98. package/src/routes/userAdmin.routes.js +7 -7
  99. package/src/routes/waitingListAdmin.routes.js +2 -2
  100. package/src/routes/workflows.routes.js +3 -3
  101. package/src/services/agent.service.js +546 -0
  102. package/src/services/agentHistory.service.js +345 -0
  103. package/src/services/agentTools.service.js +578 -0
  104. package/src/services/dataCleanup.service.js +286 -0
  105. package/src/services/jsonConfigs.service.js +284 -10
  106. package/src/services/llm.service.js +219 -6
  107. package/src/services/markdowns.service.js +522 -0
  108. package/src/services/plugins.service.js +348 -0
  109. package/src/services/registry.service.js +452 -0
  110. package/src/services/scriptsRunner.service.js +328 -37
  111. package/src/services/telegram.service.js +130 -0
  112. package/src/services/uiComponents.service.js +180 -0
  113. package/src/services/waitingListJson.service.js +401 -0
  114. package/src/utils/rbac/rightsRegistry.js +118 -0
  115. package/test-access.js +63 -0
  116. package/test-iframe-fix.html +63 -0
  117. package/test-iframe.html +14 -0
  118. package/views/admin-403.ejs +92 -0
  119. package/views/admin-agents.ejs +273 -0
  120. package/views/admin-coolify-deploy.ejs +8 -8
  121. package/views/admin-dashboard-home.ejs +52 -2
  122. package/views/admin-dashboard.ejs +179 -7
  123. package/views/admin-data-cleanup.ejs +357 -0
  124. package/views/admin-experiments.ejs +1 -1
  125. package/views/admin-login.ejs +286 -0
  126. package/views/admin-markdowns.ejs +905 -0
  127. package/views/admin-plugins-system.ejs +223 -0
  128. package/views/admin-scripts.ejs +221 -4
  129. package/views/admin-telegram.ejs +269 -0
  130. package/views/admin-ui-components.ejs +82 -402
  131. package/views/admin-users.ejs +207 -11
  132. package/views/partials/dashboard/nav-items.ejs +5 -0
  133. package/views/partials/llm-provider-model-picker.ejs +0 -161
  134. package/analysis-only.skill +0 -0
@@ -0,0 +1,223 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Plugins system</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
10
+ </head>
11
+ <body class="bg-gray-100">
12
+ <div id="app" class="min-h-screen p-6">
13
+ <div class="max-w-6xl mx-auto space-y-6">
14
+ <header class="bg-white rounded-xl border border-gray-200 p-6">
15
+ <h1 class="text-2xl font-bold text-gray-900 flex items-center gap-2">
16
+ <i class="ti ti-puzzle text-blue-600"></i>
17
+ Plugins system
18
+ </h1>
19
+ <p class="text-gray-600 mt-2">Enable, disable, and install local CommonJS plugins from <code class="text-sm bg-gray-100 px-1.5 py-0.5 rounded">cwd/plugins/*</code>.</p>
20
+ </header>
21
+
22
+ <section class="bg-white rounded-xl border border-gray-200 p-6">
23
+ <div class="flex items-center justify-between mb-4">
24
+ <h2 class="text-lg font-semibold text-gray-800">Available plugins</h2>
25
+ <button @click="loadPlugins" class="px-3 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 text-sm">
26
+ Refresh
27
+ </button>
28
+ </div>
29
+
30
+ <p v-if="error" class="mb-4 text-sm text-red-600">{{ error }}</p>
31
+
32
+ <div v-if="loading" class="text-sm text-gray-500">Loading plugins...</div>
33
+
34
+ <div v-else-if="plugins.length === 0" class="text-sm text-gray-500 border border-dashed border-gray-300 rounded-lg p-4">
35
+ No plugins discovered. Create folders under <code>plugins/</code> with an <code>index.js</code> CommonJS export.
36
+ </div>
37
+
38
+ <div v-else class="space-y-3">
39
+ <article v-for="plugin in plugins" :key="plugin.id" class="border border-gray-200 rounded-lg p-4 bg-gray-50">
40
+ <div class="flex items-start justify-between gap-4">
41
+ <div>
42
+ <h3 class="font-semibold text-gray-900">{{ plugin.name }} <span class="text-xs text-gray-500">({{ plugin.id }})</span></h3>
43
+ <p class="text-sm text-gray-600 mt-1">{{ plugin.description || 'No description provided.' }}</p>
44
+ <div class="mt-2 flex flex-wrap gap-2 text-xs">
45
+ <span class="px-2 py-1 rounded bg-white border border-gray-200">version: {{ plugin.version }}</span>
46
+ <span class="px-2 py-1 rounded bg-white border border-gray-200">bootstrap: {{ plugin.hooks.bootstrap ? 'yes' : 'no' }}</span>
47
+ <span class="px-2 py-1 rounded bg-white border border-gray-200">install: {{ plugin.hooks.install ? 'yes' : 'no' }}</span>
48
+ <span class="px-2 py-1 rounded" :class="plugin.enabled ? 'bg-green-100 text-green-800 border border-green-200' : 'bg-gray-200 text-gray-700 border border-gray-300'">
49
+ {{ plugin.enabled ? 'enabled' : 'disabled' }}
50
+ </span>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="flex flex-col gap-2 min-w-[170px]">
55
+ <button
56
+ v-if="!plugin.enabled"
57
+ @click="enablePlugin(plugin.id)"
58
+ class="px-3 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 text-sm"
59
+ >Enable</button>
60
+ <button
61
+ v-else
62
+ @click="disablePlugin(plugin.id)"
63
+ class="px-3 py-2 rounded-lg bg-gray-700 text-white hover:bg-gray-800 text-sm"
64
+ >Disable</button>
65
+ <button
66
+ @click="installPlugin(plugin.id)"
67
+ class="px-3 py-2 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 text-sm"
68
+ >Run install</button>
69
+ </div>
70
+ </div>
71
+ </article>
72
+ </div>
73
+ </section>
74
+
75
+ <section class="bg-white rounded-xl border border-gray-200 p-6 space-y-3">
76
+ <h2 class="text-lg font-semibold text-gray-800">How to create and use plugins</h2>
77
+
78
+ <details class="border border-gray-200 rounded-lg p-4">
79
+ <summary class="cursor-pointer font-medium text-gray-900">Add a new plugin</summary>
80
+ <pre class="mt-3 text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-auto">plugins/my-plugin/index.js
81
+
82
+ module.exports = {
83
+ meta: {
84
+ id: 'my-plugin',
85
+ name: 'My Plugin',
86
+ version: '1.0.0',
87
+ description: 'Example plugin',
88
+ tags: ['example']
89
+ },
90
+ hooks: {
91
+ bootstrap(ctx) {
92
+ console.log('[my-plugin] bootstrap', Object.keys(ctx.services || {}));
93
+ },
94
+ install(ctx) {
95
+ console.log('[my-plugin] install');
96
+ }
97
+ }
98
+ };</pre>
99
+ </details>
100
+
101
+ <details class="border border-gray-200 rounded-lg p-4">
102
+ <summary class="cursor-pointer font-medium text-gray-900">Permissive contract supported</summary>
103
+ <pre class="mt-3 text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-auto">module.exports = {
104
+ id: 'top-level-contract',
105
+ name: 'Top level contract plugin',
106
+ bootstrap(ctx) {
107
+ console.log('bootstrap hook with fallback contract');
108
+ },
109
+ install(ctx) {
110
+ console.log('install hook with fallback contract');
111
+ }
112
+ };</pre>
113
+ </details>
114
+
115
+ <details class="border border-gray-200 rounded-lg p-4">
116
+ <summary class="cursor-pointer font-medium text-gray-900">Expose helpers/services programmatically</summary>
117
+ <pre class="mt-3 text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-auto">module.exports = {
118
+ meta: { id: 'tools', name: 'Tools plugin' },
119
+ services: {
120
+ myService() {
121
+ return 'hello';
122
+ }
123
+ },
124
+ helpers: {
125
+ formatLabel(input) {
126
+ return String(input || '').toUpperCase();
127
+ }
128
+ }
129
+ };
130
+
131
+ // then available in runtime after enable/bootstrap:
132
+ // superbackend.services.pluginsRuntime.myService()
133
+ // superbackend.helpers.pluginsRuntime.formatLabel('demo')</pre>
134
+ </details>
135
+ </section>
136
+ </div>
137
+ </div>
138
+
139
+ <script>
140
+ const { createApp, ref, onMounted } = Vue;
141
+
142
+ createApp({
143
+ setup() {
144
+ const baseUrl = '<%= baseUrl %>';
145
+ const plugins = ref([]);
146
+ const loading = ref(false);
147
+ const error = ref('');
148
+
149
+ const loadPlugins = async () => {
150
+ loading.value = true;
151
+ error.value = '';
152
+ try {
153
+ const response = await fetch(baseUrl + '/api/admin/plugins');
154
+ const payload = await response.json();
155
+ if (!response.ok) throw new Error(payload.error || 'Failed to load plugins');
156
+ plugins.value = payload.items || [];
157
+ } catch (err) {
158
+ error.value = err.message || 'Failed to load plugins';
159
+ } finally {
160
+ loading.value = false;
161
+ }
162
+ };
163
+
164
+ const enablePlugin = async (id) => {
165
+ error.value = '';
166
+ try {
167
+ const response = await fetch(baseUrl + '/api/admin/plugins/' + encodeURIComponent(id) + '/enable', {
168
+ method: 'POST',
169
+ headers: { 'Content-Type': 'application/json' }
170
+ });
171
+ const payload = await response.json();
172
+ if (!response.ok) throw new Error(payload.error || 'Enable failed');
173
+ await loadPlugins();
174
+ } catch (err) {
175
+ error.value = err.message || 'Enable failed';
176
+ }
177
+ };
178
+
179
+ const disablePlugin = async (id) => {
180
+ error.value = '';
181
+ try {
182
+ const response = await fetch(baseUrl + '/api/admin/plugins/' + encodeURIComponent(id) + '/disable', {
183
+ method: 'POST',
184
+ headers: { 'Content-Type': 'application/json' }
185
+ });
186
+ const payload = await response.json();
187
+ if (!response.ok) throw new Error(payload.error || 'Disable failed');
188
+ await loadPlugins();
189
+ } catch (err) {
190
+ error.value = err.message || 'Disable failed';
191
+ }
192
+ };
193
+
194
+ const installPlugin = async (id) => {
195
+ error.value = '';
196
+ try {
197
+ const response = await fetch(baseUrl + '/api/admin/plugins/' + encodeURIComponent(id) + '/install', {
198
+ method: 'POST',
199
+ headers: { 'Content-Type': 'application/json' }
200
+ });
201
+ const payload = await response.json();
202
+ if (!response.ok) throw new Error(payload.error || 'Install failed');
203
+ } catch (err) {
204
+ error.value = err.message || 'Install failed';
205
+ }
206
+ };
207
+
208
+ onMounted(loadPlugins);
209
+
210
+ return {
211
+ plugins,
212
+ loading,
213
+ error,
214
+ loadPlugins,
215
+ enablePlugin,
216
+ disablePlugin,
217
+ installPlugin,
218
+ };
219
+ }
220
+ }).mount('#app');
221
+ </script>
222
+ </body>
223
+ </html>
@@ -549,11 +549,44 @@ const recentRuns = await ScriptRun.find()
549
549
  </div>
550
550
 
551
551
  <div class="mt-4 bg-white border border-gray-200 rounded-lg">
552
- <div class="p-3 border-b border-gray-200 flex items-center justify-between">
553
- <div class="text-sm font-medium text-gray-800">Output</div>
554
- <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>
555
589
  </div>
556
- <pre id="output" class="p-3 text-xs font-mono whitespace-pre-wrap max-h-[40vh] overflow-auto"></pre>
557
590
  </div>
558
591
  </div>
559
592
  </div>
@@ -571,6 +604,8 @@ const recentRuns = await ScriptRun.find()
571
604
  es: null,
572
605
  };
573
606
 
607
+ let currentRunId = null;
608
+
574
609
  function qs(id) {
575
610
  return document.getElementById(id);
576
611
  }
@@ -772,8 +807,12 @@ const recentRuns = await ScriptRun.find()
772
807
  <div class="text-xs text-gray-500 font-mono">${String(r._id || '').slice(0, 10)} · ${String(r.createdAt || '').replace(/</g,'&lt;')}</div>
773
808
  `;
774
809
  btn.addEventListener('click', () => {
810
+ currentRunId = r._id; // Set the current run ID when clicking on a previous run
775
811
  setOutput('');
776
812
  if (r.outputTail) setOutput(r.outputTail, true);
813
+
814
+ // Load programmatic output for the selected run
815
+ loadProgrammaticOutput(r._id);
777
816
  });
778
817
  list.appendChild(btn);
779
818
  });
@@ -894,8 +933,16 @@ const recentRuns = await ScriptRun.find()
894
933
 
895
934
  setOutput('');
896
935
  const res = await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId) + '/run', { method: 'POST' });
936
+ currentRunId = res.runId; // Set the current run ID
897
937
  startSse(res.runId);
898
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
899
946
  }
900
947
 
901
948
  qs('btn-refresh').addEventListener('click', loadScripts);
@@ -1080,6 +1127,176 @@ const recentRuns = await ScriptRun.find()
1080
1127
  });
1081
1128
  }
1082
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
+
1083
1300
  (async function init() {
1084
1301
  clearForm();
1085
1302
  normalizeRunnerOptions();