From e37eec0276c07814c4bb89382b3ec9c109fbd349 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 17 Aug 2026 07:23:08 +0200 Subject: [PATCH] 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 --- Client/udpstreamer/static/app.js | 60 +++++++++++++++++++++++++---- Client/udpstreamer/static/style.css | 10 +++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/Client/udpstreamer/static/app.js b/Client/udpstreamer/static/app.js index 7695363..10afc84 100644 --- a/Client/udpstreamer/static/app.js +++ b/Client/udpstreamer/static/app.js @@ -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 = ' Add Source'; + title.innerHTML = ' Sources & 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', () => { diff --git a/Client/udpstreamer/static/style.css b/Client/udpstreamer/static/style.css index 64d83c1..2b6803d 100644 --- a/Client/udpstreamer/static/style.css +++ b/Client/udpstreamer/static/style.css @@ -478,6 +478,16 @@ input[type=range].trig-range::-webkit-slider-thumb { .add-src-btn:hover { background:rgba(137,180,250,0.15); border-color:var(--accent); } .save-src-btn { color:var(--green); } .save-src-btn:hover { background:rgba(166,227,161,0.1); border-color:var(--green); } +.cfg-btn-row { display:flex; gap:6px; } +.cfg-btn-row .add-src-btn { flex:1; } +.reload-src-btn { color:var(--peach); } +.reload-src-btn:hover { background:rgba(250,179,135,0.1); border-color:var(--peach); } +.cfg-status { + font-size:10px; line-height:1.3; padding:2px 0; min-height:13px; + overflow-wrap:anywhere; +} +.cfg-status.ok { color:var(--green); } +.cfg-status.err { color:var(--red); } /* ── Stats panel ─────────────────────────────────────────────── */ #stats-panel {