@intranefr/superbackend 1.4.4 → 1.5.0
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.
- package/index.js +16 -1
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +89 -0
- package/src/controllers/adminHeadless.controller.js +82 -0
- package/src/controllers/adminScripts.controller.js +229 -0
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/orgAdmin.controller.js +286 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware.js +115 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminHeadless.routes.js +6 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +29 -0
- package/src/routes/llmUi.routes.js +26 -0
- package/src/routes/orgAdmin.routes.js +5 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +312 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +261 -4
- package/views/partials/dashboard/nav-items.ejs +3 -0
|
@@ -0,0 +1,497 @@
|
|
|
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>Admin Scripts</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="bg-gray-50">
|
|
10
|
+
<div class="max-w-7xl mx-auto px-6 py-6">
|
|
11
|
+
<div class="flex items-center justify-between mb-6">
|
|
12
|
+
<div>
|
|
13
|
+
<h1 class="text-2xl font-semibold text-gray-900">Scripts</h1>
|
|
14
|
+
<div class="text-sm text-gray-500">Configure and run Bash / Node / Browser scripts</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="flex items-center gap-2">
|
|
17
|
+
<button id="btn-new" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">New</button>
|
|
18
|
+
<button id="btn-save" class="px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-black">Save</button>
|
|
19
|
+
<button id="btn-delete" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700">Delete</button>
|
|
20
|
+
<button id="btn-run" class="px-3 py-2 rounded bg-emerald-600 text-white text-sm hover:bg-emerald-700">Run</button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="grid grid-cols-12 gap-6">
|
|
25
|
+
<div class="col-span-4">
|
|
26
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
27
|
+
<div class="p-3 border-b border-gray-200 flex items-center justify-between">
|
|
28
|
+
<div class="text-sm font-medium text-gray-800">Scripts</div>
|
|
29
|
+
<button id="btn-refresh" class="text-sm text-blue-600 hover:underline">Refresh</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div id="scripts-list" class="p-2 max-h-[70vh] overflow-auto"></div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="mt-4 bg-white border border-gray-200 rounded-lg">
|
|
35
|
+
<div class="p-3 border-b border-gray-200">
|
|
36
|
+
<div class="text-sm font-medium text-gray-800">Runs</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div id="runs-list" class="p-2 max-h-[30vh] overflow-auto"></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="col-span-8">
|
|
43
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
44
|
+
<div class="p-4 grid grid-cols-2 gap-4">
|
|
45
|
+
<div>
|
|
46
|
+
<label class="text-xs font-semibold text-gray-600">Name</label>
|
|
47
|
+
<input id="f-name" class="mt-1 w-full border rounded px-3 py-2" placeholder="Update ssh and sync" />
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<label class="text-xs font-semibold text-gray-600">Code</label>
|
|
51
|
+
<input id="f-code" class="mt-1 w-full border rounded px-3 py-2 font-mono text-sm" placeholder="update-ssh-sync" />
|
|
52
|
+
</div>
|
|
53
|
+
<div class="col-span-2">
|
|
54
|
+
<label class="text-xs font-semibold text-gray-600">Description</label>
|
|
55
|
+
<input id="f-desc" class="mt-1 w-full border rounded px-3 py-2" placeholder="(optional)" />
|
|
56
|
+
</div>
|
|
57
|
+
<div>
|
|
58
|
+
<label class="text-xs font-semibold text-gray-600">Type</label>
|
|
59
|
+
<select id="f-type" class="mt-1 w-full border rounded px-3 py-2">
|
|
60
|
+
<option value="bash">bash</option>
|
|
61
|
+
<option value="node">node</option>
|
|
62
|
+
<option value="browser">browser</option>
|
|
63
|
+
</select>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<label class="text-xs font-semibold text-gray-600">Runner</label>
|
|
67
|
+
<select id="f-runner" class="mt-1 w-full border rounded px-3 py-2">
|
|
68
|
+
<option value="host">host</option>
|
|
69
|
+
<option value="vm2">vm2</option>
|
|
70
|
+
<option value="browser">browser</option>
|
|
71
|
+
</select>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<label class="text-xs font-semibold text-gray-600">Timeout (ms)</label>
|
|
75
|
+
<input id="f-timeout" type="number" class="mt-1 w-full border rounded px-3 py-2" />
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<label class="text-xs font-semibold text-gray-600">Working directory</label>
|
|
79
|
+
<input id="f-cwd" class="mt-1 w-full border rounded px-3 py-2 font-mono text-sm" placeholder="(optional)" />
|
|
80
|
+
</div>
|
|
81
|
+
<div class="col-span-2">
|
|
82
|
+
<label class="text-xs font-semibold text-gray-600">Enabled</label>
|
|
83
|
+
<div class="mt-2 flex items-center gap-2">
|
|
84
|
+
<input id="f-enabled" type="checkbox" class="h-4 w-4" />
|
|
85
|
+
<span class="text-sm text-gray-700">Script can be run</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="border-t border-gray-200 p-4">
|
|
91
|
+
<div class="flex items-center justify-between mb-2">
|
|
92
|
+
<div class="text-sm font-medium text-gray-800">Environment</div>
|
|
93
|
+
<button id="btn-add-env" class="text-sm text-blue-600 hover:underline">Add</button>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="overflow-auto">
|
|
96
|
+
<table class="w-full text-sm">
|
|
97
|
+
<thead>
|
|
98
|
+
<tr class="text-left text-gray-500">
|
|
99
|
+
<th class="py-1 pr-2">Key</th>
|
|
100
|
+
<th class="py-1 pr-2">Value</th>
|
|
101
|
+
<th class="py-1 w-12"></th>
|
|
102
|
+
</tr>
|
|
103
|
+
</thead>
|
|
104
|
+
<tbody id="env-tbody"></tbody>
|
|
105
|
+
</table>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="border-t border-gray-200 p-4">
|
|
110
|
+
<div class="text-sm font-medium text-gray-800 mb-2">Script</div>
|
|
111
|
+
<textarea id="f-script" class="w-full h-56 border rounded px-3 py-2 font-mono text-sm" placeholder="#!/usr/bin/env bash\n..."></textarea>
|
|
112
|
+
<div id="runner-warning" class="mt-2 text-xs text-amber-700"></div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<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>
|
|
120
|
+
</div>
|
|
121
|
+
<pre id="output" class="p-3 text-xs font-mono whitespace-pre-wrap max-h-[40vh] overflow-auto"></pre>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<script>
|
|
128
|
+
window.BASE_URL = '<%= baseUrl %>';
|
|
129
|
+
window.ADMIN_PATH = '<%= adminPath %>';
|
|
130
|
+
|
|
131
|
+
const state = {
|
|
132
|
+
scripts: [],
|
|
133
|
+
runs: [],
|
|
134
|
+
selected: null,
|
|
135
|
+
selectedId: null,
|
|
136
|
+
es: null,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function qs(id) {
|
|
140
|
+
return document.getElementById(id);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toast(msg) {
|
|
144
|
+
alert(msg);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function api(path, opts) {
|
|
148
|
+
const baseUrl = window.BASE_URL || '';
|
|
149
|
+
const url = baseUrl + path;
|
|
150
|
+
const res = await fetch(url, {
|
|
151
|
+
credentials: 'same-origin',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
...opts,
|
|
156
|
+
});
|
|
157
|
+
const json = await res.json().catch(() => ({}));
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
throw new Error(json.error || 'Request failed');
|
|
160
|
+
}
|
|
161
|
+
return json;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setOutput(text, append = false) {
|
|
165
|
+
const el = qs('output');
|
|
166
|
+
if (!append) el.textContent = '';
|
|
167
|
+
el.textContent += String(text || '');
|
|
168
|
+
el.scrollTop = el.scrollHeight;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function setRunnerWarning() {
|
|
172
|
+
const type = qs('f-type').value;
|
|
173
|
+
const runner = qs('f-runner').value;
|
|
174
|
+
const el = qs('runner-warning');
|
|
175
|
+
let msg = '';
|
|
176
|
+
if (type === 'bash' && runner === 'host') {
|
|
177
|
+
msg = 'Warning: bash host runner executes on the server host.';
|
|
178
|
+
}
|
|
179
|
+
if (type === 'node' && runner === 'host') {
|
|
180
|
+
msg = 'Warning: node host runner executes on the server host.';
|
|
181
|
+
}
|
|
182
|
+
if (type === 'node' && runner === 'vm2') {
|
|
183
|
+
msg = 'vm2 mode is best-effort and does not support arbitrary Node APIs.';
|
|
184
|
+
}
|
|
185
|
+
if (type === 'browser') {
|
|
186
|
+
msg = 'Browser scripts run in this page only.';
|
|
187
|
+
}
|
|
188
|
+
el.textContent = msg;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeRunnerOptions() {
|
|
192
|
+
const type = qs('f-type').value;
|
|
193
|
+
const runnerSelect = qs('f-runner');
|
|
194
|
+
const runner = runnerSelect.value;
|
|
195
|
+
const allowed =
|
|
196
|
+
type === 'bash' ? ['host'] :
|
|
197
|
+
type === 'node' ? ['host', 'vm2'] :
|
|
198
|
+
['browser'];
|
|
199
|
+
|
|
200
|
+
runnerSelect.innerHTML = allowed
|
|
201
|
+
.map((v) => `<option value="${v}">${v}</option>`)
|
|
202
|
+
.join('');
|
|
203
|
+
|
|
204
|
+
if (allowed.includes(runner)) runnerSelect.value = runner;
|
|
205
|
+
setRunnerWarning();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function currentPayload() {
|
|
209
|
+
const env = [];
|
|
210
|
+
qs('env-tbody').querySelectorAll('tr').forEach((tr) => {
|
|
211
|
+
const key = tr.querySelector('.env-key').value.trim();
|
|
212
|
+
const value = tr.querySelector('.env-val').value;
|
|
213
|
+
if (!key) return;
|
|
214
|
+
env.push({ key, value });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
name: qs('f-name').value.trim(),
|
|
219
|
+
codeIdentifier: qs('f-code').value.trim(),
|
|
220
|
+
description: qs('f-desc').value,
|
|
221
|
+
type: qs('f-type').value,
|
|
222
|
+
runner: qs('f-runner').value,
|
|
223
|
+
timeoutMs: Number(qs('f-timeout').value || 0) || undefined,
|
|
224
|
+
defaultWorkingDirectory: qs('f-cwd').value,
|
|
225
|
+
enabled: !!qs('f-enabled').checked,
|
|
226
|
+
env,
|
|
227
|
+
script: qs('f-script').value,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderEnv(env) {
|
|
232
|
+
const tbody = qs('env-tbody');
|
|
233
|
+
tbody.innerHTML = '';
|
|
234
|
+
const items = Array.isArray(env) ? env : [];
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
const tr = document.createElement('tr');
|
|
237
|
+
tr.innerHTML = `
|
|
238
|
+
<td class="py-1 pr-2"><input class="env-key w-full border rounded px-2 py-1 font-mono text-xs" value="${String(item.key || '').replace(/</g,'<')}" /></td>
|
|
239
|
+
<td class="py-1 pr-2"><input class="env-val w-full border rounded px-2 py-1 font-mono text-xs" value="${String(item.value || '').replace(/</g,'<')}" /></td>
|
|
240
|
+
<td class="py-1 text-right"><button class="btn-del-env text-red-600 hover:underline text-xs">Del</button></td>
|
|
241
|
+
`;
|
|
242
|
+
tbody.appendChild(tr);
|
|
243
|
+
tr.querySelector('.btn-del-env').addEventListener('click', () => {
|
|
244
|
+
tr.remove();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function clearForm() {
|
|
250
|
+
state.selectedId = null;
|
|
251
|
+
state.selected = { enabled: true, timeoutMs: 300000, type: 'bash', runner: 'host', env: [] };
|
|
252
|
+
qs('f-name').value = '';
|
|
253
|
+
qs('f-code').value = '';
|
|
254
|
+
qs('f-desc').value = '';
|
|
255
|
+
qs('f-type').value = 'bash';
|
|
256
|
+
normalizeRunnerOptions();
|
|
257
|
+
qs('f-timeout').value = '300000';
|
|
258
|
+
qs('f-cwd').value = '';
|
|
259
|
+
qs('f-enabled').checked = true;
|
|
260
|
+
qs('f-script').value = '';
|
|
261
|
+
renderEnv([]);
|
|
262
|
+
renderRuns();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function setFormFromScript(s) {
|
|
266
|
+
state.selectedId = s._id;
|
|
267
|
+
state.selected = s;
|
|
268
|
+
qs('f-name').value = s.name || '';
|
|
269
|
+
qs('f-code').value = s.codeIdentifier || '';
|
|
270
|
+
qs('f-desc').value = s.description || '';
|
|
271
|
+
qs('f-type').value = s.type || 'bash';
|
|
272
|
+
normalizeRunnerOptions();
|
|
273
|
+
qs('f-runner').value = s.runner || (s.type === 'bash' ? 'host' : 'host');
|
|
274
|
+
qs('f-timeout').value = String(s.timeoutMs || 300000);
|
|
275
|
+
qs('f-cwd').value = s.defaultWorkingDirectory || '';
|
|
276
|
+
qs('f-enabled').checked = !!s.enabled;
|
|
277
|
+
qs('f-script').value = s.script || '';
|
|
278
|
+
renderEnv(s.env || []);
|
|
279
|
+
loadRuns();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderScripts() {
|
|
283
|
+
const list = qs('scripts-list');
|
|
284
|
+
list.innerHTML = '';
|
|
285
|
+
if (!state.scripts.length) {
|
|
286
|
+
list.innerHTML = '<div class="text-sm text-gray-500 p-2">No scripts</div>';
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
state.scripts.forEach((s) => {
|
|
290
|
+
const btn = document.createElement('button');
|
|
291
|
+
const active = state.selectedId === s._id;
|
|
292
|
+
btn.className = `w-full text-left px-3 py-2 rounded border ${active ? 'bg-blue-50 border-blue-200' : 'bg-white border-gray-200 hover:bg-gray-50'} mb-2`;
|
|
293
|
+
btn.innerHTML = `
|
|
294
|
+
<div class="flex items-center justify-between gap-2">
|
|
295
|
+
<div class="font-medium text-gray-900">${String(s.name || '').replace(/</g,'<')}</div>
|
|
296
|
+
<div class="text-[10px] uppercase bg-gray-200 text-gray-800 px-2 py-0.5 rounded">${String(s.type || '').replace(/</g,'<')}</div>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="text-xs text-gray-500 font-mono">${String(s.codeIdentifier || '').replace(/</g,'<')} · ${String(s.runner || '').replace(/</g,'<')}</div>
|
|
299
|
+
`;
|
|
300
|
+
btn.addEventListener('click', () => setFormFromScript(s));
|
|
301
|
+
list.appendChild(btn);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderRuns() {
|
|
306
|
+
const list = qs('runs-list');
|
|
307
|
+
list.innerHTML = '';
|
|
308
|
+
if (!state.runs.length) {
|
|
309
|
+
list.innerHTML = '<div class="text-sm text-gray-500 p-2">No runs</div>';
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
state.runs.forEach((r) => {
|
|
313
|
+
const btn = document.createElement('button');
|
|
314
|
+
btn.className = 'w-full text-left px-3 py-2 rounded border bg-white border-gray-200 hover:bg-gray-50 mb-2';
|
|
315
|
+
btn.innerHTML = `
|
|
316
|
+
<div class="flex items-center justify-between">
|
|
317
|
+
<div class="text-sm font-medium">${String(r.status || '').replace(/</g,'<')}</div>
|
|
318
|
+
<div class="text-xs text-gray-500">${r.exitCode === null || r.exitCode === undefined ? '' : 'exit ' + r.exitCode}</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="text-xs text-gray-500 font-mono">${String(r._id || '').slice(0, 10)} · ${String(r.createdAt || '').replace(/</g,'<')}</div>
|
|
321
|
+
`;
|
|
322
|
+
btn.addEventListener('click', () => {
|
|
323
|
+
setOutput('');
|
|
324
|
+
if (r.outputTail) setOutput(r.outputTail, true);
|
|
325
|
+
});
|
|
326
|
+
list.appendChild(btn);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function loadScripts() {
|
|
331
|
+
const json = await api('/api/admin/scripts');
|
|
332
|
+
state.scripts = json.items || [];
|
|
333
|
+
renderScripts();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function loadRuns() {
|
|
337
|
+
if (!state.selectedId) {
|
|
338
|
+
state.runs = [];
|
|
339
|
+
renderRuns();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const json = await api('/api/admin/scripts/runs?scriptId=' + encodeURIComponent(state.selectedId));
|
|
343
|
+
state.runs = json.items || [];
|
|
344
|
+
renderRuns();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function saveScript() {
|
|
348
|
+
const payload = currentPayload();
|
|
349
|
+
if (!payload.name) throw new Error('name is required');
|
|
350
|
+
if (!payload.codeIdentifier) throw new Error('codeIdentifier is required');
|
|
351
|
+
if (!payload.type) throw new Error('type is required');
|
|
352
|
+
if (!payload.runner) throw new Error('runner is required');
|
|
353
|
+
|
|
354
|
+
if (payload.type === 'bash' && payload.runner !== 'host') {
|
|
355
|
+
throw new Error('bash runner must be host');
|
|
356
|
+
}
|
|
357
|
+
if (payload.type === 'browser' && payload.runner !== 'browser') {
|
|
358
|
+
throw new Error('browser runner must be browser');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!state.selectedId) {
|
|
362
|
+
const res = await api('/api/admin/scripts', { method: 'POST', body: JSON.stringify(payload) });
|
|
363
|
+
toast('Created');
|
|
364
|
+
await loadScripts();
|
|
365
|
+
setFormFromScript(res.item);
|
|
366
|
+
} else {
|
|
367
|
+
const res = await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId), { method: 'PUT', body: JSON.stringify(payload) });
|
|
368
|
+
toast('Saved');
|
|
369
|
+
await loadScripts();
|
|
370
|
+
setFormFromScript(res.item);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function deleteScript() {
|
|
375
|
+
if (!state.selectedId) return;
|
|
376
|
+
if (!confirm('Delete script?')) return;
|
|
377
|
+
await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId), { method: 'DELETE' });
|
|
378
|
+
toast('Deleted');
|
|
379
|
+
clearForm();
|
|
380
|
+
await loadScripts();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function closeStream() {
|
|
384
|
+
if (state.es) {
|
|
385
|
+
try { state.es.close(); } catch {}
|
|
386
|
+
}
|
|
387
|
+
state.es = null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function startSse(runId) {
|
|
391
|
+
closeStream();
|
|
392
|
+
const baseUrl = window.BASE_URL || '';
|
|
393
|
+
const url = baseUrl + '/api/admin/scripts/runs/' + encodeURIComponent(runId) + '/stream';
|
|
394
|
+
const es = new EventSource(url);
|
|
395
|
+
state.es = es;
|
|
396
|
+
|
|
397
|
+
const onLog = (e) => {
|
|
398
|
+
const data = JSON.parse(e.data || '{}');
|
|
399
|
+
setOutput(data.line || '', true);
|
|
400
|
+
};
|
|
401
|
+
const onStatus = (e) => {
|
|
402
|
+
const data = JSON.parse(e.data || '{}');
|
|
403
|
+
setOutput(`\n[status] ${data.status}${data.exitCode === undefined ? '' : ' (exit ' + data.exitCode + ')'}\n`, true);
|
|
404
|
+
loadRuns();
|
|
405
|
+
};
|
|
406
|
+
const onDone = () => {
|
|
407
|
+
closeStream();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
es.addEventListener('log', onLog);
|
|
411
|
+
es.addEventListener('status', onStatus);
|
|
412
|
+
es.addEventListener('done', onDone);
|
|
413
|
+
es.onerror = () => {
|
|
414
|
+
setOutput('\n[stream] disconnected\n', true);
|
|
415
|
+
closeStream();
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function runScript() {
|
|
420
|
+
const payload = currentPayload();
|
|
421
|
+
|
|
422
|
+
if (payload.type === 'browser') {
|
|
423
|
+
setOutput('');
|
|
424
|
+
const originalLog = console.log;
|
|
425
|
+
const originalErr = console.error;
|
|
426
|
+
try {
|
|
427
|
+
console.log = (...args) => setOutput(args.join(' ') + '\n', true);
|
|
428
|
+
console.error = (...args) => setOutput(args.join(' ') + '\n', true);
|
|
429
|
+
const fn = new Function(payload.script);
|
|
430
|
+
fn();
|
|
431
|
+
setOutput('\n[done] ok\n', true);
|
|
432
|
+
} finally {
|
|
433
|
+
console.log = originalLog;
|
|
434
|
+
console.error = originalErr;
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!state.selectedId) {
|
|
440
|
+
await saveScript();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
setOutput('');
|
|
444
|
+
const res = await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId) + '/run', { method: 'POST' });
|
|
445
|
+
startSse(res.runId);
|
|
446
|
+
await loadRuns();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
qs('btn-refresh').addEventListener('click', loadScripts);
|
|
450
|
+
qs('btn-new').addEventListener('click', clearForm);
|
|
451
|
+
qs('btn-save').addEventListener('click', async () => {
|
|
452
|
+
try {
|
|
453
|
+
await saveScript();
|
|
454
|
+
} catch (e) {
|
|
455
|
+
toast(e.message);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
qs('btn-delete').addEventListener('click', async () => {
|
|
459
|
+
try {
|
|
460
|
+
await deleteScript();
|
|
461
|
+
} catch (e) {
|
|
462
|
+
toast(e.message);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
qs('btn-run').addEventListener('click', async () => {
|
|
466
|
+
try {
|
|
467
|
+
await runScript();
|
|
468
|
+
} catch (e) {
|
|
469
|
+
toast(e.message);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
qs('btn-clear-output').addEventListener('click', () => setOutput(''));
|
|
473
|
+
qs('btn-add-env').addEventListener('click', () => {
|
|
474
|
+
const tbody = qs('env-tbody');
|
|
475
|
+
const tr = document.createElement('tr');
|
|
476
|
+
tr.innerHTML = `
|
|
477
|
+
<td class="py-1 pr-2"><input class="env-key w-full border rounded px-2 py-1 font-mono text-xs" /></td>
|
|
478
|
+
<td class="py-1 pr-2"><input class="env-val w-full border rounded px-2 py-1 font-mono text-xs" /></td>
|
|
479
|
+
<td class="py-1 text-right"><button class="btn-del-env text-red-600 hover:underline text-xs">Del</button></td>
|
|
480
|
+
`;
|
|
481
|
+
tbody.appendChild(tr);
|
|
482
|
+
tr.querySelector('.btn-del-env').addEventListener('click', () => tr.remove());
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
qs('f-type').addEventListener('change', () => {
|
|
486
|
+
normalizeRunnerOptions();
|
|
487
|
+
});
|
|
488
|
+
qs('f-runner').addEventListener('change', setRunnerWarning);
|
|
489
|
+
|
|
490
|
+
(async function init() {
|
|
491
|
+
clearForm();
|
|
492
|
+
normalizeRunnerOptions();
|
|
493
|
+
await loadScripts();
|
|
494
|
+
})();
|
|
495
|
+
</script>
|
|
496
|
+
</body>
|
|
497
|
+
</html>
|