@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,328 @@
|
|
|
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 Terminals</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<link rel="stylesheet" href="https://unpkg.com/xterm@latest/css/xterm.css" />
|
|
9
|
+
<style>
|
|
10
|
+
/* Fullscreen styles */
|
|
11
|
+
:fullscreen .max-w-7xl {
|
|
12
|
+
max-width: 100vw;
|
|
13
|
+
width: 100vw;
|
|
14
|
+
height: 100vh;
|
|
15
|
+
padding: 0;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
}
|
|
19
|
+
:fullscreen .bg-white {
|
|
20
|
+
flex: 1;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
border: none;
|
|
24
|
+
border-radius: 0;
|
|
25
|
+
}
|
|
26
|
+
:fullscreen #terminal-container {
|
|
27
|
+
flex: 1;
|
|
28
|
+
height: auto !important;
|
|
29
|
+
}
|
|
30
|
+
:fullscreen .h-\[70vh\] {
|
|
31
|
+
height: 100% !important;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body class="bg-gray-50">
|
|
36
|
+
<div class="max-w-7xl mx-auto px-6 py-6">
|
|
37
|
+
<div class="flex items-center justify-between mb-4">
|
|
38
|
+
<div>
|
|
39
|
+
<h1 class="text-2xl font-semibold text-gray-900">Terminals</h1>
|
|
40
|
+
<div class="text-sm text-gray-500">Interactive shell sessions on the backend host</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="flex items-center gap-2">
|
|
43
|
+
<button id="btn-new" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">New</button>
|
|
44
|
+
<button id="btn-close" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700">Close</button>
|
|
45
|
+
<button id="btn-fullscreen" class="px-3 py-2 rounded bg-gray-600 text-white text-sm hover:bg-gray-700">Fullscreen</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
50
|
+
<div class="border-b border-gray-200 flex items-center justify-between px-3 py-2">
|
|
51
|
+
<div id="tabs" class="flex items-center gap-2 overflow-auto"></div>
|
|
52
|
+
<div class="text-xs text-gray-500 whitespace-nowrap">
|
|
53
|
+
Ctrl+Shift+T new · Ctrl+Shift+W close · Ctrl+Tab next · Ctrl+Shift+Tab prev · Alt+1..9 switch
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div id="terminal-container" class="h-[70vh]"></div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<script src="https://unpkg.com/xterm@latest/lib/xterm.js"></script>
|
|
61
|
+
|
|
62
|
+
<script>
|
|
63
|
+
window.BASE_URL = '<%= baseUrl %>';
|
|
64
|
+
window.ADMIN_PATH = '<%= adminPath %>';
|
|
65
|
+
|
|
66
|
+
const state = {
|
|
67
|
+
sessions: [],
|
|
68
|
+
activeId: null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function baseWsUrl() {
|
|
72
|
+
const base = window.BASE_URL || '';
|
|
73
|
+
const u = new URL(base || window.location.origin);
|
|
74
|
+
const proto = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
75
|
+
return proto + '//' + u.host + u.pathname.replace(/\/$/, '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function apiUrl(path) {
|
|
79
|
+
return (window.BASE_URL || '') + path;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function api(path, opts) {
|
|
83
|
+
const res = await fetch(apiUrl(path), {
|
|
84
|
+
credentials: 'same-origin',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
...opts,
|
|
87
|
+
});
|
|
88
|
+
const json = await res.json().catch(() => ({}));
|
|
89
|
+
if (!res.ok) throw new Error(json.error || 'Request failed');
|
|
90
|
+
return json;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function qs(id) { return document.getElementById(id); }
|
|
94
|
+
|
|
95
|
+
function renderTabs() {
|
|
96
|
+
const tabs = qs('tabs');
|
|
97
|
+
tabs.innerHTML = '';
|
|
98
|
+
state.sessions.forEach((s, idx) => {
|
|
99
|
+
const btn = document.createElement('button');
|
|
100
|
+
const active = s.sessionId === state.activeId;
|
|
101
|
+
btn.className = `px-3 py-1.5 rounded text-sm border ${active ? 'bg-blue-50 border-blue-200 text-blue-800' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'}`;
|
|
102
|
+
btn.textContent = `#${idx + 1} ${s.sessionId.slice(0, 6)}`;
|
|
103
|
+
btn.addEventListener('click', () => activateSession(s.sessionId));
|
|
104
|
+
tabs.appendChild(btn);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function destroySessionClient(s) {
|
|
109
|
+
try { if (s.ws) s.ws.close(); } catch {}
|
|
110
|
+
try { if (s.term) s.term.dispose(); } catch {}
|
|
111
|
+
s.ws = null;
|
|
112
|
+
s.term = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function activateSession(sessionId) {
|
|
116
|
+
state.activeId = sessionId;
|
|
117
|
+
renderTabs();
|
|
118
|
+
|
|
119
|
+
const container = qs('terminal-container');
|
|
120
|
+
container.innerHTML = '';
|
|
121
|
+
|
|
122
|
+
const s = state.sessions.find((x) => x.sessionId === sessionId);
|
|
123
|
+
if (!s) return;
|
|
124
|
+
|
|
125
|
+
container.appendChild(s.termElement);
|
|
126
|
+
// Only open the terminal if it hasn't been opened yet
|
|
127
|
+
if (!s.term._opened) {
|
|
128
|
+
s.term.open(s.termElement);
|
|
129
|
+
s.term._opened = true;
|
|
130
|
+
}
|
|
131
|
+
setTimeout(() => resizeActive(), 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resizeActive() {
|
|
135
|
+
const s = state.sessions.find((x) => x.sessionId === state.activeId);
|
|
136
|
+
if (!s || !s.term || !s.ws) return;
|
|
137
|
+
|
|
138
|
+
const el = s.termElement;
|
|
139
|
+
const container = el.parentElement;
|
|
140
|
+
if (!container) return;
|
|
141
|
+
|
|
142
|
+
const cols = Math.max(40, Math.floor(container.clientWidth / 9));
|
|
143
|
+
const rows = Math.max(10, Math.floor(container.clientHeight / 18));
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
s.term.resize(cols, rows);
|
|
147
|
+
} catch {}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
s.ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function newTerminal() {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
ensureTerminal(() => {
|
|
157
|
+
(async () => {
|
|
158
|
+
try {
|
|
159
|
+
const created = await api('/api/admin/terminals/sessions', { method: 'POST', body: JSON.stringify({ cols: 120, rows: 30 }) });
|
|
160
|
+
const sessionId = created.sessionId;
|
|
161
|
+
|
|
162
|
+
const term = new Terminal({
|
|
163
|
+
cursorBlink: true,
|
|
164
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
165
|
+
fontSize: 13,
|
|
166
|
+
theme: { background: '#0b1020' },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const termElement = document.createElement('div');
|
|
170
|
+
termElement.className = 'w-full h-full';
|
|
171
|
+
|
|
172
|
+
const ws = new WebSocket(baseWsUrl() + '/api/admin/terminals/ws?sessionId=' + encodeURIComponent(sessionId));
|
|
173
|
+
|
|
174
|
+
const s = { sessionId, term, ws, termElement };
|
|
175
|
+
state.sessions.push(s);
|
|
176
|
+
state.activeId = sessionId;
|
|
177
|
+
renderTabs();
|
|
178
|
+
|
|
179
|
+
ws.onopen = () => {
|
|
180
|
+
activateSession(sessionId);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
ws.onmessage = (ev) => {
|
|
184
|
+
let msg;
|
|
185
|
+
try { msg = JSON.parse(String(ev.data || '')); } catch { return; }
|
|
186
|
+
if (!msg || typeof msg !== 'object') return;
|
|
187
|
+
|
|
188
|
+
if (msg.type === 'output') {
|
|
189
|
+
term.write(msg.data || '');
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
ws.onclose = () => {
|
|
194
|
+
term.write('\r\n[disconnected]\r\n');
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
term.onData((data) => {
|
|
198
|
+
try {
|
|
199
|
+
ws.send(JSON.stringify({ type: 'input', data }));
|
|
200
|
+
} catch {}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
window.addEventListener('resize', () => {
|
|
204
|
+
if (state.activeId === sessionId) resizeActive();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
resolve();
|
|
208
|
+
} catch (e) {
|
|
209
|
+
reject(e);
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function closeActive() {
|
|
217
|
+
const sessionId = state.activeId;
|
|
218
|
+
if (!sessionId) return;
|
|
219
|
+
|
|
220
|
+
const idx = state.sessions.findIndex((x) => x.sessionId === sessionId);
|
|
221
|
+
if (idx === -1) return;
|
|
222
|
+
|
|
223
|
+
const s = state.sessions[idx];
|
|
224
|
+
destroySessionClient(s);
|
|
225
|
+
|
|
226
|
+
state.sessions.splice(idx, 1);
|
|
227
|
+
state.activeId = state.sessions[idx - 1]?.sessionId || state.sessions[0]?.sessionId || null;
|
|
228
|
+
renderTabs();
|
|
229
|
+
|
|
230
|
+
qs('terminal-container').innerHTML = '';
|
|
231
|
+
if (state.activeId) activateSession(state.activeId);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await api('/api/admin/terminals/sessions/' + encodeURIComponent(sessionId), { method: 'DELETE' });
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function nextTab(dir) {
|
|
239
|
+
if (!state.sessions.length) return;
|
|
240
|
+
const idx = state.sessions.findIndex((x) => x.sessionId === state.activeId);
|
|
241
|
+
if (idx === -1) return;
|
|
242
|
+
const next = (idx + dir + state.sessions.length) % state.sessions.length;
|
|
243
|
+
activateSession(state.sessions[next].sessionId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function switchToIndex(n) {
|
|
247
|
+
const idx = n - 1;
|
|
248
|
+
if (idx < 0 || idx >= state.sessions.length) return;
|
|
249
|
+
activateSession(state.sessions[idx].sessionId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
document.addEventListener('keydown', (e) => {
|
|
253
|
+
const key = e.key;
|
|
254
|
+
|
|
255
|
+
if (e.ctrlKey && e.shiftKey && (key === 'T' || key === 't')) {
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
newTerminal();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (e.ctrlKey && e.shiftKey && (key === 'W' || key === 'w')) {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
closeActive();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (e.ctrlKey && !e.shiftKey && key === 'Tab') {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
nextTab(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (e.ctrlKey && e.shiftKey && key === 'Tab') {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
nextTab(-1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (e.altKey && /^[1-9]$/.test(key)) {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
switchToIndex(Number(key));
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
qs('btn-new').addEventListener('click', () => newTerminal());
|
|
282
|
+
qs('btn-close').addEventListener('click', () => closeActive());
|
|
283
|
+
|
|
284
|
+
(async function init() {
|
|
285
|
+
const listed = await api('/api/admin/terminals/sessions');
|
|
286
|
+
const items = listed.items || [];
|
|
287
|
+
if (items.length) {
|
|
288
|
+
for (const it of items.slice(0, 1)) {
|
|
289
|
+
await newTerminal();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
293
|
+
|
|
294
|
+
function toggleFullscreen() {
|
|
295
|
+
if (!document.fullscreenElement) {
|
|
296
|
+
document.documentElement.requestFullscreen().catch(() => {});
|
|
297
|
+
} else {
|
|
298
|
+
document.exitFullscreen().catch(() => {});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
document.addEventListener('fullscreenchange', () => {
|
|
303
|
+
const btn = qs('btn-fullscreen');
|
|
304
|
+
if (btn) {
|
|
305
|
+
btn.textContent = document.fullscreenElement ? 'Exit Fullscreen' : 'Fullscreen';
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
qs('btn-fullscreen').addEventListener('click', toggleFullscreen);
|
|
310
|
+
|
|
311
|
+
document.addEventListener('keydown', (e) => {
|
|
312
|
+
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
toggleFullscreen();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Ensure Terminal is available (wait for script load if needed)
|
|
319
|
+
function ensureTerminal(cb) {
|
|
320
|
+
if (typeof Terminal !== 'undefined') {
|
|
321
|
+
cb();
|
|
322
|
+
} else {
|
|
323
|
+
setTimeout(() => ensureTerminal(cb), 50);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
</script>
|
|
327
|
+
</body>
|
|
328
|
+
</html>
|