feat: openSession — full remote routing via sourceUrl

This commit is contained in:
Brian Krabach
2026-03-31 03:40:57 -07:00
parent 3444310bb2
commit ac0b84c1d9
9 changed files with 5499 additions and 5 deletions
+9 -4
View File
@@ -120,6 +120,7 @@ const MOBILE_THRESHOLD = 600;
let _deviceId = '';
let _currentSessions = [];
let _viewingSession = null;
let _viewingSourceUrl = '';
let _viewMode = 'grid';
let _lastInteractionAt = Date.now() / 1000;
let _pollingTimer;
@@ -1091,6 +1092,7 @@ function updatePillBell() {
async function openSession(name, opts = {}) {
hidePreview();
_viewingSession = name;
_viewingSourceUrl = opts.sourceUrl || '';
_viewMode = 'fullscreen';
// Pre-render sidebar with current sessions before first poll tick
@@ -1156,11 +1158,14 @@ async function openSession(name, opts = {}) {
if (fab) fab.classList.add('hidden');
// Connect to session (kill old ttyd, spawn new one for this session)
var sourceUrl = opts.sourceUrl || '';
var _sourceUrl = opts.sourceUrl || '';
try {
if (!opts.skipConnect) {
if (!sourceUrl) {
await api('POST', `/api/sessions/${name}/connect`);
if (_sourceUrl) {
var remoteConnectUrl = _sourceUrl.replace(/\/+$/, '') + '/api/sessions/' + encodeURIComponent(name) + '/connect';
await fetch(remoteConnectUrl, { method: 'POST', credentials: 'include' });
} else {
await api('POST', '/api/sessions/' + encodeURIComponent(name) + '/connect');
}
}
} catch (err) {
@@ -1172,7 +1177,7 @@ async function openSession(name, opts = {}) {
await animDone;
// Mount terminal NOW — /connect has completed, new ttyd is serving the correct session
if (!sourceUrl && window._openTerminal) window._openTerminal(name);
if (window._openTerminal) window._openTerminal(name, _sourceUrl);
}
/**
+71 -1
View File
@@ -1046,6 +1046,76 @@ test('openSession shows toast and calls closeSession on connect failure', async
globalThis.setTimeout = origSetTimeout;
});
test('openSession with sourceUrl POSTs connect to remote instance URL', async () => {
const fetchCalls = [];
const origFetch = globalThis.fetch;
const origGetById = globalThis.document.getElementById;
const origQS = globalThis.document.querySelector;
const origSetTimeout = globalThis.setTimeout;
globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; };
globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } });
globalThis.document.querySelector = () => null;
globalThis.setTimeout = () => {};
globalThis.window._openTerminal = () => {};
await app.openSession('work-project', { sourceUrl: 'http://work:8088' });
const connectCall = fetchCalls.find((c) => c.url === 'http://work:8088/api/sessions/work-project/connect');
assert.ok(connectCall, 'should POST to http://work:8088/api/sessions/work-project/connect');
assert.strictEqual(connectCall.opts.method, 'POST');
assert.strictEqual(connectCall.opts.credentials, 'include');
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.querySelector = origQS;
globalThis.setTimeout = origSetTimeout;
});
test('openSession with sourceUrl passes sourceUrl to window._openTerminal', async () => {
let openTerminalArgs = null;
const origFetch = globalThis.fetch;
const origGetById = globalThis.document.getElementById;
const origQS = globalThis.document.querySelector;
const origSetTimeout = globalThis.setTimeout;
globalThis.fetch = async () => ({ ok: true });
globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } });
globalThis.document.querySelector = () => null;
globalThis.setTimeout = (fn) => { fn(); };
globalThis.window._openTerminal = (...args) => { openTerminalArgs = args; };
await app.openSession('my-session', { sourceUrl: 'http://work:8088' });
assert.ok(openTerminalArgs !== null, '_openTerminal should have been called');
assert.strictEqual(openTerminalArgs[0], 'my-session', '_openTerminal first arg should be session name');
assert.strictEqual(openTerminalArgs[1], 'http://work:8088', '_openTerminal second arg should be sourceUrl');
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.querySelector = origQS;
globalThis.setTimeout = origSetTimeout;
});
test('openSession without sourceUrl still POSTs to local /api/sessions/{name}/connect', async () => {
const fetchCalls = [];
const origFetch = globalThis.fetch;
const origGetById = globalThis.document.getElementById;
const origQS = globalThis.document.querySelector;
const origSetTimeout = globalThis.setTimeout;
globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; };
globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } });
globalThis.document.querySelector = () => null;
globalThis.setTimeout = () => {};
globalThis.window._openTerminal = () => {};
await app.openSession('local-session', {});
const connectCall = fetchCalls.find((c) => c.url === '/api/sessions/local-session/connect');
assert.ok(connectCall, 'should POST to /api/sessions/local-session/connect');
assert.strictEqual(connectCall.opts.method, 'POST');
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.querySelector = origQS;
globalThis.setTimeout = origSetTimeout;
});
// --- closeSession ---
test('closeSession is exported', () => {
@@ -1643,7 +1713,7 @@ test('openSession mounts terminal AFTER connect POST, not inside animation timer
// Find the openSession function body
const fnStart = source.indexOf('async function openSession');
const fnBody = source.substring(fnStart, fnStart + 3000);
const fnBody = source.substring(fnStart, fnStart + 4000);
// _openTerminal must NOT appear inside setTimeout
const setTimeoutIdx = fnBody.indexOf('setTimeout');