From 442cfcc0513b791e416f021c6e5906eba85ac171 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 1 Apr 2026 12:54:08 -0700 Subject: [PATCH] feat: simplify pollSessions to use federation proxy endpoint --- muxplex/frontend/app.js | 81 +++++------------------------ muxplex/frontend/tests/test_app.mjs | 64 +++++++++++++++++++++++ 2 files changed, 78 insertions(+), 67 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 9492e41..52793fb 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -303,78 +303,25 @@ function setConnectionStatus(level) { * @returns {Promise} */ async function pollSessions() { - // Falls back to local-only if _sources is empty - if (_sources.length === 0) { - try { - const res = await api('GET', '/api/sessions'); - const sessions = await res.json(); - const prev = _currentSessions; - _currentSessions = sessions; - _pollFailCount = 0; - setConnectionStatus('ok'); - renderGrid(sessions); - renderSidebar(sessions, _viewingSession); - handleBellTransitions(prev, sessions); - updateSessionPill(sessions); - } catch (err) { - _pollFailCount++; - setConnectionStatus(_pollFailCount <= 2 ? 'warn' : 'err'); - } - return; - } - - // Multi-source parallel polling - const now = Date.now(); - const fetchResults = await Promise.all( - _sources.map(async (source) => { - // Skip sources currently in backoff (nextRetryAt in future) - if (source.nextRetryAt && now < source.nextRetryAt) { - return null; - } - try { - const res = await api('GET', '/api/sessions', undefined, source.url || undefined); - const sessions = await res.json(); - // Reset source status on success - source.status = 'authenticated'; - source.backoffMs = 2000; - delete source.nextRetryAt; - return { source, sessions }; - } catch (err) { - if (err.status === 401 || err.status === 403) { - source.status = 'auth_required'; - } else { - source.status = 'unreachable'; - // Exponential backoff: current * 2, capped at 30s - const newBackoff = Math.min((source.backoffMs || 2000) * 2, 30000); - source.backoffMs = newBackoff; - source.nextRetryAt = Date.now() + newBackoff; - } - return null; - } - }), - ); - - // Filter skipped/failed sources and merge results - const validResults = fetchResults.filter((r) => r !== null); - const merged = mergeSources(validResults); - - // Connection status is based on local source health - const localSource = _sources.find((s) => s.type === 'local' || s.url === ''); - if (localSource && localSource.status === 'authenticated') { + try { + var endpoint = (_serverSettings && _serverSettings.multi_device_enabled) + ? '/api/federation/sessions' + : '/api/sessions'; + const res = await api('GET', endpoint); + const sessions = await res.json(); + const prev = _currentSessions; + _currentSessions = sessions; _pollFailCount = 0; setConnectionStatus('ok'); - } else { + renderGrid(sessions); + renderSidebar(sessions, _viewingSession); + handleBellTransitions(prev, sessions); + updateSessionPill(sessions); + updateFaviconBadge(); + } catch (err) { _pollFailCount++; setConnectionStatus(_pollFailCount <= 2 ? 'warn' : 'err'); } - - const prev = _currentSessions; - _currentSessions = merged; - renderGrid(merged); - renderSidebar(merged, _viewingSession); - handleBellTransitions(prev, merged); - updateSessionPill(merged); - updateFaviconBadge(); } /** diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 1459b0c..e2cb057 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -375,6 +375,70 @@ test('pollSessions calls renderSidebar when viewMode is fullscreen', async () => app._setViewMode('grid'); }); +// --- pollSessions federation endpoint --- + +test('pollSessions source includes /api/federation/sessions', () => { + assert.ok( + app.pollSessions.toString().includes('/api/federation/sessions'), + 'pollSessions must reference /api/federation/sessions endpoint', + ); +}); + +test('pollSessions source includes multi_device_enabled check', () => { + assert.ok( + app.pollSessions.toString().includes('multi_device_enabled'), + 'pollSessions must check multi_device_enabled flag', + ); +}); + +test('pollSessions uses /api/federation/sessions when multi_device_enabled is true', async () => { + const fetchedUrls = []; + const mockEl = { textContent: '', className: '' }; + const origGetById = globalThis.document.getElementById; + globalThis.document.getElementById = (id) => (id === 'connection-status' ? mockEl : null); + globalThis.fetch = async (url) => { + fetchedUrls.push(url); + return { ok: true, json: async () => [] }; + }; + + app._setServerSettings({ multi_device_enabled: true }); + await app.pollSessions(); + app._setServerSettings(null); + + assert.ok( + fetchedUrls.some((u) => u === '/api/federation/sessions'), + 'should fetch /api/federation/sessions when multi_device_enabled is true', + ); + globalThis.document.getElementById = origGetById; + globalThis.fetch = undefined; +}); + +test('pollSessions uses /api/sessions when multi_device_enabled is false', async () => { + const fetchedUrls = []; + const mockEl = { textContent: '', className: '' }; + const origGetById = globalThis.document.getElementById; + globalThis.document.getElementById = (id) => (id === 'connection-status' ? mockEl : null); + globalThis.fetch = async (url) => { + fetchedUrls.push(url); + return { ok: true, json: async () => [] }; + }; + + app._setServerSettings({ multi_device_enabled: false }); + await app.pollSessions(); + app._setServerSettings(null); + + assert.ok( + fetchedUrls.some((u) => u === '/api/sessions'), + 'should fetch /api/sessions when multi_device_enabled is false', + ); + assert.ok( + !fetchedUrls.some((u) => u === '/api/federation/sessions'), + 'should NOT fetch /api/federation/sessions when multi_device_enabled is false', + ); + globalThis.document.getElementById = origGetById; + globalThis.fetch = undefined; +}); + // --- startPolling --- test('startPolling guards against double-start (only creates one interval)', () => {