fix(ui): suppress empty device blocks in renderGrid grouped mode
Root cause: the v0.6.3 fix added a guard in renderGroupedGrid
(groupSessions.length === 0) that is unreachable — groups are built by
iterating the already-filtered session list, so every group that exists
always has ≥1 entry. The actual empty-device block was coming from a
separate code path in renderGrid that unconditionally appends a
source-tile--empty status tile for every federation remote whose server
returns {status: 'empty'}. In flat mode this is correct and intentional
("No sessions" badge); in grouped mode it produced a visible block
showing the device name, exactly what the user saw for alienware-r13.
The v0.6.3 tests only covered the hidden-sessions case (sessions exist
but are hidden); they didn't cover the zero-sessions case (remote device
online, zero tmux sessions → server emits status:empty sentinel). Both
status-tile generation paths in renderGrid (the visible.length===0 early
return and the main append-at-end path) were appending the empty tile
regardless of gridViewMode.
Fix: skip status:empty tiles when _gridViewMode === 'grouped'.
auth_failed and unreachable tiles are still shown in all modes because
they represent actionable error states.
Adds three regression tests in test_app.mjs:
- status:empty NOT rendered in grouped mode (would have caught the bug)
- status:empty IS still rendered in flat mode (backward compat)
- auth_failed/unreachable still appear in grouped mode
This commit is contained in:
@@ -1658,12 +1658,14 @@ function renderGrid(sessions) {
|
|||||||
var visible = getVisibleSessions(sessions);
|
var visible = getVisibleSessions(sessions);
|
||||||
|
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
// Build status tiles for auth_failed/unreachable sessions even when no regular sessions exist
|
// 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.
|
||||||
var statusTilesHtml = '';
|
var statusTilesHtml = '';
|
||||||
(sessions || []).forEach(function(session) {
|
(sessions || []).forEach(function(session) {
|
||||||
if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth');
|
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 === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline');
|
||||||
else if (session.status === 'empty') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty');
|
else if (session.status === 'empty' && _gridViewMode !== 'grouped') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty');
|
||||||
});
|
});
|
||||||
if (grid) grid.innerHTML = statusTilesHtml;
|
if (grid) grid.innerHTML = statusTilesHtml;
|
||||||
// Only show empty-state when there are truly no tiles at all
|
// Only show empty-state when there are truly no tiles at all
|
||||||
@@ -1695,12 +1697,15 @@ function renderGrid(sessions) {
|
|||||||
html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join('');
|
html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append status tiles for auth_failed, unreachable, and empty sessions
|
// 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.
|
||||||
var statusTilesHtml = '';
|
var statusTilesHtml = '';
|
||||||
(sessions || []).forEach(function(session) {
|
(sessions || []).forEach(function(session) {
|
||||||
if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth');
|
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 === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline');
|
||||||
else if (session.status === 'empty') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty');
|
else if (session.status === 'empty' && _gridViewMode !== 'grouped') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty');
|
||||||
});
|
});
|
||||||
if (grid) grid.innerHTML = html + statusTilesHtml;
|
if (grid) grid.innerHTML = html + statusTilesHtml;
|
||||||
|
|
||||||
|
|||||||
@@ -5584,6 +5584,140 @@ test('v0.6.3: grouped view still shows device header when device has at least on
|
|||||||
app._setActiveView('all');
|
app._setActiveView('all');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── v0.6.4 empty-device-block regression ────────────────────────────────────
|
||||||
|
|
||||||
|
test('v0.6.4: status:empty block is NOT rendered in grouped grid mode for a remote with zero sessions', () => {
|
||||||
|
// The real user scenario (spark-1 viewing alienware-r13 via federation):
|
||||||
|
// alienware-r13 has zero tmux sessions → server emits {status:"empty", deviceName:"alienware-r13"}.
|
||||||
|
// spark-1 (the local device) still has its own sessions.
|
||||||
|
// BEFORE v0.6.4: renderGrid appended a source-tile--empty block for alienware-r13 even in
|
||||||
|
// grouped mode, showing the device name in the grid (the user's reported "device block").
|
||||||
|
// AFTER v0.6.4: the status:empty tile is suppressed in grouped mode.
|
||||||
|
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('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('alienware-r13'),
|
||||||
|
'alienware-r13 must NOT appear in grouped grid when the device has zero sessions; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!capturedHTML.includes('source-tile--empty'),
|
||||||
|
'source-tile--empty must NOT be rendered for an empty remote in grouped mode; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
capturedHTML.includes('spark-1') || capturedHTML.includes('local-sess'),
|
||||||
|
'spark-1 local session must still appear; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
|
||||||
|
globalThis.document.getElementById = origGetById;
|
||||||
|
globalThis.document.querySelectorAll = origQSA;
|
||||||
|
app._setGridViewMode('flat');
|
||||||
|
app._setServerSettings(null);
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
// are actionable error states and must always be shown.
|
||||||
|
const sessions = [
|
||||||
|
{ name: 'local-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:local-sess' },
|
||||||
|
{ status: 'auth_failed', deviceName: 'device-b', deviceId: 'b-uuid', remoteId: 'b-uuid' },
|
||||||
|
{ status: 'unreachable', deviceName: 'device-c', deviceId: 'c-uuid', remoteId: 'c-uuid' },
|
||||||
|
];
|
||||||
|
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--auth'),
|
||||||
|
'auth_failed tile must appear in grouped mode; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
capturedHTML.includes('source-tile--offline'),
|
||||||
|
'unreachable tile must appear in grouped mode; 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', () => {
|
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.
|
// When ALL sessions across ALL devices are hidden, visible.length === 0.
|
||||||
// renderGrid() must still reach its early-return branch and show empty-state,
|
// renderGrid() must still reach its early-return branch and show empty-state,
|
||||||
|
|||||||
Reference in New Issue
Block a user