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 {