lobstakit-cloud 1.3.9 → 1.3.10
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/package.json +1 -1
- package/public/js/manage.js +112 -0
- package/public/manage.html +9 -0
- package/server.js +31 -1
package/package.json
CHANGED
package/public/js/manage.js
CHANGED
|
@@ -96,6 +96,8 @@ const SETTINGS_PROVIDER_FALLBACK = {
|
|
|
96
96
|
|
|
97
97
|
let settingsProviders = SETTINGS_PROVIDER_FALLBACK;
|
|
98
98
|
let currentAiSettings = null;
|
|
99
|
+
let terminalHistory = [];
|
|
100
|
+
let terminalHistoryIndex = -1;
|
|
99
101
|
|
|
100
102
|
async function logout() {
|
|
101
103
|
const token = localStorage.getItem('lobstakit_token');
|
|
@@ -229,6 +231,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
229
231
|
refreshVersionStatus();
|
|
230
232
|
loadAccountEmail();
|
|
231
233
|
checkMissionControlAccess();
|
|
234
|
+
initTerminal();
|
|
232
235
|
|
|
233
236
|
// Auto-refresh
|
|
234
237
|
setInterval(fetchStatus, 10000);
|
|
@@ -243,6 +246,115 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
243
246
|
setInterval(refreshVersionStatus, 60000);
|
|
244
247
|
});
|
|
245
248
|
|
|
249
|
+
function escapeHtml(value) {
|
|
250
|
+
return String(value || '')
|
|
251
|
+
.replace(/&/g, '&')
|
|
252
|
+
.replace(/</g, '<')
|
|
253
|
+
.replace(/>/g, '>')
|
|
254
|
+
.replace(/"/g, '"')
|
|
255
|
+
.replace(/'/g, ''');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function appendTerminalOutput(html) {
|
|
259
|
+
const outputEl = document.getElementById('terminal-output');
|
|
260
|
+
if (!outputEl) return;
|
|
261
|
+
outputEl.insertAdjacentHTML('beforeend', html);
|
|
262
|
+
outputEl.scrollTop = outputEl.scrollHeight;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function setTerminalRunning(isRunning) {
|
|
266
|
+
const runBtn = document.getElementById('terminal-run-btn');
|
|
267
|
+
if (!runBtn) return;
|
|
268
|
+
runBtn.disabled = isRunning;
|
|
269
|
+
runBtn.textContent = isRunning ? 'Running...' : 'Run';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function terminalHistoryUp(inputEl) {
|
|
273
|
+
if (!terminalHistory.length) return;
|
|
274
|
+
if (terminalHistoryIndex <= 0) {
|
|
275
|
+
terminalHistoryIndex = 0;
|
|
276
|
+
} else {
|
|
277
|
+
terminalHistoryIndex -= 1;
|
|
278
|
+
}
|
|
279
|
+
inputEl.value = terminalHistory[terminalHistoryIndex];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function terminalHistoryDown(inputEl) {
|
|
283
|
+
if (!terminalHistory.length) return;
|
|
284
|
+
if (terminalHistoryIndex >= terminalHistory.length - 1) {
|
|
285
|
+
terminalHistoryIndex = terminalHistory.length;
|
|
286
|
+
inputEl.value = '';
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
terminalHistoryIndex += 1;
|
|
290
|
+
inputEl.value = terminalHistory[terminalHistoryIndex];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function runTerminalCommand() {
|
|
294
|
+
const inputEl = document.getElementById('terminal-input');
|
|
295
|
+
if (!inputEl) return;
|
|
296
|
+
|
|
297
|
+
const command = inputEl.value.trim();
|
|
298
|
+
if (!command) return;
|
|
299
|
+
|
|
300
|
+
terminalHistory.push(command);
|
|
301
|
+
terminalHistoryIndex = terminalHistory.length;
|
|
302
|
+
inputEl.value = '';
|
|
303
|
+
setTerminalRunning(true);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const res = await fetch('/api/run-command', authFetchOpts({
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
body: JSON.stringify({ command: command })
|
|
310
|
+
}));
|
|
311
|
+
const data = await res.json();
|
|
312
|
+
|
|
313
|
+
if (!res.ok) {
|
|
314
|
+
const requestError = escapeHtml(data.error || 'Request failed');
|
|
315
|
+
appendTerminalOutput(`$ ${escapeHtml(command)}\n<span style="color:#f85149">${requestError}</span>\n\n`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const stdout = escapeHtml(data.stdout || '');
|
|
320
|
+
const stderr = escapeHtml(data.stderr || '');
|
|
321
|
+
const hasError = Number(data.exitCode) !== 0;
|
|
322
|
+
const stderrBlock = stderr
|
|
323
|
+
? (hasError ? `<span style="color:#f85149">${stderr}</span>` : stderr)
|
|
324
|
+
: '';
|
|
325
|
+
|
|
326
|
+
appendTerminalOutput(`$ ${escapeHtml(command)}\n${stdout}${stderrBlock}\n\n`);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
appendTerminalOutput(`$ ${escapeHtml(command)}\n<span style="color:#f85149">${escapeHtml(err.message || 'Connection error')}</span>\n\n`);
|
|
329
|
+
} finally {
|
|
330
|
+
setTerminalRunning(false);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function initTerminal() {
|
|
335
|
+
const inputEl = document.getElementById('terminal-input');
|
|
336
|
+
const runBtn = document.getElementById('terminal-run-btn');
|
|
337
|
+
if (!inputEl || !runBtn) return;
|
|
338
|
+
|
|
339
|
+
runBtn.addEventListener('click', runTerminalCommand);
|
|
340
|
+
inputEl.addEventListener('keydown', (event) => {
|
|
341
|
+
if (event.key === 'Enter') {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
runTerminalCommand();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (event.key === 'ArrowUp') {
|
|
347
|
+
event.preventDefault();
|
|
348
|
+
terminalHistoryUp(inputEl);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (event.key === 'ArrowDown') {
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
terminalHistoryDown(inputEl);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
246
358
|
// ─── Gateway Status ──────────────────────────────────────────
|
|
247
359
|
|
|
248
360
|
async function fetchStatus() {
|
package/public/manage.html
CHANGED
|
@@ -187,6 +187,15 @@
|
|
|
187
187
|
</div>
|
|
188
188
|
</div>
|
|
189
189
|
|
|
190
|
+
<div class="card" id="terminal-section">
|
|
191
|
+
<h3>🖥️ Terminal</h3>
|
|
192
|
+
<div class="terminal-output" id="terminal-output" style="min-height:120px;max-height:300px;overflow-y:auto;background:#0d1117;color:#e6edf3;font-family:monospace;font-size:13px;padding:12px;border-radius:6px;margin-bottom:10px;white-space:pre-wrap;"></div>
|
|
193
|
+
<div style="display:flex;gap:8px;">
|
|
194
|
+
<input type="text" id="terminal-input" placeholder="Enter command..." style="flex:1;font-family:monospace;font-size:13px;" />
|
|
195
|
+
<button id="terminal-run-btn" class="btn btn-secondary">Run</button>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
190
199
|
<!-- Channels Management -->
|
|
191
200
|
<div class="card" id="channels-card">
|
|
192
201
|
<div class="flex items-center justify-between mb-4">
|
package/server.js
CHANGED
|
@@ -217,7 +217,7 @@ function requireAuth(req, res, next) {
|
|
|
217
217
|
// NEW-1 FIX: Even before password is set, protect destructive endpoints with setupToken
|
|
218
218
|
if (!lobstaConfig.passwordHash) {
|
|
219
219
|
// These endpoints MUST require setupToken even before password is set
|
|
220
|
-
const protectedPreSetup = ['/api/setup', '/api/restart', '/api/security/harden', '/api/settings', '/api/diagnostics', '/api/diagnostics/fix'];
|
|
220
|
+
const protectedPreSetup = ['/api/setup', '/api/restart', '/api/security/harden', '/api/settings', '/api/diagnostics', '/api/diagnostics/fix', '/api/run-command'];
|
|
221
221
|
const isProtected = protectedPreSetup.some(p => req.path === p) ||
|
|
222
222
|
req.path.startsWith('/api/channels/');
|
|
223
223
|
if (isProtected) {
|
|
@@ -895,6 +895,36 @@ app.post('/api/diagnostics/fix', (req, res) => {
|
|
|
895
895
|
});
|
|
896
896
|
});
|
|
897
897
|
|
|
898
|
+
/**
|
|
899
|
+
* POST /api/run-command — Execute a shell command from the dashboard terminal
|
|
900
|
+
*/
|
|
901
|
+
app.post('/api/run-command', (req, res) => {
|
|
902
|
+
const command = typeof req.body?.command === 'string' ? req.body.command.trim() : '';
|
|
903
|
+
|
|
904
|
+
if (!command) {
|
|
905
|
+
return res.status(400).json({ error: 'Command is required' });
|
|
906
|
+
}
|
|
907
|
+
if (command.length > 500) {
|
|
908
|
+
return res.status(400).json({ error: 'Command too long (max 500 characters)' });
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
const stdout = execSync(command, {
|
|
913
|
+
encoding: 'utf8',
|
|
914
|
+
timeout: 30000,
|
|
915
|
+
shell: true
|
|
916
|
+
});
|
|
917
|
+
return res.json({ success: true, stdout, stderr: '', exitCode: 0 });
|
|
918
|
+
} catch (err) {
|
|
919
|
+
return res.json({
|
|
920
|
+
success: false,
|
|
921
|
+
stdout: err.stdout || '',
|
|
922
|
+
stderr: err.stderr || err.message,
|
|
923
|
+
exitCode: err.status || 1
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
|
|
898
928
|
/**
|
|
899
929
|
* GET /api/gateway-status — Check if gateway is running
|
|
900
930
|
*/
|