feat: federation key management UI in Settings > Multi-Device
This commit is contained in:
+29
-5
@@ -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('', '', ''));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -242,6 +242,14 @@
|
||||
<option value="local">Local Only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label">Federation Key</label>
|
||||
<div class="settings-federation-key">
|
||||
<span id="federation-key-display" class="settings-key-display">••••••••</span>
|
||||
<button id="federation-generate-btn" class="settings-action-btn">Generate Key</button>
|
||||
</div>
|
||||
<span class="settings-helper">Share this key with remote instances to allow federation access</span>
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label">Remote Instances</label>
|
||||
<div id="setting-remote-instances" class="settings-remote-list"></div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 <button>, got: {generate_btn.name}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_settings_remote_instance_key_input() -> None:
|
||||
"""Multi-Device tab must contain #setting-remote-instances container."""
|
||||
soup = _SOUP
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
el = devices_panel.find(id="setting-remote-instances")
|
||||
assert el is not None, (
|
||||
"Missing #setting-remote-instances inside devices panel"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user