fix: guard openSession against empty name from unreachable federation tiles

Clicking an unreachable remote device tile passed empty session name
to openSession(), producing /api/federation/2/connect/ (no session name)
→ 405. Fix: bail early if name is empty or whitespace.

Also added guard in grid click handlers to skip error/status tiles
entirely, preventing clicks on unreachable or auth_failed tiles.
This commit is contained in:
Brian Krabach
2026-04-06 05:43:09 -07:00
parent 39f076502f
commit 4beffc2cf9
2 changed files with 51 additions and 0 deletions
+5
View File
@@ -839,10 +839,14 @@ function renderGrid(sessions) {
on(tile, 'click', (e) => { on(tile, 'click', (e) => {
// Don't navigate when clicking the delete button inside the tile // Don't navigate when clicking the delete button inside the tile
if (e.target.closest && e.target.closest('.tile-delete')) return; if (e.target.closest && e.target.closest('.tile-delete')) return;
// Don't open error/status tiles (unreachable, auth_failed)
if (tile.classList.contains('source-tile--error') || !tile.dataset.session) return;
openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' }); openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' });
}); });
on(tile, 'keydown', (e) => { on(tile, 'keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
// Don't open error/status tiles (unreachable, auth_failed)
if (tile.classList.contains('source-tile--error') || !tile.dataset.session) return;
openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' }); openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' });
} }
}); });
@@ -1154,6 +1158,7 @@ function updatePageTitle() {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function openSession(name, opts = {}) { async function openSession(name, opts = {}) {
if (!name || !name.trim()) return;
hidePreview(); hidePreview();
_viewingSession = name; _viewingSession = name;
_viewingRemoteId = opts.remoteId != null ? opts.remoteId : ''; _viewingRemoteId = opts.remoteId != null ? opts.remoteId : '';
+46
View File
@@ -1240,6 +1240,52 @@ test('openSession for local session still POSTs to local /api/sessions/{name}/co
globalThis.setTimeout = origSetTimeout; globalThis.setTimeout = origSetTimeout;
}); });
test('openSession bails early when name is empty string', 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('', {});
// Should NOT make any fetch calls to /connect
const connectCall = fetchCalls.find((c) => c.url && c.url.includes('/connect'));
assert.ok(!connectCall, 'should NOT call connect when name is empty string');
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.querySelector = origQS;
globalThis.setTimeout = origSetTimeout;
});
test('openSession bails early when name is whitespace only', 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(' \t\n ', {});
// Should NOT make any fetch calls to /connect
const connectCall = fetchCalls.find((c) => c.url && c.url.includes('/connect'));
assert.ok(!connectCall, 'should NOT call connect when name is whitespace only');
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.querySelector = origQS;
globalThis.setTimeout = origSetTimeout;
});
// --- closeSession --- // --- closeSession ---
test('closeSession is exported', () => { test('closeSession is exported', () => {