feat: add buildAuthTileHTML for auth-required federation tiles

- Add buildAuthTileHTML(source) function to frontend/app.js after buildSidebarHTML
- Function renders an <article class='source-tile source-tile--auth'> with device name,
  login button (with data-url), and hint text
- HTML-escapes both name and url to prevent XSS
- Export buildAuthTileHTML under '// Federation tiles' comment in module.exports
- Add 5 tests covering: export, article class, device name, login button data-url, HTML escaping

Task: task-2-build-auth-tile-html
This commit is contained in:
Brian Krabach
2026-03-31 02:37:05 -07:00
parent 55c80b4c9b
commit 83e5318ba1
2 changed files with 48 additions and 0 deletions
+19
View File
@@ -524,6 +524,23 @@ function buildSidebarHTML(session, currentSession) {
); );
} }
/**
* Build the HTML string for an auth-required source tile.
* @param {{ name: string, url: string }} source
* @returns {string}
*/
function buildAuthTileHTML(source) {
const escapedName = escapeHtml(source.name || '');
const escapedUrl = escapeHtml(source.url || '');
return (
'<article class="source-tile source-tile--auth">' +
'<span class="source-tile__name">' + escapedName + '</span>' +
'<button class="source-tile__login-btn" data-url="' + escapedUrl + '">Log in</button>' +
'<span class="source-tile__hint">Authenticate to see sessions</span>' +
'</article>'
);
}
/** /**
* Returns sessions with hidden session names removed. * Returns sessions with hidden session names removed.
* Only hides LOCAL sessions (those with empty/absent sourceUrl) matching the * Only hides LOCAL sessions (those with empty/absent sourceUrl) matching the
@@ -2200,6 +2217,8 @@ if (typeof module !== 'undefined' && module.exports) {
mergeSources, mergeSources,
// Filter bar // Filter bar
renderFilterBar, renderFilterBar,
// Federation tiles
buildAuthTileHTML,
// Test-only helpers // Test-only helpers
_setCurrentSessions, _setCurrentSessions,
_setViewMode, _setViewMode,
+29
View File
@@ -2835,3 +2835,32 @@ test('Phase 2 end-to-end: buildSources → tagSessions → mergeSources produces
); );
}); });
// --- buildAuthTileHTML ---
test('buildAuthTileHTML is exported as a function', () => {
assert.strictEqual(typeof app.buildAuthTileHTML, 'function');
});
test('buildAuthTileHTML returns article with source-tile--auth class', () => {
const html = app.buildAuthTileHTML({ name: 'Dev Server', url: 'http://dev:8088' });
assert.ok(html.startsWith('<article'), 'html should start with <article');
assert.ok(html.includes('source-tile--auth'), 'html should include source-tile--auth class');
});
test('buildAuthTileHTML includes device name', () => {
const html = app.buildAuthTileHTML({ name: 'Dev Server', url: 'http://dev:8088' });
assert.ok(html.includes('Dev Server'), 'html should include the device name');
});
test('buildAuthTileHTML includes login button with data-url attribute', () => {
const html = app.buildAuthTileHTML({ name: 'Dev Server', url: 'http://dev:8088' });
assert.ok(html.includes('source-tile__login-btn'), 'html should include source-tile__login-btn class');
assert.ok(html.includes('data-url="http://dev:8088"'), 'html should include data-url attribute with correct value');
});
test('buildAuthTileHTML escapes HTML in device name', () => {
const html = app.buildAuthTileHTML({ name: '<script>alert(1)</script>', url: '' });
assert.ok(!html.includes('<script>alert(1)</script>'), 'raw script tag should not appear in html');
assert.ok(html.includes('&lt;script&gt;'), 'escaped script tag should appear in html');
});