From c3a059a7ee14e92ce4b6f0778aea5adef73e9aad Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sun, 17 May 2026 16:55:12 -0700 Subject: [PATCH] fix(ui): suppress empty device status tiles in all view modes (not just grouped) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.6.4 added `&& _gridViewMode !== 'grouped'` to the status:empty branch in renderGrid, suppressing the "No sessions" tile only in grouped grid view. User reported the alienware-r13 "No sessions" tile still appearing in flat view (gridViewMode: "flat"), which is the muxplex default. The original request was unambiguous: "don't need a block for those that don't have anything to show" — no view-mode qualifier was implied. Changes: - Both renderGrid call sites (early-return branch for visible.length === 0, and the normal rendering branch) now simply omit the status:empty else-if entirely. The status:empty sentinel may still arrive from the server; the renderer silently ignores it in every view mode. - Updated surrounding comments to reflect the new unconditional suppression. - Updated/replaced tests that asserted the old (wrong) flat-mode behaviour: - Replaced 'renderGrid shows "No sessions" status tile for status=empty devices' with a v0.6.5 variant that asserts NO tile is emitted. - Removed 'v0.6.4: status:empty block IS still rendered in flat grid mode' (superseded; kept as a tombstone comment). - Added three new tests under 'v0.6.5 empty tile suppressed in all view modes': 1. Flat view: empty sentinel → no source-tile--empty, no 'No sessions' text. 2. Grouped view: same (regression guard for the v0.6.4 suppression). 3. Mixed input: real session renders, empty sentinel is discarded, both modes. No other rendering paths that produce a per-device block for empty devices were found: renderSidebar and renderGroupedGrid both operate on getVisibleSessions() output which already excludes status-bearing sentinels. Test count: 402 (v0.6.4) → 404 (v0.6.5). --- muxplex/frontend/app.js | 13 +- muxplex/frontend/tests/test_app.mjs | 205 +++++++++++++++++++++------- 2 files changed, 161 insertions(+), 57 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 54636f7..74cf9ac 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1659,13 +1659,12 @@ function renderGrid(sessions) { if (visible.length === 0) { // Build status tiles for auth_failed/unreachable sessions even when no regular sessions exist. - // In grouped grid mode, skip status:empty tiles — empty devices are silently omitted so that - // a remote with zero tmux sessions doesn't produce a visible device block in the grouped view. + // status:empty sentinels are intentionally ignored — a remote with zero tmux sessions + // produces no visible tile in any view mode (flat, grouped, or otherwise). var statusTilesHtml = ''; (sessions || []).forEach(function(session) { if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth'); else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline'); - else if (session.status === 'empty' && _gridViewMode !== 'grouped') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty'); }); if (grid) grid.innerHTML = statusTilesHtml; // Only show empty-state when there are truly no tiles at all @@ -1697,15 +1696,13 @@ function renderGrid(sessions) { html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join(''); } - // Append status tiles for auth_failed, unreachable, and empty sessions. - // In grouped grid mode, skip status:empty tiles — a remote with zero tmux sessions is simply - // omitted from the grouped view. auth_failed and unreachable are always shown because those - // are actionable error states regardless of view mode. + // Append status tiles for auth_failed and unreachable sessions. status:empty sentinels are + // intentionally ignored in all view modes — a remote with zero tmux sessions produces no + // visible tile. auth_failed and unreachable are actionable error states and are always shown. var statusTilesHtml = ''; (sessions || []).forEach(function(session) { if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth'); else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline'); - else if (session.status === 'empty' && _gridViewMode !== 'grouped') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty'); }); if (grid) grid.innerHTML = html + statusTilesHtml; diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 60b23df..0bb5d80 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -4829,13 +4829,14 @@ test('renderGrid status tiles use session.deviceName not session.name for offlin // --- renderGrid: status=empty shows "No sessions" tile --- -test('renderGrid shows "No sessions" status tile for status=empty devices', () => { - // A device that is online but has zero tmux sessions returns +test('renderGrid silently drops status=empty devices (no tile emitted) [v0.6.5]', () => { + // v0.6.5: a device that is online but has zero tmux sessions returns // {status: 'empty', deviceName: '...'} from the federation endpoint. - // renderGrid must render a status tile with the text "No sessions" (not blank). + // renderGrid must NOT render any tile for it — the user asked not to see + // blocks for devices that have nothing to show (flat OR grouped mode). // - // Before implementation: fails because neither status loop handles status === 'empty', - // so the tile is never rendered and grid.innerHTML stays empty. + // Previously (pre-v0.6.5) a "No sessions" status tile was emitted; that + // behaviour is intentionally removed. const grid = { innerHTML: '' }; const emptyState = { style: {}, classList: { add() {}, remove() {} } }; const origGetById = globalThis.document.getElementById; @@ -4848,12 +4849,16 @@ test('renderGrid shows "No sessions" status tile for status=empty devices', () = app.renderGrid([{ status: 'empty', deviceName: 'quiet-box', remoteId: 3 }]); assert.ok( - grid.innerHTML.includes('No sessions'), - `renderGrid must include "No sessions" text for status=empty device, got: ${grid.innerHTML}` + !grid.innerHTML.includes('No sessions'), + `renderGrid must NOT include "No sessions" text for status=empty device, got: ${grid.innerHTML}` ); assert.ok( - grid.innerHTML.includes('quiet-box'), - `renderGrid must include the deviceName "quiet-box" in the status tile, got: ${grid.innerHTML}` + !grid.innerHTML.includes('quiet-box'), + `renderGrid must NOT include the deviceName "quiet-box" for a status=empty device, got: ${grid.innerHTML}` + ); + assert.ok( + !grid.innerHTML.includes('source-tile--empty'), + `renderGrid must NOT emit source-tile--empty class for status=empty device, got: ${grid.innerHTML}` ); globalThis.document.getElementById = origGetById; @@ -5635,46 +5640,9 @@ test('v0.6.4: status:empty block is NOT rendered in grouped grid mode for a remo app._setActiveView('all'); }); -test('v0.6.4: status:empty block IS still rendered in flat grid mode', () => { - // In flat mode the "No sessions" tile for an empty remote is intentional — only - // the grouped view should suppress it. - const sessions = [ - { name: 'local-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:local-sess' }, - { status: 'empty', deviceName: 'alienware-r13', deviceId: 'aw-uuid', remoteId: 'aw-uuid' }, - ]; - app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] }); - app._setGridViewMode('flat'); - app._setActiveView('all'); - - let capturedHTML = ''; - const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } }; - const mockEmpty = { 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.renderGrid(sessions); - - assert.ok( - capturedHTML.includes('source-tile--empty'), - 'source-tile--empty must still appear for an empty remote in flat mode; got: ' + capturedHTML - ); - assert.ok( - capturedHTML.includes('alienware-r13'), - 'alienware-r13 name must appear in the status tile in flat mode; got: ' + capturedHTML - ); - - globalThis.document.getElementById = origGetById; - globalThis.document.querySelectorAll = origQSA; - app._setGridViewMode('flat'); - app._setServerSettings(null); - app._setActiveView('all'); -}); +// NOTE: v0.6.4 tested that status:empty blocks appeared in flat mode. That behaviour was +// wrong and is superseded by v0.6.5, which silently drops status:empty in ALL view modes. +// See the v0.6.5 section below for the authoritative tests. test('v0.6.4: auth_failed and unreachable tiles still appear in grouped mode', () => { // Only status:empty is suppressed in grouped mode; auth_failed and unreachable @@ -5718,6 +5686,145 @@ test('v0.6.4: auth_failed and unreachable tiles still appear in grouped mode', ( app._setActiveView('all'); }); +// ─── v0.6.5 empty tile suppressed in all view modes ────────────────────────── + +test('v0.6.5: flat view — status:empty sentinel produces NO tile', () => { + // User-reported bug: alienware-r13 showed a "No sessions" tile in flat view + // (the muxplex default). v0.6.4 only suppressed it in grouped mode. + // v0.6.5 drops status:empty tiles unconditionally regardless of view mode. + const sessions = [ + { name: 'local-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:local-sess' }, + { status: 'empty', deviceName: 'alienware-r13', sessionKey: 'devX:_empty' }, + ]; + app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] }); + app._setGridViewMode('flat'); + app._setActiveView('all'); + + let capturedHTML = ''; + const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } }; + const mockEmpty = { 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.renderGrid(sessions); + + assert.ok( + !capturedHTML.includes('source-tile--empty'), + 'source-tile--empty must NOT appear in flat mode for an empty remote; got: ' + capturedHTML + ); + assert.ok( + !capturedHTML.includes('No sessions'), + '"No sessions" text must NOT appear in flat mode for an empty remote; got: ' + capturedHTML + ); + assert.ok( + capturedHTML.includes('local-sess') || capturedHTML.includes('spark-1'), + 'the real local session must still render; got: ' + capturedHTML + ); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + app._setGridViewMode('flat'); + app._setServerSettings(null); + app._setActiveView('all'); +}); + +test('v0.6.5: grouped view — status:empty sentinel still produces NO tile (regression)', () => { + // v0.6.4 suppressed empty tiles only in grouped mode; v0.6.5 keeps that suppression + // intact. This is a regression guard: grouped suppression must not have been broken + // while fixing flat mode. + const sessions = [ + { name: 'local-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:local-sess' }, + { status: 'empty', deviceName: 'alienware-r13', sessionKey: 'devX:_empty' }, + ]; + app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] }); + app._setGridViewMode('grouped'); + app._setActiveView('all'); + + let capturedHTML = ''; + const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } }; + const mockEmpty = { 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.renderGrid(sessions); + + assert.ok( + !capturedHTML.includes('source-tile--empty'), + 'source-tile--empty must NOT appear in grouped mode for an empty remote; got: ' + capturedHTML + ); + assert.ok( + !capturedHTML.includes('No sessions'), + '"No sessions" text must NOT appear in grouped mode for an empty remote; got: ' + capturedHTML + ); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + app._setGridViewMode('flat'); + app._setServerSettings(null); + app._setActiveView('all'); +}); + +test('v0.6.5: real sessions still render alongside an empty sentinel', () => { + // A mix of a real session and a status:empty sentinel. The real session must + // appear; the empty sentinel must be silently discarded in both flat and grouped modes. + const sessions = [ + { name: 'my-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:my-sess' }, + { status: 'empty', deviceName: 'alienware-r13', sessionKey: 'devX:_empty' }, + ]; + app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] }); + app._setActiveView('all'); + + for (const mode of ['flat', 'grouped']) { + app._setGridViewMode(mode); + + let capturedHTML = ''; + const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } }; + const mockEmpty = { 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.renderGrid(sessions); + + assert.ok( + capturedHTML.includes('my-sess') || capturedHTML.includes('spark-1'), + `[${mode}] real session must render; got: ` + capturedHTML + ); + assert.ok( + !capturedHTML.includes('source-tile--empty'), + `[${mode}] source-tile--empty must NOT appear; got: ` + capturedHTML + ); + assert.ok( + !capturedHTML.includes('No sessions'), + `[${mode}] "No sessions" text must NOT appear; got: ` + capturedHTML + ); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + } + + app._setGridViewMode('flat'); + app._setServerSettings(null); + app._setActiveView('all'); +}); + test('v0.6.3: empty-state still appears when every device has zero visible sessions', () => { // When ALL sessions across ALL devices are hidden, visible.length === 0. // renderGrid() must still reach its early-return branch and show empty-state,