cli-tunnel 1.2.0-beta.9 → 1.2.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/README.md +98 -40
- package/dist/index.js +172 -60
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +26 -0
- package/package.json +7 -5
- package/remote-ui/app.js +365 -71
- package/remote-ui/index.html +10 -10
- package/remote-ui/styles.css +131 -3
package/remote-ui/app.js
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
const termContainer = $('#terminal-container');
|
|
48
48
|
let currentView = 'terminal'; // 'dashboard', 'terminal', or 'grid'
|
|
49
49
|
let cachedSessions = [];
|
|
50
|
-
let gridTerminals = []; // { xterm, fitAddon, ws, session }
|
|
50
|
+
let gridTerminals = []; // { xterm, fitAddon, ws, session, panel }
|
|
51
|
+
var gridMode = 'thumbnails';
|
|
52
|
+
var focusedIndex = 0;
|
|
53
|
+
var tmuxPreset = 'equal';
|
|
51
54
|
|
|
52
55
|
// ─── xterm.js Terminal ───────────────────────────────────
|
|
53
56
|
let xterm = null;
|
|
@@ -117,7 +120,9 @@
|
|
|
117
120
|
|
|
118
121
|
async function loadSessions() {
|
|
119
122
|
try {
|
|
120
|
-
const
|
|
123
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
124
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
125
|
+
const resp = await fetch('/api/sessions', { headers });
|
|
121
126
|
const data = await resp.json();
|
|
122
127
|
renderDashboard(data.sessions || []);
|
|
123
128
|
} catch (err) {
|
|
@@ -146,17 +151,17 @@
|
|
|
146
151
|
'</div>';
|
|
147
152
|
} else {
|
|
148
153
|
html += filtered.map(s => {
|
|
149
|
-
const
|
|
154
|
+
const hasAccess = s.hasToken;
|
|
150
155
|
return `
|
|
151
|
-
<div class="session-card" ${s.online &&
|
|
156
|
+
<div class="session-card" ${s.online && hasAccess ? 'data-session-port="' + s.port + '" data-session-base-url="' + escapeHtml(s.url) + '"' : ''}>
|
|
152
157
|
<span class="status-dot ${s.online ? 'online' : 'offline'}"></span>
|
|
153
158
|
<div class="info">
|
|
154
159
|
<div class="session-name">${escapeHtml(s.name)}</div>
|
|
155
160
|
<div class="repo">📦 ${escapeHtml(s.repo)}</div>
|
|
156
161
|
<div class="branch">🌿 ${escapeHtml(s.branch)}</div>
|
|
157
|
-
<div class="machine">💻 ${escapeHtml(s.machine)}${!
|
|
162
|
+
<div class="machine">💻 ${escapeHtml(s.machine)}${!hasAccess && s.online ? ' 🔒' : ''}</div>
|
|
158
163
|
</div>
|
|
159
|
-
${s.online &&
|
|
164
|
+
${s.online && hasAccess ? '<span class="arrow">→</span>' :
|
|
160
165
|
!s.online ? '<button data-delete-id="' + escapeHtml(s.id) + '" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:14px" title="Remove">✕</button>'
|
|
161
166
|
: '<span style="color:var(--text-dim);font-size:11px">remote</span>'}
|
|
162
167
|
</div>`;
|
|
@@ -165,8 +170,25 @@
|
|
|
165
170
|
dashboard.innerHTML = html;
|
|
166
171
|
cachedSessions = sessions;
|
|
167
172
|
// Event delegation
|
|
168
|
-
dashboard.querySelectorAll('.session-card[data-session-
|
|
169
|
-
card.addEventListener('click', function() {
|
|
173
|
+
dashboard.querySelectorAll('.session-card[data-session-port]').forEach(function(card) {
|
|
174
|
+
card.addEventListener('click', function() {
|
|
175
|
+
var port = card.dataset.sessionPort;
|
|
176
|
+
var baseUrl = card.dataset.sessionBaseUrl;
|
|
177
|
+
var tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
178
|
+
var proxyUrl = '/api/proxy/ticket/' + port;
|
|
179
|
+
fetch(proxyUrl, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {}
|
|
182
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
183
|
+
if (data.ticket) {
|
|
184
|
+
window.location.href = baseUrl + '?ticket=' + encodeURIComponent(data.ticket);
|
|
185
|
+
} else {
|
|
186
|
+
window.location.href = baseUrl;
|
|
187
|
+
}
|
|
188
|
+
}).catch(function() {
|
|
189
|
+
window.location.href = baseUrl;
|
|
190
|
+
});
|
|
191
|
+
});
|
|
170
192
|
});
|
|
171
193
|
dashboard.querySelectorAll('[data-delete-id]').forEach(function(btn) {
|
|
172
194
|
btn.addEventListener('click', function(e) { e.stopPropagation(); deleteSession(btn.dataset.deleteId); });
|
|
@@ -187,29 +209,36 @@
|
|
|
187
209
|
};
|
|
188
210
|
|
|
189
211
|
window.cleanOffline = async () => {
|
|
190
|
-
const
|
|
212
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
213
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
214
|
+
const resp = await fetch('/api/sessions', { headers });
|
|
191
215
|
const data = await resp.json();
|
|
192
216
|
const offline = (data.sessions || []).filter(s => !s.online);
|
|
193
217
|
for (const s of offline) {
|
|
194
|
-
await fetch('/api/sessions/' + s.id, { method: 'DELETE' });
|
|
218
|
+
await fetch('/api/sessions/' + s.id, { method: 'DELETE', headers });
|
|
195
219
|
}
|
|
196
220
|
loadSessions();
|
|
197
221
|
};
|
|
198
222
|
|
|
199
223
|
window.deleteSession = async (id) => {
|
|
200
|
-
|
|
224
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
225
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
226
|
+
await fetch('/api/sessions/' + id, { method: 'DELETE', headers });
|
|
201
227
|
loadSessions();
|
|
202
228
|
};
|
|
203
229
|
|
|
204
|
-
// ─── Grid View (
|
|
230
|
+
// ─── Grid View (multi-terminal with layout modes) ───────────
|
|
205
231
|
function showGridView(sessions) {
|
|
206
|
-
|
|
232
|
+
var connectable = sessions.filter(function(s) { return s.online && s.token; });
|
|
207
233
|
if (connectable.length === 0) return;
|
|
208
234
|
|
|
209
235
|
// Clean up previous grid
|
|
210
236
|
destroyGrid();
|
|
211
237
|
|
|
212
238
|
currentView = 'grid';
|
|
239
|
+
gridMode = 'thumbnails';
|
|
240
|
+
focusedIndex = 0;
|
|
241
|
+
tmuxPreset = 'equal';
|
|
213
242
|
dashboard.classList.add('hidden');
|
|
214
243
|
terminal.classList.add('hidden');
|
|
215
244
|
termContainer.classList.add('hidden');
|
|
@@ -224,30 +253,95 @@
|
|
|
224
253
|
gridEl.classList.remove('hidden');
|
|
225
254
|
gridEl.innerHTML = '';
|
|
226
255
|
|
|
227
|
-
//
|
|
228
|
-
var
|
|
229
|
-
|
|
256
|
+
// ── Toolbar ──
|
|
257
|
+
var toolbar = document.createElement('div');
|
|
258
|
+
toolbar.className = 'grid-toolbar';
|
|
259
|
+
|
|
260
|
+
var modes = [
|
|
261
|
+
{ id: 'thumbnails', label: '\u229E Tiles' },
|
|
262
|
+
{ id: 'tmux', label: '\u229F Tmux' },
|
|
263
|
+
{ id: 'focus', label: '\u25C9 Focus' },
|
|
264
|
+
{ id: 'fullscreen', label: '\u2A21 Full' }
|
|
265
|
+
];
|
|
266
|
+
modes.forEach(function(m) {
|
|
267
|
+
var btn = document.createElement('button');
|
|
268
|
+
btn.textContent = m.label;
|
|
269
|
+
btn.dataset.mode = m.id;
|
|
270
|
+
if (m.id === gridMode) btn.classList.add('active');
|
|
271
|
+
btn.addEventListener('click', function() { switchGridMode(m.id); });
|
|
272
|
+
toolbar.appendChild(btn);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Tmux preset buttons (visible only in tmux mode)
|
|
276
|
+
var presetGroup = document.createElement('span');
|
|
277
|
+
presetGroup.className = 'grid-toolbar-presets hidden';
|
|
278
|
+
presetGroup.id = 'tmux-presets';
|
|
279
|
+
var presets = [
|
|
280
|
+
{ id: 'equal', label: '\u2550 Equal' },
|
|
281
|
+
{ id: 'main-side', label: '\u2590 Main+Side' },
|
|
282
|
+
{ id: 'stacked', label: '\u2261 Stacked' }
|
|
283
|
+
];
|
|
284
|
+
presets.forEach(function(p) {
|
|
285
|
+
var btn = document.createElement('button');
|
|
286
|
+
btn.textContent = p.label;
|
|
287
|
+
btn.dataset.preset = p.id;
|
|
288
|
+
if (p.id === tmuxPreset) btn.classList.add('active');
|
|
289
|
+
btn.addEventListener('click', function() { switchTmuxPreset(p.id); });
|
|
290
|
+
presetGroup.appendChild(btn);
|
|
291
|
+
});
|
|
292
|
+
toolbar.appendChild(presetGroup);
|
|
293
|
+
|
|
294
|
+
var spacer = document.createElement('span');
|
|
295
|
+
spacer.className = 'spacer';
|
|
296
|
+
toolbar.appendChild(spacer);
|
|
297
|
+
|
|
298
|
+
var listBtn = document.createElement('button');
|
|
299
|
+
listBtn.textContent = '\u2190 List';
|
|
300
|
+
listBtn.addEventListener('click', function() {
|
|
301
|
+
destroyGrid();
|
|
302
|
+
currentView = 'dashboard';
|
|
303
|
+
dashboard.classList.remove('hidden');
|
|
304
|
+
if ($('#btn-sessions')) $('#btn-sessions').textContent = 'Terminal';
|
|
305
|
+
loadSessions();
|
|
306
|
+
});
|
|
307
|
+
toolbar.appendChild(listBtn);
|
|
308
|
+
gridEl.appendChild(toolbar);
|
|
309
|
+
|
|
310
|
+
// ── Content container ──
|
|
311
|
+
var contentEl = document.createElement('div');
|
|
312
|
+
contentEl.id = 'grid-content';
|
|
313
|
+
gridEl.appendChild(contentEl);
|
|
230
314
|
|
|
231
|
-
|
|
315
|
+
// ── Create panels & connect ──
|
|
316
|
+
connectable.forEach(function(s, index) {
|
|
232
317
|
var panel = document.createElement('div');
|
|
233
318
|
panel.className = 'grid-panel';
|
|
319
|
+
panel.dataset.index = index;
|
|
234
320
|
|
|
235
|
-
// Header
|
|
236
321
|
var header = document.createElement('div');
|
|
237
322
|
header.className = 'grid-panel-header';
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
323
|
+
var nameSpan = document.createElement('span');
|
|
324
|
+
nameSpan.className = 'grid-panel-name';
|
|
325
|
+
nameSpan.textContent = s.name;
|
|
326
|
+
var machineSpan = document.createElement('span');
|
|
327
|
+
machineSpan.className = 'grid-panel-machine';
|
|
328
|
+
machineSpan.textContent = s.machine;
|
|
329
|
+
var statusDot = document.createElement('span');
|
|
330
|
+
statusDot.className = 'grid-panel-status';
|
|
331
|
+
statusDot.textContent = '\u25CF';
|
|
332
|
+
header.appendChild(nameSpan);
|
|
333
|
+
header.appendChild(machineSpan);
|
|
334
|
+
header.appendChild(statusDot);
|
|
241
335
|
panel.appendChild(header);
|
|
242
336
|
|
|
243
|
-
// Terminal container
|
|
244
337
|
var termDiv = document.createElement('div');
|
|
245
338
|
termDiv.className = 'grid-panel-terminal';
|
|
246
339
|
panel.appendChild(termDiv);
|
|
247
340
|
|
|
248
|
-
|
|
341
|
+
// Append to contentEl so xterm.open has a DOM-attached container
|
|
342
|
+
contentEl.appendChild(panel);
|
|
249
343
|
|
|
250
|
-
//
|
|
344
|
+
// xterm instance
|
|
251
345
|
var panelXterm = new Terminal({
|
|
252
346
|
theme: {
|
|
253
347
|
background: '#0d1117', foreground: '#c9d1d9', cursor: '#3fb950',
|
|
@@ -262,60 +356,229 @@
|
|
|
262
356
|
panelXterm.loadAddon(panelFit);
|
|
263
357
|
panelXterm.open(termDiv);
|
|
264
358
|
|
|
265
|
-
//
|
|
266
|
-
|
|
359
|
+
// Store entry before async connect so index is stable
|
|
360
|
+
var entry = { xterm: panelXterm, fitAddon: panelFit, ws: null, session: s, panel: panel };
|
|
361
|
+
gridTerminals.push(entry);
|
|
267
362
|
|
|
268
363
|
// Connect WebSocket to this session
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
364
|
+
(function connectPanel() {
|
|
365
|
+
// Use hub's proxy endpoint to get a ticket for the session
|
|
366
|
+
var tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
367
|
+
var proxyUrl = '/api/proxy/ticket/' + s.port;
|
|
368
|
+
var wsBase = s.isLocal ? 'ws://127.0.0.1:' + s.port : s.url.replace('https://', 'wss://');
|
|
369
|
+
|
|
370
|
+
fetch(proxyUrl, {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
headers: { 'Authorization': 'Bearer ' + tokenParam }
|
|
373
|
+
}).then(function(resp) {
|
|
374
|
+
if (!resp.ok) throw new Error('Auth failed');
|
|
375
|
+
return resp.json();
|
|
376
|
+
}).then(function(data) {
|
|
377
|
+
var panelWs = new WebSocket(wsBase + '?ticket=' + encodeURIComponent(data.ticket));
|
|
378
|
+
entry.ws = panelWs;
|
|
379
|
+
|
|
380
|
+
panelWs.onopen = function() {
|
|
381
|
+
if (statusDot) { statusDot.style.color = 'var(--green)'; statusDot.title = 'Connected'; }
|
|
382
|
+
panelWs.send(JSON.stringify({ type: 'pty_resize', cols: panelXterm.cols, rows: panelXterm.rows }));
|
|
383
|
+
};
|
|
384
|
+
panelWs.onclose = function() {
|
|
385
|
+
if (statusDot) { statusDot.style.color = 'var(--red)'; statusDot.title = 'Disconnected'; }
|
|
386
|
+
};
|
|
387
|
+
panelWs.onerror = function() {
|
|
388
|
+
if (statusDot) { statusDot.style.color = 'var(--red)'; }
|
|
389
|
+
};
|
|
390
|
+
panelWs.onmessage = function(e) {
|
|
391
|
+
try {
|
|
392
|
+
var msg = JSON.parse(e.data);
|
|
393
|
+
if (msg.type === 'pty') {
|
|
394
|
+
panelXterm.write(msg.data);
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
panelXterm.onData(function(data) {
|
|
400
|
+
if (panelWs && panelWs.readyState === WebSocket.OPEN) {
|
|
401
|
+
panelWs.send(JSON.stringify({ type: 'pty_input', data: data }));
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}).catch(function() {
|
|
405
|
+
if (statusDot) { statusDot.style.color = 'var(--red)'; statusDot.title = 'Auth failed'; }
|
|
406
|
+
});
|
|
407
|
+
})();
|
|
408
|
+
});
|
|
293
409
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
410
|
+
// ── Event delegation for panel clicks ──
|
|
411
|
+
contentEl.addEventListener('click', function(e) {
|
|
412
|
+
var panel = e.target.closest('.grid-panel');
|
|
413
|
+
if (!panel) return;
|
|
414
|
+
var idx = parseInt(panel.dataset.index, 10);
|
|
415
|
+
if (isNaN(idx)) return;
|
|
416
|
+
|
|
417
|
+
if (gridMode === 'thumbnails') {
|
|
418
|
+
focusedIndex = idx;
|
|
419
|
+
switchGridMode('fullscreen');
|
|
420
|
+
} else if (gridMode === 'focus' && panel.classList.contains('focus-strip')) {
|
|
421
|
+
focusedIndex = idx;
|
|
422
|
+
applyGridLayout('focus');
|
|
423
|
+
} else if (gridMode === 'tmux') {
|
|
424
|
+
focusedIndex = idx;
|
|
425
|
+
contentEl.querySelectorAll('.grid-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
426
|
+
panel.classList.add('active');
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Apply initial layout
|
|
431
|
+
applyGridLayout(gridMode);
|
|
432
|
+
|
|
433
|
+
// Handle window resize
|
|
434
|
+
window.removeEventListener('resize', fitGridPanels);
|
|
435
|
+
window.addEventListener('resize', fitGridPanels);
|
|
436
|
+
if ($('#btn-sessions')) { $('#btn-sessions').textContent = 'List'; }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function switchGridMode(mode) {
|
|
440
|
+
gridMode = mode;
|
|
441
|
+
if (mode === 'fullscreen') {
|
|
442
|
+
$('#input-area').classList.remove('hidden');
|
|
443
|
+
$('#input-form').classList.add('hidden');
|
|
444
|
+
} else {
|
|
445
|
+
$('#input-area').classList.add('hidden');
|
|
446
|
+
}
|
|
447
|
+
applyGridLayout(mode);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function switchTmuxPreset(preset) {
|
|
451
|
+
tmuxPreset = preset;
|
|
452
|
+
var presetGroup = document.getElementById('tmux-presets');
|
|
453
|
+
if (presetGroup) {
|
|
454
|
+
presetGroup.querySelectorAll('[data-preset]').forEach(function(btn) {
|
|
455
|
+
btn.classList.toggle('active', btn.dataset.preset === preset);
|
|
299
456
|
});
|
|
457
|
+
}
|
|
458
|
+
if (gridMode === 'tmux') applyGridLayout('tmux');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function applyGridLayout(mode) {
|
|
462
|
+
gridMode = mode;
|
|
463
|
+
var contentEl = document.getElementById('grid-content');
|
|
464
|
+
if (!contentEl || gridTerminals.length === 0) return;
|
|
465
|
+
|
|
466
|
+
// Clamp focusedIndex
|
|
467
|
+
if (focusedIndex >= gridTerminals.length) focusedIndex = 0;
|
|
300
468
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
469
|
+
// Update toolbar button states
|
|
470
|
+
var toolbar = contentEl.parentElement.querySelector('.grid-toolbar');
|
|
471
|
+
if (toolbar) {
|
|
472
|
+
toolbar.querySelectorAll('[data-mode]').forEach(function(btn) {
|
|
473
|
+
btn.classList.toggle('active', btn.dataset.mode === mode);
|
|
304
474
|
});
|
|
475
|
+
var presetsEl = document.getElementById('tmux-presets');
|
|
476
|
+
if (presetsEl) presetsEl.classList.toggle('hidden', mode !== 'tmux');
|
|
477
|
+
}
|
|
305
478
|
|
|
306
|
-
|
|
479
|
+
// Detach all panels without destroying them
|
|
480
|
+
gridTerminals.forEach(function(gt, i) {
|
|
481
|
+
if (gt.panel.parentNode) gt.panel.parentNode.removeChild(gt.panel);
|
|
482
|
+
gt.panel.className = 'grid-panel';
|
|
483
|
+
gt.panel.dataset.index = i;
|
|
484
|
+
var termDiv = gt.panel.querySelector('.grid-panel-terminal');
|
|
485
|
+
if (termDiv) termDiv.style.cssText = '';
|
|
486
|
+
gt.panel.style.cssText = '';
|
|
307
487
|
});
|
|
308
488
|
|
|
309
|
-
//
|
|
310
|
-
|
|
489
|
+
// Remove leftover elements (focus-strips, back-to-grid button)
|
|
490
|
+
while (contentEl.firstChild) contentEl.removeChild(contentEl.firstChild);
|
|
491
|
+
|
|
492
|
+
// Reset content styles
|
|
493
|
+
contentEl.className = 'mode-' + mode;
|
|
494
|
+
contentEl.style.cssText = '';
|
|
495
|
+
|
|
496
|
+
switch (mode) {
|
|
497
|
+
case 'thumbnails':
|
|
498
|
+
gridTerminals.forEach(function(gt) {
|
|
499
|
+
gt.panel.classList.add('thumbnail');
|
|
500
|
+
var termDiv = gt.panel.querySelector('.grid-panel-terminal');
|
|
501
|
+
termDiv.style.width = '560px';
|
|
502
|
+
termDiv.style.height = '360px';
|
|
503
|
+
termDiv.style.transform = 'scale(0.5)';
|
|
504
|
+
termDiv.style.transformOrigin = 'top left';
|
|
505
|
+
contentEl.appendChild(gt.panel);
|
|
506
|
+
});
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
case 'tmux':
|
|
510
|
+
gridTerminals.forEach(function(gt, i) {
|
|
511
|
+
if (i === focusedIndex) gt.panel.classList.add('active');
|
|
512
|
+
contentEl.appendChild(gt.panel);
|
|
513
|
+
});
|
|
514
|
+
if (tmuxPreset === 'equal') {
|
|
515
|
+
var cols = gridTerminals.length <= 2 ? gridTerminals.length : gridTerminals.length <= 4 ? 2 : 3;
|
|
516
|
+
contentEl.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
|
517
|
+
} else if (tmuxPreset === 'main-side') {
|
|
518
|
+
contentEl.style.gridTemplateColumns = '70% 30%';
|
|
519
|
+
var sideCount = Math.max(gridTerminals.length - 1, 1);
|
|
520
|
+
contentEl.style.gridTemplateRows = 'repeat(' + sideCount + ', 1fr)';
|
|
521
|
+
if (gridTerminals.length > 0) gridTerminals[0].panel.style.gridRow = '1 / -1';
|
|
522
|
+
} else if (tmuxPreset === 'stacked') {
|
|
523
|
+
contentEl.style.gridTemplateColumns = '1fr';
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
|
|
527
|
+
case 'focus':
|
|
528
|
+
var mainGt = gridTerminals[focusedIndex];
|
|
529
|
+
mainGt.panel.classList.add('focus-main');
|
|
530
|
+
contentEl.appendChild(mainGt.panel);
|
|
531
|
+
if (gridTerminals.length > 1) {
|
|
532
|
+
var stripsEl = document.createElement('div');
|
|
533
|
+
stripsEl.className = 'focus-strips';
|
|
534
|
+
gridTerminals.forEach(function(gt, i) {
|
|
535
|
+
if (i === focusedIndex) return;
|
|
536
|
+
gt.panel.classList.add('focus-strip');
|
|
537
|
+
stripsEl.appendChild(gt.panel);
|
|
538
|
+
});
|
|
539
|
+
contentEl.appendChild(stripsEl);
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
|
|
543
|
+
case 'fullscreen':
|
|
544
|
+
var fullGt = gridTerminals[focusedIndex];
|
|
545
|
+
fullGt.panel.classList.add('fullscreen');
|
|
546
|
+
contentEl.appendChild(fullGt.panel);
|
|
547
|
+
var backBtn = document.createElement('button');
|
|
548
|
+
backBtn.className = 'back-to-grid';
|
|
549
|
+
backBtn.textContent = '\u2190 Grid';
|
|
550
|
+
backBtn.addEventListener('click', function() { switchGridMode('thumbnails'); });
|
|
551
|
+
contentEl.appendChild(backBtn);
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
311
554
|
|
|
312
|
-
//
|
|
313
|
-
|
|
555
|
+
// Fit visible terminals after DOM settles
|
|
556
|
+
setTimeout(function() {
|
|
557
|
+
gridTerminals.forEach(function(gt) {
|
|
558
|
+
if (!document.contains(gt.panel)) return;
|
|
559
|
+
if (gt.fitAddon) {
|
|
560
|
+
try {
|
|
561
|
+
gt.fitAddon.fit();
|
|
562
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN && gt.xterm) {
|
|
563
|
+
gt.ws.send(JSON.stringify({ type: 'pty_resize', cols: gt.xterm.cols, rows: gt.xterm.rows }));
|
|
564
|
+
}
|
|
565
|
+
} catch(e) {}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}, 100);
|
|
314
569
|
}
|
|
315
570
|
|
|
316
571
|
function fitGridPanels() {
|
|
317
572
|
gridTerminals.forEach(function(gt) {
|
|
318
|
-
if (
|
|
573
|
+
if (!document.contains(gt.panel)) return;
|
|
574
|
+
if (gt.fitAddon) {
|
|
575
|
+
try {
|
|
576
|
+
gt.fitAddon.fit();
|
|
577
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN && gt.xterm) {
|
|
578
|
+
gt.ws.send(JSON.stringify({ type: 'pty_resize', cols: gt.xterm.cols, rows: gt.xterm.rows }));
|
|
579
|
+
}
|
|
580
|
+
} catch(e) {}
|
|
581
|
+
}
|
|
319
582
|
});
|
|
320
583
|
}
|
|
321
584
|
|
|
@@ -328,6 +591,10 @@
|
|
|
328
591
|
window.removeEventListener('resize', fitGridPanels);
|
|
329
592
|
var gridEl = document.getElementById('grid-view');
|
|
330
593
|
if (gridEl) { gridEl.innerHTML = ''; gridEl.classList.add('hidden'); }
|
|
594
|
+
$('#input-area').classList.add('hidden');
|
|
595
|
+
gridMode = 'thumbnails';
|
|
596
|
+
focusedIndex = 0;
|
|
597
|
+
tmuxPreset = 'equal';
|
|
331
598
|
}
|
|
332
599
|
|
|
333
600
|
window.toggleView = () => {
|
|
@@ -519,7 +786,7 @@
|
|
|
519
786
|
}
|
|
520
787
|
|
|
521
788
|
// ─── Detect hub mode (no token in URL) ────────────────────
|
|
522
|
-
const isHubMode =
|
|
789
|
+
const isHubMode = new URLSearchParams(window.location.search).get('hub') === '1';
|
|
523
790
|
|
|
524
791
|
// ─── WebSocket ───────────────────────────────────────────
|
|
525
792
|
let reconnectAttempt = 0;
|
|
@@ -544,7 +811,7 @@
|
|
|
544
811
|
|
|
545
812
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
546
813
|
|
|
547
|
-
// F-02:
|
|
814
|
+
// F-02: Ticket-based auth (required)
|
|
548
815
|
try {
|
|
549
816
|
const resp = await fetch('/api/auth/ticket', {
|
|
550
817
|
method: 'POST',
|
|
@@ -554,12 +821,12 @@
|
|
|
554
821
|
const { ticket } = await resp.json();
|
|
555
822
|
ws = new WebSocket(`${proto}//${location.host}?ticket=${encodeURIComponent(ticket)}`);
|
|
556
823
|
} else {
|
|
557
|
-
|
|
558
|
-
|
|
824
|
+
setStatus('offline', 'Auth failed');
|
|
825
|
+
return;
|
|
559
826
|
}
|
|
560
827
|
} catch {
|
|
561
|
-
|
|
562
|
-
|
|
828
|
+
setStatus('offline', 'Auth failed');
|
|
829
|
+
return;
|
|
563
830
|
}
|
|
564
831
|
setStatus('connecting', 'Connecting...');
|
|
565
832
|
|
|
@@ -714,6 +981,33 @@
|
|
|
714
981
|
};
|
|
715
982
|
|
|
716
983
|
// ─── Mobile Key Bar ───────────────────────────────────────
|
|
984
|
+
// F-5: Event delegation for key-bar buttons (no inline onclick)
|
|
985
|
+
const keyBar = document.getElementById('key-bar');
|
|
986
|
+
if (keyBar) {
|
|
987
|
+
var keyMap = {
|
|
988
|
+
'\\x1b[A': '\x1b[A', '\\x1b[B': '\x1b[B', '\\x1b[C': '\x1b[C', '\\x1b[D': '\x1b[D',
|
|
989
|
+
'\\t': '\t', '\\r': '\r', '\\x1b': '\x1b', '\\x03': '\x03', ' ': ' ', '\\x7f': '\x7f',
|
|
990
|
+
};
|
|
991
|
+
keyBar.addEventListener('click', function(e) {
|
|
992
|
+
var btn = e.target;
|
|
993
|
+
if (btn && btn.tagName === 'BUTTON' && btn.dataset.key) {
|
|
994
|
+
var key = keyMap[btn.dataset.key] || btn.dataset.key;
|
|
995
|
+
if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
|
|
996
|
+
var gt = gridTerminals[focusedIndex];
|
|
997
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN) {
|
|
998
|
+
gt.ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
999
|
+
}
|
|
1000
|
+
if (gt.xterm) gt.xterm.focus();
|
|
1001
|
+
} else {
|
|
1002
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1003
|
+
ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
1004
|
+
}
|
|
1005
|
+
if (xterm) xterm.focus();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
717
1011
|
window.sendKey = (key) => {
|
|
718
1012
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
719
1013
|
ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
@@ -758,7 +1052,7 @@
|
|
|
758
1052
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
759
1053
|
}
|
|
760
1054
|
function escapeHtml(s) {
|
|
761
|
-
const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML.replace(/'/g, ''');
|
|
1055
|
+
const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML.replace(/'/g, ''').replace(/"/g, '"');
|
|
762
1056
|
}
|
|
763
1057
|
function formatText(text) {
|
|
764
1058
|
return escapeHtml(text)
|
package/remote-ui/index.html
CHANGED
|
@@ -32,16 +32,16 @@
|
|
|
32
32
|
|
|
33
33
|
<footer id="input-area">
|
|
34
34
|
<div id="key-bar">
|
|
35
|
-
<button
|
|
36
|
-
<button
|
|
37
|
-
<button
|
|
38
|
-
<button
|
|
39
|
-
<button
|
|
40
|
-
<button
|
|
41
|
-
<button
|
|
42
|
-
<button
|
|
43
|
-
<button
|
|
44
|
-
<button
|
|
35
|
+
<button data-key="\x1b[A">↑</button>
|
|
36
|
+
<button data-key="\x1b[B">↓</button>
|
|
37
|
+
<button data-key="\x1b[C">→</button>
|
|
38
|
+
<button data-key="\x1b[D">←</button>
|
|
39
|
+
<button data-key="\t">Tab</button>
|
|
40
|
+
<button data-key="\r">Enter</button>
|
|
41
|
+
<button data-key="\x1b">Esc</button>
|
|
42
|
+
<button data-key="\x03">Ctrl+C</button>
|
|
43
|
+
<button data-key=" ">Space</button>
|
|
44
|
+
<button data-key="\x7f">⌫</button>
|
|
45
45
|
</div>
|
|
46
46
|
<form id="input-form">
|
|
47
47
|
<span class="prompt">></span>
|