From 84b277854b7c7b14350a116d2141432ebf3565cd Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 1 Apr 2026 15:43:51 -0700 Subject: [PATCH] refactor: remove _sources state and buildSources/tagSessions/mergeSources --- muxplex/frontend/app.js | 168 ++++----------- muxplex/frontend/tests/test_app.mjs | 306 +++++----------------------- 2 files changed, 81 insertions(+), 393 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 48893f8..7a812b7 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -133,7 +133,6 @@ let _previewTimer = null; var _previewSessionName = null; // track by NAME, not DOM element // ─── Settings state ─────────────────────────────────────────────────────────── -let _sources = []; let _settingsOpen = false; let _serverSettings = null; let _gridViewMode = 'flat'; @@ -467,7 +466,7 @@ function buildTileHTML(session, index, mobile) { // Device badge — right-aligned in header, separate from name span // Shown when multiple sources configured AND session has a device name let badgeHtml = ''; - if (_sources.length > 1 && session.deviceName && ds.showDeviceBadges !== false) { + if (_serverSettings && _serverSettings.multi_device_enabled && session.deviceName && ds.showDeviceBadges !== false) { badgeHtml = `${escapeHtml(session.deviceName)}`; } @@ -522,9 +521,9 @@ function buildSidebarHTML(session, currentSession) { // Edge bar only (left border amber, no glow): applied when actIndicator is 'dot' or 'both' if (isBell && (actIndicator === 'dot' || actIndicator === 'both')) classes += ' sidebar-item--edge-bell'; - // Device badge — shown in meta line when multiple sources configured + // Device badge — shown in meta line when multi_device_enabled let badgeHtml = ''; - if (_sources.length > 1 && session.deviceName && ds.showDeviceBadges !== false) { + if (_serverSettings && _serverSettings.multi_device_enabled && session.deviceName && ds.showDeviceBadges !== false) { badgeHtml = `${escapeHtml(session.deviceName)}`; } @@ -606,6 +605,22 @@ function buildOfflineTileHTML(source) { ); } +/** + * Build the HTML string for a generic status tile (auth_failed or unreachable). + * @param {string} deviceName + * @param {string} statusText + * @param {string} statusClass + * @returns {string} + */ +function buildStatusTileHTML(deviceName, statusText, statusClass) { + return ( + '
' + + '' + escapeHtml(deviceName || '') + '' + + '' + escapeHtml(statusText || '') + '' + + '
' + ); +} + /** * Open a login popup window for a remote muxplex instance. * Strips trailing slashes from remoteUrl before appending /login. @@ -656,8 +671,8 @@ function renderSidebar(sessions, currentSession) { let html = ''; - if (_sources.length > 1) { - // Group sessions by deviceName when multiple sources configured + if (_serverSettings && _serverSettings.multi_device_enabled) { + // Group sessions by deviceName when multi_device_enabled const groups = new Map(); for (const session of visible) { const deviceName = session.deviceName || 'Unknown'; @@ -871,14 +886,12 @@ function renderGrid(sessions) { } if (visible.length === 0) { - // Build status tiles for non-authenticated sources even when no sessions exist + // Build status tiles for auth_failed/unreachable sessions even when no regular sessions exist var statusTilesHtml = ''; - if (typeof _sources !== 'undefined' && _sources) { - _sources.forEach(function(source) { - if (source.status === 'auth_required') statusTilesHtml += buildAuthTileHTML(source); - else if (source.status === 'unreachable') statusTilesHtml += buildOfflineTileHTML(source); - }); - } + (sessions || []).forEach(function(session) { + if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.name, 'Auth required', 'auth'); + else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.name, 'Offline', 'offline'); + }); if (grid) grid.innerHTML = statusTilesHtml; // Only show empty-state when there are truly no tiles at all if (emptyState) { @@ -916,17 +929,12 @@ function renderGrid(sessions) { html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join(''); } - // Append status tiles for auth_required and unreachable sources + // Append status tiles for auth_failed and unreachable sessions var statusTilesHtml = ''; - if (typeof _sources !== 'undefined' && _sources) { - _sources.forEach(function(source) { - if (source.status === 'auth_required') { - statusTilesHtml += buildAuthTileHTML(source); - } else if (source.status === 'unreachable') { - statusTilesHtml += buildOfflineTileHTML(source); - } - }); - } + (sessions || []).forEach(function(session) { + if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.name, 'Auth required', 'auth'); + else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.name, 'Offline', 'offline'); + }); if (grid) grid.innerHTML = html + statusTilesHtml; // Render filter bar @@ -1387,7 +1395,6 @@ async function loadServerSettings() { console.warn('[loadServerSettings] failed:', err); if (!_serverSettings) _serverSettings = {}; } - _sources = buildSources(_serverSettings); return _serverSettings; } @@ -1409,42 +1416,6 @@ async function patchServerSetting(key, value) { } } -/** - * Build the list of session sources from server settings. - * Local source is always first with url: ''. - * Remote instances come from settings.remote_instances array. - * Trailing slashes are stripped from remote URLs. - * Default device name is 'This device' when device_name is empty. - * @param {object} settings - server settings object - * @returns {object[]} array of source objects with {url, name, type, status, backoffMs} - */ -function buildSources(settings) { - var localName = (settings && settings.device_name) || 'This device'; - var sources = [ - { url: '', name: localName, type: 'local', status: 'authenticated', backoffMs: 2000 }, - ]; - // Only add remote sources when multi-device is enabled. - // Smart default: treat as enabled if remote_instances is non-empty (backward compat). - var remotes = (settings && settings.remote_instances) || []; - var multiDeviceEnabled = (settings && settings.multi_device_enabled) || - (remotes.length > 0); - if (multiDeviceEnabled) { - for (var i = 0; i < remotes.length; i++) { - var r = remotes[i]; - if (r && r.url) { - sources.push({ - url: r.url.replace(/\/+$/, ''), - name: r.name || r.url, - type: 'remote', - status: 'authenticated', - backoffMs: 2000, - }); - } - } - } - return sources; -} - /** * Build a single remote instance row element with URL input, name input, and remove button. * @param {string} url - remote instance URL @@ -1478,7 +1449,6 @@ function _buildRemoteInstanceRow(url, name) { /** * Read remote instance rows from the DOM and save to server settings. - * Rebuilds _sources after saving so polling updates immediately. */ function _saveRemoteInstances() { var container = $('setting-remote-instances'); @@ -1493,26 +1463,7 @@ function _saveRemoteInstances() { instances.push({ url: url, name: name }); } }); - patchServerSetting('remote_instances', instances).then(function() { - _sources = buildSources(_serverSettings); - // Prune federation tokens for URLs no longer in the remote instances list - try { - var activeOrigins = instances.map(function(r) { - try { return new URL(r.url).origin; } catch (_) { return null; } - }).filter(Boolean); - var ftokens = JSON.parse(localStorage.getItem('muxplex.federation_tokens') || '{}'); - var pruned = false; - Object.keys(ftokens).forEach(function(origin) { - if (!activeOrigins.includes(origin)) { - delete ftokens[origin]; - pruned = true; - } - }); - if (pruned) { - localStorage.setItem('muxplex.federation_tokens', JSON.stringify(ftokens)); - } - } catch (_) { /* blocked — ok */ } - }); + patchServerSetting('remote_instances', instances); } // ─── Multi-Device helper ────────────────────────────────────────────────────────── @@ -2379,9 +2330,7 @@ function bindStaticEventListeners() { on($('setting-multi-device-enabled'), 'change', function() { var enabled = this.checked; _updateMultiDeviceFieldsState(enabled); - patchServerSetting('multi_device_enabled', enabled).then(function() { - _sources = buildSources(_serverSettings); - }); + patchServerSetting('multi_device_enabled', enabled); }); // Multi-Device tab — device name with 500ms debounce; updates document.title immediately @@ -2391,9 +2340,7 @@ function bindStaticEventListeners() { var val = this.value; document.title = val || 'muxplex'; _deviceNameDebounceTimer = setTimeout(function() { - patchServerSetting('device_name', val).then(function() { - _sources = buildSources(_serverSettings); - }); + patchServerSetting('device_name', val); }, 500); }); @@ -2461,36 +2408,6 @@ function bindStaticEventListeners() { // ─── Test-only helpers ──────────────────────────────────────────────────────── -// ─── Multi-source parallel polling ───────────────────────────────────────── - -/** - * Tag each session in the array with deviceName, sourceUrl, and sessionKey. - * Returns new session objects; does NOT mutate originals. - * sessionKey format: sourceUrl + '::' + name - * @param {object[]} sessions - * @param {string} deviceName - * @param {string} sourceUrl - * @returns {object[]} - */ -function tagSessions(sessions, deviceName, sourceUrl) { - return (sessions || []).map((s) => Object.assign({}, s, { - deviceName, - sourceUrl, - sessionKey: sourceUrl + '::' + (s.name || ''), - })); -} - -/** - * Merge sessions from multiple sources into a single flat array. - * Each result is an object with {source, sessions}. - * Tags each source's sessions with deviceName/sourceUrl/sessionKey. - * @param {Array<{source: {name: string, url: string}, sessions: object[]}>} results - * @returns {object[]} - */ -function mergeSources(results) { - return (results || []).reduce((all, r) => all.concat(tagSessions(r.sessions, r.source.name, r.source.url)), []); -} - /** Test-only: set _currentSessions directly. */ function _setCurrentSessions(sessions) { _currentSessions = sessions; @@ -2501,11 +2418,6 @@ function _setViewMode(mode) { _viewMode = mode; } -/** Test-only: set _sources directly. */ -function _setSources(sources) { - _sources = sources; -} - /** Test-only: set _serverSettings directly. */ function _setServerSettings(settings) { _serverSettings = settings; @@ -2521,11 +2433,6 @@ function _setGridViewMode(mode) { _gridViewMode = mode; } -/** Test-only: get _sources. */ -function _getSources() { - return _sources; -} - /** Test-only: set _activeFilterDevice directly. */ function _setActiveFilterDevice(device) { _activeFilterDevice = device; @@ -2630,7 +2537,6 @@ if (typeof module !== 'undefined' && module.exports) { // Server settings loadServerSettings, patchServerSetting, - buildSources, // Fetch wrapper api, // Header + button with inline name input @@ -2639,14 +2545,12 @@ if (typeof module !== 'undefined' && module.exports) { createNewSession, // Kill session killSession, - // Multi-source parallel polling - tagSessions, - mergeSources, // Filter bar renderFilterBar, // Federation tiles buildAuthTileHTML, buildOfflineTileHTML, + buildStatusTileHTML, openLoginPopup, formatLastSeen, // Federation auth token relay @@ -2657,11 +2561,9 @@ if (typeof module !== 'undefined' && module.exports) { // Test-only helpers _setCurrentSessions, _setViewMode, - _setSources, _setServerSettings, _getGridViewMode, _setGridViewMode, - _getSources, _setActiveFilterDevice, }; } diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 54dabd1..3bc39d1 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -616,7 +616,7 @@ test('renderGrid hides empty-state and populates grid when sessions exist', () = globalThis.document.querySelectorAll = origQSA; }); -test('renderGrid includes auth tile HTML when a source is auth_required', () => { +test('renderGrid includes auth tile HTML when a session has auth_failed status', () => { const mockGrid = { innerHTML: '' }; const mockEmpty = { style: {}, classList: { add: () => {}, remove: () => {} } }; const origGetById = globalThis.document.getElementById; @@ -628,12 +628,10 @@ test('renderGrid includes auth tile HTML when a source is auth_required', () => }; globalThis.document.querySelectorAll = () => []; - app._setSources([ - { url: 'http://local:8088', name: 'Local', status: 'authenticated' }, - { url: 'http://workstation:8088', name: 'Workstation', status: 'auth_required' }, - ]); - - const sessions = [{ name: 'my-session', snapshot: 'hello' }]; + const sessions = [ + { name: 'my-session', snapshot: 'hello' }, + { name: 'Workstation', status: 'auth_failed' }, + ]; app.renderGrid(sessions); assert.ok(mockGrid.innerHTML.includes('source-tile--auth'), 'grid should include auth tile class'); @@ -641,10 +639,9 @@ test('renderGrid includes auth tile HTML when a source is auth_required', () => globalThis.document.getElementById = origGetById; globalThis.document.querySelectorAll = origQSA; - app._setSources([]); }); -test('renderGrid includes offline tile HTML when a source is unreachable', () => { +test('renderGrid includes offline tile HTML when a session has unreachable status', () => { const mockGrid = { innerHTML: '' }; const mockEmpty = { style: {}, classList: { add: () => {}, remove: () => {} } }; const origGetById = globalThis.document.getElementById; @@ -656,12 +653,10 @@ test('renderGrid includes offline tile HTML when a source is unreachable', () => }; globalThis.document.querySelectorAll = () => []; - app._setSources([ - { url: 'http://local:8088', name: 'Local', status: 'authenticated' }, - { url: 'http://devserver:8088', name: 'Dev Server', status: 'unreachable', lastSeenAt: Date.now() - 300000 }, - ]); - - const sessions = [{ name: 'my-session', snapshot: 'hello' }]; + const sessions = [ + { name: 'my-session', snapshot: 'hello' }, + { name: 'Dev Server', status: 'unreachable' }, + ]; app.renderGrid(sessions); assert.ok(mockGrid.innerHTML.includes('source-tile--offline'), 'grid should include offline tile class'); @@ -669,10 +664,9 @@ test('renderGrid includes offline tile HTML when a source is unreachable', () => globalThis.document.getElementById = origGetById; globalThis.document.querySelectorAll = origQSA; - app._setSources([]); }); -test('renderGrid shows auth tile and hides empty-state when no sessions but source is auth_required', () => { +test('renderGrid shows auth tile and hides empty-state when session has auth_failed status', () => { const addedClasses = []; const removedClasses = []; const mockGrid = { innerHTML: '' }; @@ -684,22 +678,17 @@ test('renderGrid shows auth tile and hides empty-state when no sessions but sour return null; }; - app._setSources([ - { url: 'http://workstation:8088', name: 'Workstation', status: 'auth_required' }, - ]); + // Pass status entry as a session — early-return path (no regular sessions) + app.renderGrid([{ name: 'Workstation', status: 'auth_failed' }]); - // No sessions — early-return path - app.renderGrid([]); - - assert.ok(mockGrid.innerHTML.includes('source-tile--auth'), 'grid should show auth tile even when sessions list is empty'); + assert.ok(mockGrid.innerHTML.includes('source-tile--auth'), 'grid should show auth tile even when no regular sessions'); assert.ok(addedClasses.includes('hidden'), 'empty-state should be hidden when status tiles are present'); assert.ok(!removedClasses.includes('hidden'), 'empty-state hidden class should NOT be removed when status tiles are present'); globalThis.document.getElementById = origGetById; - app._setSources([]); }); -test('renderGrid shows offline tile and hides empty-state when no sessions but source is unreachable', () => { +test('renderGrid shows offline tile and hides empty-state when session has unreachable status', () => { const addedClasses = []; const mockGrid = { innerHTML: '' }; const mockEmpty = { style: {}, classList: { add: (c) => addedClasses.push(c), remove: () => {} } }; @@ -710,18 +699,13 @@ test('renderGrid shows offline tile and hides empty-state when no sessions but s return null; }; - app._setSources([ - { url: 'http://devbox:8088', name: 'Dev Box', status: 'unreachable', lastSeenAt: Date.now() - 120000 }, - ]); + // Pass status entry as a session — early-return path + app.renderGrid([{ name: 'Dev Box', status: 'unreachable' }]); - // No sessions — early-return path - app.renderGrid([]); - - assert.ok(mockGrid.innerHTML.includes('source-tile--offline'), 'grid should show offline tile even when sessions list is empty'); + assert.ok(mockGrid.innerHTML.includes('source-tile--offline'), 'grid should show offline tile even when no regular sessions'); assert.ok(addedClasses.includes('hidden'), 'empty-state should be hidden when status tiles are present'); globalThis.document.getElementById = origGetById; - app._setSources([]); }); test('_previewClickHandler looks up remoteId from _currentSessions before calling openSession', () => { @@ -2377,10 +2361,6 @@ test('createNewSession polls for session before auto-opening (not immediate setT // --- federation state helpers (task-3) --- -test('_setSources sets internal _sources array', () => { - assert.doesNotThrow(() => app._setSources([{ url: '', name: 'test' }])); -}); - test('_setServerSettings sets internal _serverSettings', () => { assert.doesNotThrow(() => app._setServerSettings({ sort_order: 'recent' })); }); @@ -2390,122 +2370,6 @@ test('_getGridViewMode returns current _gridViewMode value', () => { assert.strictEqual(app._getGridViewMode(), 'flat', '_gridViewMode should default to flat'); }); -test('_getSources returns current _sources array', () => { - app._setSources([{ url: 'http://test', name: 'Test' }]); - const sources = app._getSources(); - assert.ok(Array.isArray(sources), '_getSources should return an array'); - assert.strictEqual(sources[0].url, 'http://test', '_getSources should return value set by _setSources'); -}); - -// --- buildSources --- - -test('buildSources returns only local source when no remote_instances', () => { - const sources = app.buildSources({ device_name: 'Laptop' }); - assert.strictEqual(sources.length, 1, 'should return exactly one source (local)'); - assert.strictEqual(sources[0].url, '', 'local source url should be empty string'); - assert.strictEqual(sources[0].name, 'Laptop', 'local source name should be device_name'); - assert.strictEqual(sources[0].type, 'local', 'local source type should be local'); - assert.strictEqual(sources[0].status, 'authenticated', 'local source status should be authenticated'); -}); - -test('buildSources returns local + remote sources from remote_instances', () => { - const sources = app.buildSources({ - device_name: 'Laptop', - remote_instances: [ - { url: 'https://server1.example.com', name: 'Server 1' }, - { url: 'https://server2.example.com', name: 'Server 2' }, - ], - }); - assert.strictEqual(sources.length, 3, 'should return 3 sources: 1 local + 2 remote'); - assert.strictEqual(sources[0].type, 'local', 'first source should be local'); - assert.strictEqual(sources[1].type, 'remote', 'second source should be remote'); - assert.strictEqual(sources[1].url, 'https://server1.example.com', 'remote source url should match'); - assert.strictEqual(sources[1].name, 'Server 1', 'remote source name should match'); - assert.strictEqual(sources[2].type, 'remote', 'third source should be remote'); - assert.strictEqual(sources[2].url, 'https://server2.example.com', 'remote source url should match'); -}); - -test('buildSources uses hostname fallback when device_name is empty', () => { - const sources = app.buildSources({}); - assert.strictEqual(sources.length, 1, 'should return one source'); - assert.strictEqual(sources[0].name, 'This device', 'local source name should fall back to This device'); -}); - -test('buildSources strips trailing slash from remote URLs', () => { - const sources = app.buildSources({ - device_name: 'Laptop', - remote_instances: [ - { url: 'https://server1.example.com/', name: 'Server 1' }, - { url: 'https://server2.example.com///', name: 'Server 2' }, - ], - }); - assert.strictEqual(sources[1].url, 'https://server1.example.com', 'trailing slash should be stripped from remote URL'); - assert.strictEqual(sources[2].url, 'https://server2.example.com', 'multiple trailing slashes should be stripped from remote URL'); -}); - -// --- tagSessions --- - -test('tagSessions adds deviceName and sourceUrl to each session', () => { - const sessions = [{ name: 'work' }, { name: 'play' }]; - const result = app.tagSessions(sessions, 'Laptop', 'https://host.example.com'); - assert.strictEqual(result[0].deviceName, 'Laptop', 'first session should have deviceName set'); - assert.strictEqual(result[0].sourceUrl, 'https://host.example.com', 'first session should have sourceUrl set'); - assert.strictEqual(result[1].deviceName, 'Laptop', 'second session should have deviceName set'); - assert.strictEqual(result[1].sourceUrl, 'https://host.example.com', 'second session should have sourceUrl set'); -}); - -test('tagSessions adds sessionKey formatted as sourceUrl::name', () => { - const sessions = [{ name: 'my-session' }]; - const result = app.tagSessions(sessions, 'Laptop', 'https://host.example.com'); - assert.strictEqual(result[0].sessionKey, 'https://host.example.com::my-session', 'sessionKey should be sourceUrl::name'); -}); - -test('tagSessions handles empty sessions input (returns empty array)', () => { - const result = app.tagSessions([], 'Laptop', 'https://host.example.com'); - assert.deepStrictEqual(result, [], 'tagSessions should return empty array for empty input'); -}); - -test('tagSessions does not mutate the original session objects', () => { - const original = { name: 'work' }; - const sessions = [original]; - const result = app.tagSessions(sessions, 'Laptop', 'https://host.example.com'); - assert.ok(!('deviceName' in original), 'original session should not be mutated'); - assert.ok(!('sourceUrl' in original), 'original session should not have sourceUrl added'); - assert.ok(!('sessionKey' in original), 'original session should not have sessionKey added'); - assert.notStrictEqual(result[0], original, 'returned session should be a new object, not the original'); -}); - -// --- mergeSources --- - -test('mergeSources combines sessions from multiple sources with correct sessionKeys', () => { - const results = [ - { - source: { name: 'Laptop', url: '' }, - sessions: [{ name: 'local-session' }], - }, - { - source: { name: 'Server', url: 'https://server.example.com' }, - sessions: [{ name: 'remote-session' }], - }, - ]; - const merged = app.mergeSources(results); - assert.strictEqual(merged.length, 2, 'merged array should have 2 sessions total'); - const local = merged.find((s) => s.name === 'local-session'); - const remote = merged.find((s) => s.name === 'remote-session'); - assert.ok(local, 'local-session should be present'); - assert.ok(remote, 'remote-session should be present'); - assert.strictEqual(local.sessionKey, '::local-session', 'local session key should be ::local-session'); - assert.strictEqual(remote.sessionKey, 'https://server.example.com::remote-session', 'remote session key should include url'); - assert.strictEqual(remote.deviceName, 'Server', 'remote session should have correct deviceName'); -}); - -test('mergeSources returns empty array for empty input', () => { - const result = app.mergeSources([]); - assert.deepStrictEqual(result, [], 'mergeSources should return empty array for empty input'); -}); - - - // --- getVisibleSessions (task-7) --- test('getVisibleSessions exported and filters hidden sessions', () => { @@ -2561,37 +2425,29 @@ test('getVisibleSessions hides local sessions by name but not remote sessions wi // --- buildSidebarHTML device badge (task-10) --- -test('buildSidebarHTML shows device-badge when multiple sources configured', () => { - app._setSources([ - { url: '', name: 'This device', type: 'local', status: 'authenticated', backoffMs: 2000 }, - { url: 'https://remote.example.com', name: 'Remote', type: 'remote', status: 'authenticated', backoffMs: 2000 }, - ]); +test('buildSidebarHTML shows device-badge when multi_device_enabled', () => { + app._setServerSettings({ multi_device_enabled: true }); const session = { name: 'work', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::work', snapshot: '', bell: { unseen_count: 0 } }; const html = app.buildSidebarHTML(session, null); - assert.ok(html.includes('device-badge'), 'should show device-badge when _sources.length > 1 and session has deviceName'); + assert.ok(html.includes('device-badge'), 'should show device-badge when multi_device_enabled and session has deviceName'); assert.ok(html.includes('Laptop'), 'device-badge should contain the deviceName'); - app._setSources([]); + app._setServerSettings(null); }); -test('buildSidebarHTML omits device-badge when only one source configured', () => { - app._setSources([ - { url: '', name: 'This device', type: 'local', status: 'authenticated', backoffMs: 2000 }, - ]); +test('buildSidebarHTML omits device-badge when multi_device_enabled is false', () => { + app._setServerSettings({ multi_device_enabled: false }); const session = { name: 'work', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::work', snapshot: '', bell: { unseen_count: 0 } }; const html = app.buildSidebarHTML(session, null); - assert.ok(!html.includes('device-badge'), 'should NOT show device-badge when _sources.length is 1'); - app._setSources([]); + assert.ok(!html.includes('device-badge'), 'should NOT show device-badge when multi_device_enabled is false'); + app._setServerSettings(null); }); test('buildSidebarHTML omits device-badge when session has no deviceName', () => { - app._setSources([ - { url: '', name: 'This device', type: 'local', status: 'authenticated', backoffMs: 2000 }, - { url: 'https://remote.example.com', name: 'Remote', type: 'remote', status: 'authenticated', backoffMs: 2000 }, - ]); + app._setServerSettings({ multi_device_enabled: true }); const session = { name: 'work', sourceUrl: '', sessionKey: '::work', snapshot: '', bell: { unseen_count: 0 } }; const html = app.buildSidebarHTML(session, null); assert.ok(!html.includes('device-badge'), 'should NOT show device-badge when session has no deviceName'); - app._setSources([]); + app._setServerSettings(null); }); test('buildSidebarHTML includes data-remote-id attribute on article element', () => { @@ -2614,39 +2470,31 @@ test('buildSidebarHTML data-remote-id is empty string when session has no remote }); test('buildSidebarHTML escapes HTML in deviceName within device-badge', () => { - app._setSources([ - { url: '', name: 'This device', type: 'local', status: 'authenticated', backoffMs: 2000 }, - { url: 'https://remote.example.com', name: 'Remote', type: 'remote', status: 'authenticated', backoffMs: 2000 }, - ]); + app._setServerSettings({ multi_device_enabled: true }); const session = { name: 'work', deviceName: '', sourceUrl: '', sessionKey: '::work', snapshot: '', bell: { unseen_count: 0 } }; const html = app.buildSidebarHTML(session, null); assert.ok(!html.includes('', sourceUrl: '', sessionKey: '::work', snapshot: '' }; const html = app.buildTileHTML(session, 0, false); assert.ok(!html.includes('