diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js
index 3892347..a8310e7 100644
--- a/muxplex/frontend/app.js
+++ b/muxplex/frontend/app.js
@@ -561,14 +561,35 @@ function renderSidebar(sessions, currentSession) {
return;
}
- list.innerHTML = visible.map((session) => buildSidebarHTML(session, currentSession)).join('');
+ let html = '';
- // Bind click handlers on each sidebar item
+ if (_sources.length > 1) {
+ // Group sessions by deviceName when multiple sources configured
+ const groups = new Map();
+ for (const session of visible) {
+ const deviceName = session.deviceName || 'Unknown';
+ if (!groups.has(deviceName)) groups.set(deviceName, []);
+ groups.get(deviceName).push(session);
+ }
+
+ for (const [deviceName, deviceSessions] of groups) {
+ html += `
`;
+ html += deviceSessions.map((session) => buildSidebarHTML(session, currentSession)).join('');
+ }
+ } else {
+ // Single source: flat list with no device headers
+ html = visible.map((session) => buildSidebarHTML(session, currentSession)).join('');
+ }
+
+ list.innerHTML = html;
+
+ // Bind click handlers on each sidebar item, passing sourceUrl
if (typeof list.querySelectorAll === 'function') {
list.querySelectorAll('.sidebar-item').forEach((item) => {
const name = item.dataset.session;
+ const sourceUrl = item.dataset.sourceUrl || '';
on(item, 'click', () => {
- if (name !== currentSession) openSession(name);
+ if (name !== currentSession) openSession(name, { sourceUrl });
});
});
}
diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs
index 9c19d8f..9998e01 100644
--- a/muxplex/frontend/tests/test_app.mjs
+++ b/muxplex/frontend/tests/test_app.mjs
@@ -2666,4 +2666,75 @@ test('saveGridViewMode stores to localStorage when scope is local', () => {
app._setGridViewMode('flat');
});
+// --- renderSidebar device grouping (task-16) ---
+
+test('renderSidebar groups sessions by device with sidebar-device-header when multiple sources configured', () => {
+ let capturedHTML = '';
+ const mockList = {
+ get innerHTML() { return capturedHTML; },
+ set innerHTML(v) { capturedHTML = v; },
+ querySelectorAll: () => [],
+ };
+ const origGetById = globalThis.document.getElementById;
+ globalThis.document.getElementById = (id) => {
+ if (id === 'sidebar-list') return mockList;
+ return null;
+ };
+
+ // Set up multiple sources
+ app._setSources([
+ { url: '', name: 'Laptop', type: 'local', status: 'authenticated', backoffMs: 2000 },
+ { url: 'https://remote.example.com', name: 'Server', type: 'remote', status: 'authenticated', backoffMs: 2000 },
+ ]);
+
+ app._setViewMode('fullscreen');
+ const sessions = [
+ { name: 'alpha', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::alpha', snapshot: '', bell: { unseen_count: 0 } },
+ { name: 'beta', deviceName: 'Server', sourceUrl: 'https://remote.example.com', sessionKey: 'https://remote.example.com::beta', snapshot: '', bell: { unseen_count: 0 } },
+ ];
+ app.renderSidebar(sessions, null);
+
+ assert.ok(capturedHTML.includes('sidebar-device-header'), 'sidebar HTML should contain sidebar-device-header elements when multiple sources');
+ assert.ok(capturedHTML.includes('Laptop'), 'sidebar HTML should contain device name "Laptop"');
+ assert.ok(capturedHTML.includes('Server'), 'sidebar HTML should contain device name "Server"');
+
+ // Cleanup
+ app._setSources([]);
+ globalThis.document.getElementById = origGetById;
+ app._setViewMode('grid');
+});
+
+test('renderSidebar does NOT group when only one source configured', () => {
+ let capturedHTML = '';
+ const mockList = {
+ get innerHTML() { return capturedHTML; },
+ set innerHTML(v) { capturedHTML = v; },
+ querySelectorAll: () => [],
+ };
+ const origGetById = globalThis.document.getElementById;
+ globalThis.document.getElementById = (id) => {
+ if (id === 'sidebar-list') return mockList;
+ return null;
+ };
+
+ // Set up single source
+ app._setSources([
+ { url: '', name: 'Laptop', type: 'local', status: 'authenticated', backoffMs: 2000 },
+ ]);
+
+ app._setViewMode('fullscreen');
+ const sessions = [
+ { name: 'alpha', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::alpha', snapshot: '', bell: { unseen_count: 0 } },
+ { name: 'beta', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::beta', snapshot: '', bell: { unseen_count: 0 } },
+ ];
+ app.renderSidebar(sessions, null);
+
+ assert.ok(!capturedHTML.includes('sidebar-device-header'), 'sidebar HTML should NOT contain sidebar-device-header when only one source');
+ assert.ok(capturedHTML.includes('sidebar-item'), 'sidebar HTML should still contain sidebar-item elements');
+
+ // Cleanup
+ app._setSources([]);
+ globalThis.document.getElementById = origGetById;
+ app._setViewMode('grid');
+});