feat(ui): suppress empty device blocks in grouped grid view

In grouped-by-device mode, devices whose sessions are all filtered out
of the current view (hidden, not in the active user view, or status-only
tiles) were still rendering a device-group header with an empty body —
visual clutter that made the dashboard feel broken.

Change: renderGroupedGrid() now skips a device entirely when its session
list is empty after partitioning the already-filtered visible session set.
The guard (devSessions.length === 0 → continue) is placed before the
<h3 class="device-group-header"> write so no HTML is emitted for the
empty device. The empty-state UI path (visible.length === 0 early-return
in renderGrid) is unaffected — it runs before renderGroupedGrid is ever
called, so the "all sessions hidden" case still shows the empty state.

Filtering source: renderGrid() already calls getVisibleSessions() →
filterVisible(_currentSessions, _serverSettings, _activeView) before
passing the ordered list to renderGroupedGrid(), so status tiles and
hidden sessions are never in the partition input.

Tests added (test_app.mjs — v0.6.3 section):
- grouped view skips device header when device has only hidden sessions
- grouped view still shows device header when ≥1 session is visible
- empty-state still appears when every device has zero visible sessions
This commit is contained in:
Brian Krabach
2026-05-17 16:09:53 -07:00
parent 5f2f8fb36b
commit 43c559f812
2 changed files with 134 additions and 1 deletions
+5 -1
View File
@@ -1013,8 +1013,12 @@ function renderGroupedGrid(sessions, mobile) {
var html = '';
for (var g = 0; g < groupOrder.length; g++) {
var name = groupOrder[g];
html += '<h3 class="device-group-header">' + escapeHtml(name) + '</h3>';
var groupSessions = groups[name];
// Skip device entirely when it has no visible sessions to render.
// This prevents empty device headers from appearing in the grouped grid
// (e.g. when all of a device's sessions are hidden in the current view).
if (groupSessions.length === 0) continue;
html += '<h3 class="device-group-header">' + escapeHtml(name) + '</h3>';
for (var j = 0; j < groupSessions.length; j++) {
html += buildTileHTML(groupSessions[j], j, mobile);
}
+129
View File
@@ -5494,3 +5494,132 @@ test('Phase 5: isHidden() helper (not inline check) drives the dim class', () =>
assert.strictEqual(app.isHidden('dev1:sess-a', null), false,
'isHidden must return false when settings is null');
});
// ─── v0.6.3 empty-device suppression ────────────────────────────────────────
test('v0.6.3: grouped view skips device header when device has only hidden sessions', () => {
// 3 devices: DeviceA and DeviceC have visible sessions; DeviceB has only a
// hidden session. In the "all" view DeviceB's session is filtered out by
// filterVisible(), so renderGroupedGrid() receives an ordered list with no
// DeviceB entries — meaning no DeviceB header should appear in the HTML.
const sessions = [
{ name: 'alpha', deviceName: 'DeviceA', snapshot: '', sessionKey: 'DeviceA:alpha' },
{ name: 'beta', deviceName: 'DeviceB', snapshot: '', sessionKey: 'DeviceB:beta' },
{ name: 'gamma', deviceName: 'DeviceC', snapshot: '', sessionKey: 'DeviceC:gamma' },
];
app._setServerSettings({ hidden_sessions: ['DeviceB:beta'] });
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('DeviceB'),
'DeviceB header must NOT appear when all its sessions are hidden');
assert.ok(capturedHTML.includes('DeviceA'),
'DeviceA header must appear (has at least one visible session)');
assert.ok(capturedHTML.includes('DeviceC'),
'DeviceC header must appear (has at least one visible session)');
globalThis.document.getElementById = origGetById;
globalThis.document.querySelectorAll = origQSA;
app._setGridViewMode('flat');
app._setServerSettings(null);
app._setActiveView('all');
});
test('v0.6.3: grouped view still shows device header when device has at least one visible session', () => {
// Sanity-check the positive case: DeviceA has two sessions, one of which is
// hidden. Because one session (alpha) remains visible, DeviceA's block must
// appear in the rendered HTML.
const sessions = [
{ name: 'alpha', deviceName: 'DeviceA', snapshot: '', sessionKey: 'DeviceA:alpha' },
{ name: 'beta', deviceName: 'DeviceA', snapshot: '', sessionKey: 'DeviceA:beta' },
{ name: 'gamma', deviceName: 'DeviceB', snapshot: '', sessionKey: 'DeviceB:gamma' },
];
app._setServerSettings({ hidden_sessions: ['DeviceA:beta'] }); // only beta hidden
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('DeviceA'),
'DeviceA header must appear — alpha is still visible even though beta is hidden');
assert.ok(capturedHTML.includes('DeviceB'),
'DeviceB header must appear — gamma is visible');
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,
// NOT fall through to grouped rendering and produce a blank grid.
const sessions = [
{ name: 'alpha', deviceName: 'DeviceA', snapshot: '', sessionKey: 'DeviceA:alpha' },
{ name: 'beta', deviceName: 'DeviceB', snapshot: '', sessionKey: 'DeviceB:beta' },
];
app._setServerSettings({ hidden_sessions: ['DeviceA:alpha', 'DeviceB:beta'] });
app._setGridViewMode('grouped');
app._setActiveView('all');
const removedFromEmpty = [];
const mockGrid = { innerHTML: '' };
const mockEmpty = {
classList: {
add: () => {},
remove: (c) => removedFromEmpty.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.renderGrid(sessions);
assert.ok(removedFromEmpty.includes('hidden'),
'empty-state must have its "hidden" class removed (i.e. become visible) when all sessions are hidden');
assert.ok(!mockGrid.innerHTML.includes('device-group-header'),
'no device-group-header elements must appear when all sessions are hidden');
globalThis.document.getElementById = origGetById;
app._setGridViewMode('flat');
app._setServerSettings(null);
app._setActiveView('all');
});