diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index e888ed3..7a187cd 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -860,7 +860,18 @@ function renderGrid(sessions) { html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join(''); } - if (grid) grid.innerHTML = html; + // Append status tiles for auth_required and unreachable sources + 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 = html + statusTilesHtml; // Render filter bar if (filterBar) { diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 951eac3..f7d23f7 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -538,6 +538,62 @@ test('renderGrid hides empty-state and populates grid when sessions exist', () = globalThis.document.querySelectorAll = origQSA; }); +test('renderGrid includes auth tile HTML when a source is auth_required', () => { + const mockGrid = { innerHTML: '' }; + const mockEmpty = { style: {}, classList: { add: () => {}, remove: () => {} } }; + const origGetById = globalThis.document.getElementById; + const origQSA = globalThis.document.querySelectorAll; + globalThis.document.getElementById = (id) => { + if (id === 'session-grid') return mockGrid; + if (id === 'empty-state') return mockEmpty; + return null; + }; + globalThis.document.querySelectorAll = () => []; + + app._setSources([ + { url: 'http://local:8088', name: 'Local', status: 'authenticated' }, + { url: 'http://workstation:8088', name: 'Workstation', status: 'auth_required' }, + ]); + + const sessions = [{ name: 'my-session', snapshot: 'hello' }]; + app.renderGrid(sessions); + + assert.ok(mockGrid.innerHTML.includes('source-tile--auth'), 'grid should include auth tile class'); + assert.ok(mockGrid.innerHTML.includes('Workstation'), 'grid should include device name Workstation'); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + app._setSources([]); +}); + +test('renderGrid includes offline tile HTML when a source is unreachable', () => { + const mockGrid = { innerHTML: '' }; + const mockEmpty = { style: {}, classList: { add: () => {}, remove: () => {} } }; + const origGetById = globalThis.document.getElementById; + const origQSA = globalThis.document.querySelectorAll; + globalThis.document.getElementById = (id) => { + if (id === 'session-grid') return mockGrid; + if (id === 'empty-state') return mockEmpty; + return null; + }; + globalThis.document.querySelectorAll = () => []; + + app._setSources([ + { url: 'http://local:8088', name: 'Local', status: 'authenticated' }, + { url: 'http://devserver:8088', name: 'Dev Server', status: 'unreachable', lastSeenAt: Date.now() - 300000 }, + ]); + + const sessions = [{ name: 'my-session', snapshot: 'hello' }]; + app.renderGrid(sessions); + + assert.ok(mockGrid.innerHTML.includes('source-tile--offline'), 'grid should include offline tile class'); + assert.ok(mockGrid.innerHTML.includes('Dev Server'), 'grid should include device name Dev Server'); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + app._setSources([]); +}); + // --- requestNotificationPermission --- test('requestNotificationPermission is exported', () => {