From 1d49b54bd14066ac48d9b358e22b0aab8b3433e4 Mon Sep 17 00:00:00 2001 From: serversdown Date: Thu, 11 Jun 2026 20:29:20 +0000 Subject: [PATCH] feat(reports): baseline-source editor in the settings modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gear → Settings now has a "Baseline source" toggle: - Captured nights → the date-range fields (existing). - Fixed values → a per-NRL grid (metrics × Evening/Nighttime) to type spec limits or prior-report averages, with a "Copy first NRL → all" helper. Loads from GET /reports/baseline, saves mode via PUT /config and the per-NRL values via PUT /reports/baseline. Verified the template renders + gates to sound. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../partials/projects/project_header.html | 117 ++++++++++++++++-- 1 file changed, 107 insertions(+), 10 deletions(-) diff --git a/templates/partials/projects/project_header.html b/templates/partials/projects/project_header.html index 860dd70..0bed680 100644 --- a/templates/partials/projects/project_header.html +++ b/templates/partials/projects/project_header.html @@ -235,14 +235,26 @@ function loadRecentReports(projectId) {

Runs after this time for the night that just ended.

-
-
- - +
+ +
+ +
-
- - +
+
+ + +
+
+ + +
+
+
@@ -288,6 +300,8 @@ function openReportSettings(projectId) { ss.innerHTML = ' Automatic sending is off. Last reported night: ' + last + '.'; } document.getElementById('rs-test-status').textContent = ''; + rsSetMode(c.baseline_mode || 'captured'); + loadBaselineEditor(projectId); show(); }) .catch(show); @@ -297,15 +311,17 @@ function closeReportSettings() { } function saveReportSettings(projectId) { var st = document.getElementById('rs-status'); + var mode = rsGetMode(); var bs = document.getElementById('rs-baseline-start').value; var be = document.getElementById('rs-baseline-end').value; - if ((bs && !be) || (be && !bs)) { + if (mode === 'captured' && ((bs && !be) || (be && !bs))) { st.style.color = '#b00020'; st.textContent = 'Provide both baseline dates, or neither.'; return; } var body = { enabled: document.getElementById('rs-enabled').checked, report_time: document.getElementById('rs-report-time').value || '08:00', metric_keys: document.getElementById('rs-metrics').value || 'lmax,l01,l10,l90', + baseline_mode: mode, baseline_start: bs || null, baseline_end: be || null, recipients: document.getElementById('rs-recipients').value || '' @@ -315,11 +331,92 @@ function saveReportSettings(projectId) { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { - if (res.ok) { st.style.color = '#1a7f37'; st.textContent = 'Saved.'; setTimeout(closeReportSettings, 700); } - else { st.style.color = '#b00020'; st.textContent = 'Error: ' + (res.j.detail || 'save failed'); } + if (!res.ok) { st.style.color = '#b00020'; st.textContent = 'Error: ' + (res.j.detail || 'save failed'); return; } + if (mode === 'reference') { + return fetch('/api/projects/' + projectId + '/reports/baseline', { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ locations: gatherRefValues() }) + }).then(function (r2) { + if (!r2.ok) throw new Error('baseline values failed to save'); + st.style.color = '#1a7f37'; st.textContent = 'Saved.'; setTimeout(closeReportSettings, 700); + }); + } + st.style.color = '#1a7f37'; st.textContent = 'Saved.'; setTimeout(closeReportSettings, 700); }) .catch(function (e) { st.style.color = '#b00020'; st.textContent = 'Error: ' + e; }); } +var RS_BASELINE = { metrics: [], windows: [], locations: [] }; +function rsGetMode() { + var r = document.querySelector('input[name="rs-baseline-mode"]:checked'); + return r ? r.value : 'captured'; +} +function rsSetMode(mode) { + document.querySelectorAll('input[name="rs-baseline-mode"]').forEach(function (el) { el.checked = (el.value === mode); }); + rsToggleBaselineMode(); +} +function rsToggleBaselineMode() { + var ref = rsGetMode() === 'reference'; + document.getElementById('rs-baseline-captured').classList.toggle('hidden', ref); + document.getElementById('rs-baseline-reference').classList.toggle('hidden', !ref); +} +function loadBaselineEditor(projectId) { + fetch('/api/projects/' + projectId + '/reports/baseline') + .then(function (r) { return r.json(); }) + .then(function (d) { RS_BASELINE = d; renderRefGrid(); }) + .catch(function () {}); +} +function _refId(loc, w, m) { return 'ref__' + loc + '__' + w + '__' + m; } +function renderRefGrid() { + var box = document.getElementById('rs-ref-grid'); + if (!RS_BASELINE.locations || !RS_BASELINE.locations.length) { + box.innerHTML = '
No NRLs in this project yet.
'; return; + } + var W = RS_BASELINE.windows, M = RS_BASELINE.metrics; + box.innerHTML = RS_BASELINE.locations.map(function (loc) { + var head = '' + W.map(function (w) { + return '' + w.label.replace(/\s*\(.*\)/, '') + ''; + }).join('') + ''; + var rows = M.map(function (m) { + var cells = W.map(function (w) { + var v = (loc.values[w.key] && loc.values[w.key][m.key] != null) ? loc.values[w.key][m.key] : ''; + return ''; + }).join(''); + return '' + m.label + '' + cells + ''; + }).join(''); + return '
' + + '
' + loc.name + '
' + + '' + head + rows + '
'; + }).join(''); +} +function gatherRefValues() { + var out = {}; + (RS_BASELINE.locations || []).forEach(function (loc) { + var wins = {}; + RS_BASELINE.windows.forEach(function (w) { + var mv = {}; + RS_BASELINE.metrics.forEach(function (m) { + var el = document.getElementById(_refId(loc.id, w.key, m.key)); + if (el && el.value !== '') mv[m.key] = el.value; + }); + if (Object.keys(mv).length) wins[w.key] = mv; + }); + out[loc.id] = wins; + }); + return out; +} +function rsCopyFirstNrl() { + if (!RS_BASELINE.locations || RS_BASELINE.locations.length < 2) return; + var first = RS_BASELINE.locations[0].id; + RS_BASELINE.locations.slice(1).forEach(function (loc) { + RS_BASELINE.windows.forEach(function (w) { + RS_BASELINE.metrics.forEach(function (m) { + var src = document.getElementById(_refId(first, w.key, m.key)); + var dst = document.getElementById(_refId(loc.id, w.key, m.key)); + if (src && dst) dst.value = src.value; + }); + }); + }); +} function sendTestEmail(projectId) { var st = document.getElementById('rs-test-status'); st.style.color = ''; st.textContent = 'Sending…';