diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js
index 30a489a..5e2b5c3 100644
--- a/muxplex/frontend/app.js
+++ b/muxplex/frontend/app.js
@@ -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 (
+ '' +
+ '' + escapedName + '' +
+ '' +
+ 'Authenticate to see sessions' +
+ ''
+ );
+}
+
/**
* Returns sessions with hidden session names removed.
* Only hides LOCAL sessions (those with empty/absent sourceUrl) matching the
@@ -2200,6 +2217,8 @@ if (typeof module !== 'undefined' && module.exports) {
mergeSources,
// Filter bar
renderFilterBar,
+ // Federation tiles
+ buildAuthTileHTML,
// Test-only helpers
_setCurrentSessions,
_setViewMode,
diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs
index cefee58..6194569 100644
--- a/muxplex/frontend/tests/test_app.mjs
+++ b/muxplex/frontend/tests/test_app.mjs
@@ -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(' {
+ 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: '', url: '' });
+ assert.ok(!html.includes(''), 'raw script tag should not appear in html');
+ assert.ok(html.includes('<script>'), 'escaped script tag should appear in html');
+});
+