feat(reports): FTP night-report pipeline foundation #62
@@ -235,16 +235,28 @@ function loadRecentReports(projectId) {
|
|||||||
<input type="time" id="rs-report-time" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
<input type="time" id="rs-report-time" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
||||||
<p class="text-xs text-gray-400 mt-1">Runs after this time for the night that just ended.</p>
|
<p class="text-xs text-gray-400 mt-1">Runs after this time for the night that just ended.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Baseline start</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Baseline source</label>
|
||||||
|
<div class="flex gap-4 text-sm mb-2">
|
||||||
|
<label class="flex items-center gap-1.5"><input type="radio" name="rs-baseline-mode" value="captured" onchange="rsToggleBaselineMode()" class="text-indigo-600"> Captured nights</label>
|
||||||
|
<label class="flex items-center gap-1.5"><input type="radio" name="rs-baseline-mode" value="reference" onchange="rsToggleBaselineMode()" class="text-indigo-600"> Fixed values</label>
|
||||||
|
</div>
|
||||||
|
<div id="rs-baseline-captured" class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Range start</label>
|
||||||
<input type="date" id="rs-baseline-start" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
<input type="date" id="rs-baseline-start" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Baseline end</label>
|
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Range end</label>
|
||||||
<input type="date" id="rs-baseline-end" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
<input type="date" id="rs-baseline-end" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="rs-baseline-reference" class="hidden">
|
||||||
|
<p class="text-xs text-gray-400 mb-2">Values to compare against (a spec limit like L10 = 85, or a prior report's averages). Blank cells aren't compared.</p>
|
||||||
|
<div class="flex justify-end mb-1"><button type="button" onclick="rsCopyFirstNrl()" class="text-xs text-indigo-600 dark:text-indigo-400 hover:underline">Copy first NRL → all</button></div>
|
||||||
|
<div id="rs-ref-grid" class="space-y-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Metrics</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Metrics</label>
|
||||||
<input type="text" id="rs-metrics" placeholder="lmax,l01,l10,l90" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
<input type="text" id="rs-metrics" placeholder="lmax,l01,l10,l90" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm">
|
||||||
@@ -288,6 +300,8 @@ function openReportSettings(projectId) {
|
|||||||
ss.innerHTML = '<span style="color:#9ca3af">●</span> Automatic sending is off. Last reported night: ' + last + '.';
|
ss.innerHTML = '<span style="color:#9ca3af">●</span> Automatic sending is off. Last reported night: ' + last + '.';
|
||||||
}
|
}
|
||||||
document.getElementById('rs-test-status').textContent = '';
|
document.getElementById('rs-test-status').textContent = '';
|
||||||
|
rsSetMode(c.baseline_mode || 'captured');
|
||||||
|
loadBaselineEditor(projectId);
|
||||||
show();
|
show();
|
||||||
})
|
})
|
||||||
.catch(show);
|
.catch(show);
|
||||||
@@ -297,15 +311,17 @@ function closeReportSettings() {
|
|||||||
}
|
}
|
||||||
function saveReportSettings(projectId) {
|
function saveReportSettings(projectId) {
|
||||||
var st = document.getElementById('rs-status');
|
var st = document.getElementById('rs-status');
|
||||||
|
var mode = rsGetMode();
|
||||||
var bs = document.getElementById('rs-baseline-start').value;
|
var bs = document.getElementById('rs-baseline-start').value;
|
||||||
var be = document.getElementById('rs-baseline-end').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;
|
st.style.color = '#b00020'; st.textContent = 'Provide both baseline dates, or neither.'; return;
|
||||||
}
|
}
|
||||||
var body = {
|
var body = {
|
||||||
enabled: document.getElementById('rs-enabled').checked,
|
enabled: document.getElementById('rs-enabled').checked,
|
||||||
report_time: document.getElementById('rs-report-time').value || '08:00',
|
report_time: document.getElementById('rs-report-time').value || '08:00',
|
||||||
metric_keys: document.getElementById('rs-metrics').value || 'lmax,l01,l10,l90',
|
metric_keys: document.getElementById('rs-metrics').value || 'lmax,l01,l10,l90',
|
||||||
|
baseline_mode: mode,
|
||||||
baseline_start: bs || null,
|
baseline_start: bs || null,
|
||||||
baseline_end: be || null,
|
baseline_end: be || null,
|
||||||
recipients: document.getElementById('rs-recipients').value || ''
|
recipients: document.getElementById('rs-recipients').value || ''
|
||||||
@@ -315,11 +331,92 @@ function saveReportSettings(projectId) {
|
|||||||
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
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 (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.ok) { st.style.color = '#1a7f37'; st.textContent = 'Saved.'; setTimeout(closeReportSettings, 700); }
|
if (!res.ok) { st.style.color = '#b00020'; st.textContent = 'Error: ' + (res.j.detail || 'save failed'); return; }
|
||||||
else { st.style.color = '#b00020'; st.textContent = 'Error: ' + (res.j.detail || 'save failed'); }
|
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; });
|
.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 = '<div class="text-xs text-gray-400">No NRLs in this project yet.</div>'; return;
|
||||||
|
}
|
||||||
|
var W = RS_BASELINE.windows, M = RS_BASELINE.metrics;
|
||||||
|
box.innerHTML = RS_BASELINE.locations.map(function (loc) {
|
||||||
|
var head = '<tr><th></th>' + W.map(function (w) {
|
||||||
|
return '<th class="text-xs text-gray-400 font-normal pb-1 px-1">' + w.label.replace(/\s*\(.*\)/, '') + '</th>';
|
||||||
|
}).join('') + '</tr>';
|
||||||
|
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 '<td class="px-1"><input type="number" step="0.1" id="' + _refId(loc.id, w.key, m.key) + '" value="' + v + '" class="w-16 px-1.5 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm text-center"></td>';
|
||||||
|
}).join('');
|
||||||
|
return '<tr><td class="text-sm text-gray-700 dark:text-gray-300 pr-2">' + m.label + '</td>' + cells + '</tr>';
|
||||||
|
}).join('');
|
||||||
|
return '<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-2">'
|
||||||
|
+ '<div class="text-sm font-medium text-gray-800 dark:text-gray-200 mb-1">' + loc.name + '</div>'
|
||||||
|
+ '<table class="w-full">' + head + rows + '</table></div>';
|
||||||
|
}).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) {
|
function sendTestEmail(projectId) {
|
||||||
var st = document.getElementById('rs-test-status');
|
var st = document.getElementById('rs-test-status');
|
||||||
st.style.color = ''; st.textContent = 'Sending…';
|
st.style.color = ''; st.textContent = 'Sending…';
|
||||||
|
|||||||
Reference in New Issue
Block a user