webui: add the Sources & Config sidebar section

Renames the "Add Source" section to "Sources & Config" and replaces the
fire-and-forget "Save list" button with Save and Reload, plus a one-line
status area that renders the hub's configSaved/configReloaded ack.

onConfigAck replaces the no-op stub Task 7 left behind. It branches on the
frame type because configSaved carries a path and configReloaded does not,
and surfaces the hub's error text on failure rather than failing silently.
The status is kept outside the DOM because buildSidebar() recreates this
section on every sources broadcast.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-08-17 07:23:08 +02:00
co-authored by Claude Sonnet 4.6
parent 5917ab1bf2
commit e37eec0276
2 changed files with 62 additions and 8 deletions
+52 -8
View File
@@ -2706,7 +2706,7 @@ function buildSidebar() {
list.appendChild(empty);
}
list.appendChild(makeAddSourceSection());
list.appendChild(makeSourcesConfigSection());
}
function makeDraggable(key, label, typeName, unit) {
const item = document.createElement('div');
@@ -3473,16 +3473,35 @@ function applyCalibrationChanged() {
if (trig.signal) { refreshTrigThresholdField(); sendTrigConfig(); }
}
// Replaced in Task 10 with the Sources & Config status renderer.
function onConfigAck(msg) { /* no-op until Task 10 */ }
// Last config acknowledgement, kept outside the DOM because buildSidebar()
// discards and recreates this whole section on every `sources` broadcast.
let _cfgStatus = null; // {ok: bool, text: string} or null
function makeAddSourceSection() {
function renderCfgStatus(el) {
el.className = 'cfg-status';
if (!_cfgStatus) { el.textContent = ''; return; }
el.classList.add(_cfgStatus.ok ? 'ok' : 'err');
el.textContent = _cfgStatus.text;
}
function onConfigAck(msg) {
const what = msg.type === 'configSaved' ? 'Saved' : 'Reloaded';
if (msg.ok) {
_cfgStatus = { ok: true, text: what + ': ' + (msg.path || 'config file') };
} else {
_cfgStatus = { ok: false, text: what + ' failed: ' + (msg.error || 'unknown error') };
}
const el = document.getElementById('cfg-status');
if (el) renderCfgStatus(el);
}
function makeSourcesConfigSection() {
const section = document.createElement('div');
section.className = 'add-source-section';
const title = document.createElement('div');
title.className = 'add-source-title';
title.innerHTML = '<span class="add-src-arrow">&#x25B6;</span> Add Source';
title.innerHTML = '<span class="add-src-arrow">&#x25B6;</span> Sources &amp; Config';
const body = document.createElement('div');
body.className = 'add-source-body';
@@ -3515,12 +3534,37 @@ function makeAddSourceSection() {
});
addrInput.addEventListener('keydown', e => { if (e.key === 'Enter') addBtn.click(); });
const btnRow = document.createElement('div');
btnRow.className = 'cfg-btn-row';
const saveBtn = document.createElement('button');
saveBtn.className = 'add-src-btn save-src-btn';
saveBtn.textContent = 'Save list'; saveBtn.title = 'Save source list to file';
saveBtn.addEventListener('click', saveConfigWS);
saveBtn.textContent = 'Save';
saveBtn.title = 'Write the source list and all signal calibration to the hub\u2019s config file';
saveBtn.addEventListener('click', () => {
_cfgStatus = null;
const el = document.getElementById('cfg-status'); if (el) renderCfgStatus(el);
saveConfigWS();
});
body.append(addrInput, labelInput, mcastInput, dataPortInput, addBtn, saveBtn);
const reloadBtn = document.createElement('button');
reloadBtn.className = 'add-src-btn reload-src-btn';
reloadBtn.textContent = 'Reload';
reloadBtn.title = 'Re-read the config file: calibration is replaced wholesale, '
+ 'missing sources are added, and no running source is stopped';
reloadBtn.addEventListener('click', () => {
_cfgStatus = null;
const el = document.getElementById('cfg-status'); if (el) renderCfgStatus(el);
reloadConfigWS();
});
btnRow.append(saveBtn, reloadBtn);
const status = document.createElement('div');
status.id = 'cfg-status';
renderCfgStatus(status);
body.append(addrInput, labelInput, mcastInput, dataPortInput, addBtn, btnRow, status);
section.append(title, body);
title.addEventListener('click', () => {