diff --git a/frontend/app.js b/frontend/app.js index 52c709a..00b8640 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -301,6 +301,44 @@ function buildTileHTML(session, index, mobile) { ); } +/** + * Build the HTML string for a single session sidebar card. + * @param {object} session + * @param {string} currentSession - name of the currently active session + * @returns {string} + */ +function buildSidebarHTML(session, currentSession) { + const name = session.name || ''; + const escapedName = escapeHtml(name); + const isActive = name === currentSession; + + let classes = 'sidebar-item'; + if (isActive) classes += ' sidebar-item--active'; + + const unseen = session.bell && session.bell.unseen_count; + + // Bell indicator + let bellHtml = ''; + if (unseen && unseen > 0) { + const countStr = unseen > 9 ? '9+' : String(unseen); + bellHtml = `${countStr}`; + } + + // Last 20 lines of snapshot + const snapshot = session.snapshot || ''; + const lastLines = snapshot.split('\n').slice(-20).join('\n'); + + return ( + `
` + + `` + + `` + + `
` + ); +} + /** * Render the session grid. Shows empty state when no sessions exist. * On mobile, sorts sessions by priority before rendering. @@ -898,6 +936,7 @@ if (typeof module !== 'undefined' && module.exports) { startPolling, escapeHtml, buildTileHTML, + buildSidebarHTML, renderGrid, requestNotificationPermission, handleBellTransitions, diff --git a/frontend/tests/test_app.mjs b/frontend/tests/test_app.mjs index 656a7c5..bf7426f 100644 --- a/frontend/tests/test_app.mjs +++ b/frontend/tests/test_app.mjs @@ -1646,3 +1646,49 @@ test('closeBottomSheet does not call removeEventListener on sheet-backdrop', () assert.strictEqual(backdropRemoveCalled, false, 'closeBottomSheet must not call removeEventListener on sheet-backdrop'); globalThis.document.getElementById = origGetById; }); + +// --- buildSidebarHTML --- + +test('buildSidebarHTML adds sidebar-item--active class for active session', () => { + const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } }; + const html = app.buildSidebarHTML(session, 'my-session'); + assert.ok(html.includes('sidebar-item--active'), 'active session should have sidebar-item--active class'); +}); + +test('buildSidebarHTML does not add sidebar-item--active class for inactive session', () => { + const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } }; + const html = app.buildSidebarHTML(session, 'other-session'); + assert.ok(!html.includes('sidebar-item--active'), 'inactive session should not have sidebar-item--active class'); +}); + +test('buildSidebarHTML renders bell badge with tile-bell class and count when unseen_count > 0', () => { + const session = { name: 's', snapshot: '', bell: { unseen_count: 3 } }; + const html = app.buildSidebarHTML(session, ''); + assert.ok(html.includes('tile-bell'), 'should contain tile-bell class when unseen_count > 0'); + assert.ok(html.includes('>3<'), 'should contain unseen count text'); +}); + +test('buildSidebarHTML omits bell badge when unseen_count is 0', () => { + const session = { name: 's', snapshot: '', bell: { unseen_count: 0 } }; + const html = app.buildSidebarHTML(session, ''); + assert.ok(!html.includes('tile-bell'), 'should not contain tile-bell class when unseen_count is 0'); +}); + +test('buildSidebarHTML HTML-escapes session name to prevent XSS', () => { + const session = { name: '', snapshot: '', bell: { unseen_count: 0 } }; + const html = app.buildSidebarHTML(session, ''); + assert.ok(!html.includes('