diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 9f01d2f..94a1d26 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1409,12 +1409,13 @@ async function patchServerSetting(key, value) { } /** - * Build a single remote instance row element with URL input, name input, and remove button. + * Build a single remote instance row element with URL input, name input, key input, and remove button. * @param {string} url - remote instance URL * @param {string} name - remote instance display name + * @param {string} key - federation key for the remote instance * @returns {HTMLDivElement} */ -function _buildRemoteInstanceRow(url, name) { +function _buildRemoteInstanceRow(url, name, key) { var row = document.createElement('div'); row.className = 'settings-remote-row'; var urlInput = document.createElement('input'); @@ -1429,12 +1430,19 @@ function _buildRemoteInstanceRow(url, name) { nameInput.placeholder = 'Device name'; nameInput.value = name || ''; nameInput.setAttribute('aria-label', 'Remote instance display name'); + var keyInput = document.createElement('input'); + keyInput.type = 'password'; + keyInput.className = 'settings-remote-key'; + keyInput.placeholder = 'Federation key'; + keyInput.value = key || ''; + keyInput.setAttribute('aria-label', 'Federation key for remote instance'); var removeBtn = document.createElement('button'); removeBtn.className = 'settings-remote-remove'; removeBtn.textContent = '\u00d7'; removeBtn.setAttribute('aria-label', 'Remove remote instance'); row.appendChild(urlInput); row.appendChild(nameInput); + row.appendChild(keyInput); row.appendChild(removeBtn); return row; } @@ -1449,10 +1457,12 @@ function _saveRemoteInstances() { container.querySelectorAll('.settings-remote-row').forEach(function(row) { var urlEl = row.querySelector('.settings-remote-url'); var nameEl = row.querySelector('.settings-remote-name'); + var keyEl = row.querySelector('.settings-remote-key'); var url = (urlEl && urlEl.value) ? urlEl.value.trim() : ''; var name = (nameEl && nameEl.value) ? nameEl.value.trim() : ''; + var key = (keyEl && keyEl.value) ? keyEl.value.trim() : ''; if (url) { - instances.push({ url: url, name: name }); + instances.push({ url: url, name: name, key: key }); } }); patchServerSetting('remote_instances', instances); @@ -1799,7 +1809,7 @@ function openSettings() { remoteInstancesEl.innerHTML = ''; var remotes = (ss && ss.remote_instances) || []; remotes.forEach(function(r) { - remoteInstancesEl.appendChild(_buildRemoteInstanceRow(r.url || '', r.name || '')); + remoteInstancesEl.appendChild(_buildRemoteInstanceRow(r.url || '', r.name || '', r.key || '')); }); } @@ -2336,11 +2346,25 @@ function bindStaticEventListeners() { }, 500); }); + // Multi-Device tab — federation generate key button + on($('federation-generate-btn'), 'click', function() { + api('POST', '/api/federation/generate-key').then(function(data) { + var displayEl = $('federation-key-display'); + if (displayEl && data && data.key) { + displayEl.textContent = data.key; + displayEl.classList.add('settings-key-display--visible'); + } + showToast('Federation key generated'); + }).catch(function() { + showToast('Failed to generate federation key'); + }); + }); + // Multi-Device tab — add remote instance button on($('add-remote-instance-btn'), 'click', function() { var container = $('setting-remote-instances'); if (container) { - container.appendChild(_buildRemoteInstanceRow('', '')); + container.appendChild(_buildRemoteInstanceRow('', '', '')); } }); diff --git a/muxplex/frontend/index.html b/muxplex/frontend/index.html index cee7282..407cf90 100644 --- a/muxplex/frontend/index.html +++ b/muxplex/frontend/index.html @@ -242,6 +242,14 @@ +
+ +
+ •••••••• + +
+ Share this key with remote instances to allow federation access +
diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index dca90e7..1274cb4 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -1746,3 +1746,36 @@ body { #setting-device-name { max-width: 200px; } + +/* Federation key display */ +.settings-federation-key { + display: flex; + align-items: center; + gap: 8px; +} + +.settings-key-display { + font-family: var(--font-mono); + font-size: 13px; + color: var(--text-muted); + user-select: all; +} + +.settings-key-display--visible { + color: var(--accent); + background: var(--accent-dim); + padding: 4px 8px; + border-radius: 4px; +} + +/* Remote instance federation key input */ +.settings-remote-key { + flex: 1; + min-width: 100px; + font-size: 12px; + padding: 4px 6px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text); +} diff --git a/muxplex/tests/test_frontend_html.py b/muxplex/tests/test_frontend_html.py index 3377f47..67ec556 100644 --- a/muxplex/tests/test_frontend_html.py +++ b/muxplex/tests/test_frontend_html.py @@ -1337,3 +1337,37 @@ def test_html_display_panel_no_view_scope() -> None: assert el is None, ( "#setting-view-scope must NOT be in the display panel (moved to Multi-Device tab)" ) + + +# ============================================================ +# Federation Key Management UI (task-6-federation-key-ui) +# ============================================================ + + +def test_html_settings_federation_key_section() -> None: + """Multi-Device tab must contain federation-key-display and federation-generate-btn.""" + soup = _SOUP + devices_panel = soup.find("div", attrs={"data-tab": "devices"}) + assert devices_panel is not None, "Missing devices panel (data-tab='devices')" + key_display = devices_panel.find(id="federation-key-display") + assert key_display is not None, ( + "Missing #federation-key-display inside devices panel" + ) + generate_btn = devices_panel.find(id="federation-generate-btn") + assert generate_btn is not None, ( + "Missing #federation-generate-btn inside devices panel" + ) + assert generate_btn.name == "button", ( + f"#federation-generate-btn must be a