From c330f340ffdc03e668ea68b8df8e6776817a8dd1 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Tue, 31 Mar 2026 04:29:49 -0700 Subject: [PATCH] fix: renderGrid shows status tiles when no sessions; _previewClickHandler forwards sourceUrl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renderGrid early-return now builds status tiles for auth_required and unreachable sources even when visible.length === 0. Hides empty-state when status tiles are present, shows it only when truly nothing to display. - _previewClickHandler now looks up the session in _currentSessions to recover its sourceUrl before calling openSession, ensuring remote sessions opened via hover-preview connect to the correct instance instead of falling back to the local API path. Tests added (3 new, 214 → 217 total): - renderGrid shows auth tile and hides empty-state when no sessions but source is auth_required - renderGrid shows offline tile and hides empty-state when no sessions but source is unreachable - _previewClickHandler looks up sourceUrl from _currentSessions before calling openSession Closes code-review Important issues #1 and #2. Co-authored-by: Amplifier --- muxplex/frontend/app.js | 21 ++++++++-- muxplex/frontend/tests/test_app.mjs | 63 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index f635687..60b3c6a 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -839,8 +839,20 @@ function renderGrid(sessions) { } if (visible.length === 0) { - if (grid) grid.innerHTML = ''; - if (emptyState) emptyState.classList.remove('hidden'); + // Build status tiles for non-authenticated sources even when no sessions exist + var statusTilesHtml = ''; + if (typeof _sources !== 'undefined' && _sources) { + _sources.forEach(function(source) { + if (source.status === 'auth_required') statusTilesHtml += buildAuthTileHTML(source); + else if (source.status === 'unreachable') statusTilesHtml += buildOfflineTileHTML(source); + }); + } + if (grid) grid.innerHTML = statusTilesHtml; + // Only show empty-state when there are truly no tiles at all + if (emptyState) { + if (statusTilesHtml) emptyState.classList.add('hidden'); + else emptyState.classList.remove('hidden'); + } // Show filter bar even when filtered to empty (so user can switch back) if (filterBar) { if (_gridViewMode === 'filtered') { @@ -920,7 +932,10 @@ function _previewClickHandler(e) { e.stopPropagation(); var name = _previewSessionName; hidePreview(); - if (name) openSession(name); + if (name) { + var session = _currentSessions && _currentSessions.find(function(s) { return s.name === name; }); + openSession(name, { sourceUrl: session && session.sourceUrl || '' }); + } } function showPreview(name) { diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 9851bd2..76e8086 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -606,6 +606,69 @@ test('renderGrid includes offline tile HTML when a source is unreachable', () => app._setSources([]); }); +test('renderGrid shows auth tile and hides empty-state when no sessions but source is auth_required', () => { + const addedClasses = []; + const removedClasses = []; + const mockGrid = { innerHTML: '' }; + const mockEmpty = { style: {}, classList: { add: (c) => addedClasses.push(c), remove: (c) => removedClasses.push(c) } }; + const origGetById = globalThis.document.getElementById; + globalThis.document.getElementById = (id) => { + if (id === 'session-grid') return mockGrid; + if (id === 'empty-state') return mockEmpty; + return null; + }; + + app._setSources([ + { url: 'http://workstation:8088', name: 'Workstation', status: 'auth_required' }, + ]); + + // No sessions — early-return path + app.renderGrid([]); + + assert.ok(mockGrid.innerHTML.includes('source-tile--auth'), 'grid should show auth tile even when sessions list is empty'); + assert.ok(addedClasses.includes('hidden'), 'empty-state should be hidden when status tiles are present'); + assert.ok(!removedClasses.includes('hidden'), 'empty-state hidden class should NOT be removed when status tiles are present'); + + globalThis.document.getElementById = origGetById; + app._setSources([]); +}); + +test('renderGrid shows offline tile and hides empty-state when no sessions but source is unreachable', () => { + const addedClasses = []; + const mockGrid = { innerHTML: '' }; + const mockEmpty = { style: {}, classList: { add: (c) => addedClasses.push(c), remove: () => {} } }; + const origGetById = globalThis.document.getElementById; + globalThis.document.getElementById = (id) => { + if (id === 'session-grid') return mockGrid; + if (id === 'empty-state') return mockEmpty; + return null; + }; + + app._setSources([ + { url: 'http://devbox:8088', name: 'Dev Box', status: 'unreachable', lastSeenAt: Date.now() - 120000 }, + ]); + + // No sessions — early-return path + app.renderGrid([]); + + assert.ok(mockGrid.innerHTML.includes('source-tile--offline'), 'grid should show offline tile even when sessions list is empty'); + assert.ok(addedClasses.includes('hidden'), 'empty-state should be hidden when status tiles are present'); + + globalThis.document.getElementById = origGetById; + app._setSources([]); +}); + +test('_previewClickHandler looks up sourceUrl from _currentSessions before calling openSession', () => { + const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); + const handlerIdx = source.indexOf('function _previewClickHandler'); + assert.ok(handlerIdx >= 0, '_previewClickHandler must exist'); + // Extract from function declaration to its closing brace + const handlerEnd = source.indexOf('\n}', handlerIdx) + 2; + const handlerBody = source.slice(handlerIdx, handlerEnd); + assert.ok(handlerBody.includes('_currentSessions'), '_previewClickHandler must look up session from _currentSessions to recover sourceUrl'); + assert.ok(handlerBody.includes('sourceUrl'), '_previewClickHandler must forward sourceUrl when calling openSession'); +}); + // --- requestNotificationPermission --- test('requestNotificationPermission is exported', () => {