The Settings → E-mailrapport tab (scheduler.js) saved the SMTP username as `user` and TLS flag as `starttls`, but every backend reader expects `username`/`use_tls` (routes/email.py). Result: username was always empty, server.login() was skipped, and the SMTP server rejected the send — surfacing as a misleading "authentication failed" message even with a valid App Password. The bug was latent because Graph is preferred whenever M365 is connected, so the SMTP path was rarely exercised. - scheduler.js: send/load canonical keys (username, use_tls). The send-report modal (scan.js) already used these. - _load_smtp_config(): normalise legacy user→username / starttls→use_tls so configs saved before the fix work without re-entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
477 lines
23 KiB
JavaScript
477 lines
23 KiB
JavaScript
// ── Scheduler — multi-job (#19) ─────────────────────────────────────────────
|
|
|
|
var _schedJobs = [];
|
|
|
|
function schedLoad() {
|
|
fetch('/api/scheduler/jobs').then(function(r){ return r.json(); }).then(function(d) {
|
|
_schedJobs = d.jobs || [];
|
|
schedRenderJobs();
|
|
schedLoadHistory();
|
|
// Fetch status AFTER rendering so run buttons exist in the DOM
|
|
return fetch('/api/scheduler/status').then(function(r){ return r.json(); });
|
|
}).then(function(d) {
|
|
if (!d) return;
|
|
var noAps = document.getElementById('schedNoAps');
|
|
if (noAps) noAps.style.display = d.available ? 'none' : 'block';
|
|
schedUpdateSidebarIndicator(d);
|
|
(d.jobs || []).forEach(function(js) {
|
|
var descEl = document.getElementById('schedDesc_' + js.id);
|
|
if (!descEl) return;
|
|
var j2 = _schedJobs.find(function(x){ return x.id === js.id; });
|
|
var freqLabel = !j2 ? '' : (j2.frequency === 'weekly' ? t('m365_sched_freq_weekly','Weekly') : j2.frequency === 'monthly' ? t('m365_sched_freq_monthly','Monthly') : t('m365_sched_freq_daily','Daily'));
|
|
var timeStr = !j2 ? '' : String(j2.hour||0).padStart(2,'0') + ':' + String(j2.minute||0).padStart(2,'0');
|
|
var base = freqLabel + ' ' + timeStr;
|
|
var runBtn = document.getElementById('schedRunBtn_' + js.id);
|
|
if (js.is_running) {
|
|
descEl.textContent = base + ' \u00b7 ' + t('m365_sched_running','Running...');
|
|
if (runBtn) { runBtn.style.borderColor='#22c55e'; runBtn.style.color='#22c55e'; }
|
|
} else if (js.next_run) {
|
|
var dt = new Date(js.next_run);
|
|
descEl.textContent = base + ' \u00b7 ' + t('m365_sched_next','Next') + ': ' + dt.toLocaleString(undefined,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
|
|
if (runBtn) { runBtn.style.borderColor='var(--border)'; runBtn.style.color='var(--muted)'; }
|
|
} else {
|
|
descEl.textContent = base + (js.enabled ? '' : ' \u00b7 ' + t('m365_sched_disabled','Disabled'));
|
|
if (runBtn) { runBtn.style.borderColor='var(--border)'; runBtn.style.color='var(--muted)'; }
|
|
}
|
|
});
|
|
}).catch(function(e){ console.warn('schedLoad:', e); });
|
|
}
|
|
|
|
function schedRenderJobs() {
|
|
var list = document.getElementById('schedJobList');
|
|
if (!list) return;
|
|
if (!_schedJobs.length) {
|
|
list.innerHTML = '<div style="font-size:11px;color:var(--muted);padding:4px 0">' + t('m365_sched_no_jobs','No scheduled scans yet.') + '</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = _schedJobs.map(function(j) {
|
|
var sid = _esc(j.id);
|
|
var sname = _esc(j.name || 'Unnamed');
|
|
var freqLabel = j.frequency === 'weekly' ? t('m365_sched_freq_weekly','Weekly') : j.frequency === 'monthly' ? t('m365_sched_freq_monthly','Monthly') : t('m365_sched_freq_daily','Daily');
|
|
var timeStr = String(j.hour||0).padStart(2,'0') + ':' + String(j.minute||0).padStart(2,'0');
|
|
var desc = freqLabel + ' ' + timeStr;
|
|
var chk = j.enabled ? ' checked' : '';
|
|
var roBadge = j.report_only
|
|
? '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#E8F4FD;color:#2980B9;border:1px solid #AED6F1;margin-left:4px">' + t('m365_sched_report_only','Report only') + '</span>'
|
|
: '';
|
|
return '<div style="display:flex;align-items:center;gap:6px;padding:5px 6px;border:1px solid var(--border);border-radius:6px;background:var(--surface)">'
|
|
+ '<label class="toggle" style="flex:unset;margin:0"><input type="checkbox"'+chk+' onchange="schedToggleEnabled(\''+sid+'\',this.checked)"><span class="toggle-slider"></span></label>'
|
|
+ '<div style="flex:1;min-width:0">'
|
|
+ '<div style="font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+sname+roBadge+'</div>'
|
|
+ '<div id="schedDesc_'+sid+'" style="font-size:10px;color:var(--muted)">'+desc+'</div>'
|
|
+ '</div>'
|
|
+ '<button onclick="schedRunJob(\''+sid+'\')" id="schedRunBtn_'+sid+'" style="background:none;border:1px solid var(--border);color:var(--muted);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer" title="Run now">▶</button>'
|
|
+ '<button onclick="schedEditJob(\''+sid+'\')" style="background:none;border:1px solid var(--border);color:var(--muted);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer" title="Edit">✎</button>'
|
|
+ '<button onclick="schedDeleteJob(\''+sid+'\')" style="background:none;border:1px solid var(--danger);color:var(--danger);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer" title="Delete">✕</button>'
|
|
+ '</div>';
|
|
}).join('');
|
|
}
|
|
|
|
function schedToggleEnabled(id, enabled) {
|
|
var j = _schedJobs.find(function(x){ return x.id === id; });
|
|
if (!j) return;
|
|
var updated = Object.assign({}, j, {enabled: enabled});
|
|
fetch('/api/scheduler/jobs/save', {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify(updated)
|
|
}).then(function(r){ return r.json(); }).then(function(d) {
|
|
if (d.error) { alert('Error: ' + d.error); return; }
|
|
j.enabled = enabled;
|
|
schedLoad();
|
|
}).catch(function(e){ alert('Error: ' + e); });
|
|
}
|
|
|
|
function schedAddJob() {
|
|
document.getElementById('schedEditId').value = '';
|
|
document.getElementById('schedName').value = '';
|
|
document.getElementById('schedEnabled').checked = true;
|
|
document.getElementById('schedFrequency').value = 'daily';
|
|
document.getElementById('schedDow').value = 'mon';
|
|
document.getElementById('schedDom').value = 1;
|
|
document.getElementById('schedHour').value = 2;
|
|
document.getElementById('schedMinute').value = 0;
|
|
document.getElementById('schedAutoEmail').checked = false;
|
|
document.getElementById('schedAutoRetention').checked = false;
|
|
document.getElementById('schedReportOnly').checked = false;
|
|
schedToggleReportOnly();
|
|
var titleEl = document.getElementById('schedEditorTitle');
|
|
if (titleEl) titleEl.textContent = t('m365_sched_editor_new', 'New scheduled scan');
|
|
schedPopulateProfiles('');
|
|
schedToggleFreqRows();
|
|
document.getElementById('schedJobEditor').style.display = 'block';
|
|
document.getElementById('schedSaveStatus').textContent = '';
|
|
document.getElementById('schedName').focus();
|
|
}
|
|
|
|
function schedEditJob(id) {
|
|
var j = _schedJobs.find(function(x){ return x.id === id; });
|
|
if (!j) return;
|
|
document.getElementById('schedEditId').value = j.id;
|
|
document.getElementById('schedName').value = j.name || '';
|
|
document.getElementById('schedEnabled').checked = !!j.enabled;
|
|
document.getElementById('schedFrequency').value = j.frequency || 'daily';
|
|
document.getElementById('schedDow').value = j.day_of_week || 'mon';
|
|
document.getElementById('schedDom').value = j.day_of_month || 1;
|
|
document.getElementById('schedHour').value = j.hour != null ? j.hour : 2;
|
|
document.getElementById('schedMinute').value = j.minute != null ? j.minute : 0;
|
|
document.getElementById('schedAutoEmail').checked = !!j.auto_email;
|
|
document.getElementById('schedAutoRetention').checked = !!j.auto_retention;
|
|
document.getElementById('schedReportOnly').checked = !!j.report_only;
|
|
schedToggleReportOnly();
|
|
var titleEl = document.getElementById('schedEditorTitle');
|
|
if (titleEl) titleEl.textContent = t('m365_sched_editor_edit', 'Edit scheduled scan');
|
|
schedPopulateProfiles(j.profile_id || '');
|
|
schedToggleFreqRows();
|
|
document.getElementById('schedJobEditor').style.display = 'block';
|
|
document.getElementById('schedSaveStatus').textContent = '';
|
|
}
|
|
|
|
function schedCancelEdit() {
|
|
document.getElementById('schedJobEditor').style.display = 'none';
|
|
}
|
|
|
|
function schedToggleReportOnly() {
|
|
var ro = !!(document.getElementById('schedReportOnly') || {}).checked;
|
|
var profileRow = document.getElementById('schedProfileRow');
|
|
var hint = document.getElementById('schedReportOnlyHint');
|
|
if (profileRow) profileRow.style.opacity = ro ? '0.4' : '';
|
|
if (hint) hint.style.display = ro ? 'block' : 'none';
|
|
// Enforce auto_email when switching to report-only
|
|
if (ro) {
|
|
var ae = document.getElementById('schedAutoEmail');
|
|
if (ae) ae.checked = true;
|
|
}
|
|
}
|
|
|
|
function schedSaveJob() {
|
|
var name = document.getElementById('schedName').value.trim();
|
|
if (!name) {
|
|
var st = document.getElementById('schedSaveStatus');
|
|
st.textContent = t('m365_sched_name_required', 'Name is required');
|
|
st.style.color = 'var(--danger)';
|
|
document.getElementById('schedName').focus();
|
|
return;
|
|
}
|
|
var job = {
|
|
id: document.getElementById('schedEditId').value || '',
|
|
name: name,
|
|
enabled: document.getElementById('schedEnabled').checked,
|
|
frequency: document.getElementById('schedFrequency').value,
|
|
day_of_week: document.getElementById('schedDow').value,
|
|
day_of_month: parseInt(document.getElementById('schedDom').value) || 1,
|
|
hour: parseInt(document.getElementById('schedHour').value) || 0,
|
|
minute: parseInt(document.getElementById('schedMinute').value) || 0,
|
|
profile_id: document.getElementById('schedProfile').value,
|
|
auto_email: document.getElementById('schedAutoEmail').checked,
|
|
auto_retention: document.getElementById('schedAutoRetention').checked,
|
|
report_only: document.getElementById('schedReportOnly').checked,
|
|
};
|
|
var st = document.getElementById('schedSaveStatus');
|
|
st.style.color = 'var(--muted)'; st.textContent = 'Saving...';
|
|
fetch('/api/scheduler/jobs/save', {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify(job)
|
|
}).then(function(r){ return r.json(); }).then(function(d) {
|
|
if (d.error) { st.style.color='var(--danger)'; st.textContent=d.error; return; }
|
|
st.style.color = 'var(--accent)'; st.textContent = '\u2713 Saved';
|
|
setTimeout(function(){ st.textContent=''; }, 1500);
|
|
document.getElementById('schedJobEditor').style.display = 'none';
|
|
schedLoad();
|
|
}).catch(function(e){ st.style.color='var(--danger)'; st.textContent=e.message; });
|
|
}
|
|
|
|
function schedDeleteJob(id) {
|
|
var j = _schedJobs.find(function(x){ return x.id === id; });
|
|
var name = j ? j.name : id;
|
|
if (!confirm('Delete "' + name + '"?')) return;
|
|
fetch('/api/scheduler/jobs/delete', {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({id: id})
|
|
}).then(function(r){ return r.json(); }).then(function(d) {
|
|
if (d.error) { alert('Delete failed: ' + d.error); return; }
|
|
schedLoad();
|
|
}).catch(function(e){ alert('Delete error: ' + e); });
|
|
}
|
|
|
|
function schedRunJob(id) {
|
|
var j = _schedJobs.find(function(x){ return x.id === id; });
|
|
var name = j ? j.name : 'this scan';
|
|
if (!confirm('Run "' + name + '" now?')) return;
|
|
fetch('/api/scheduler/jobs/run_now', {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({id: id})
|
|
}).then(function(r){ return r.json(); }).then(function(d) {
|
|
if (d.error) alert(d.error);
|
|
else schedLoad();
|
|
});
|
|
}
|
|
|
|
function schedToggleFreqRows() {
|
|
var freq = document.getElementById('schedFrequency');
|
|
if (!freq) return;
|
|
var val = freq.value;
|
|
var dowRow = document.getElementById('schedDowRow');
|
|
var domRow = document.getElementById('schedDomRow');
|
|
if (dowRow) dowRow.style.display = val === 'weekly' ? 'flex' : 'none';
|
|
if (domRow) domRow.style.display = val === 'monthly' ? 'flex' : 'none';
|
|
}
|
|
|
|
function schedPopulateProfiles(selectedId) {
|
|
fetch('/api/profiles').then(function(r){ return r.json(); }).then(function(d) {
|
|
var sel = document.getElementById('schedProfile');
|
|
if (!sel) return;
|
|
var firstOpt = sel.options[0];
|
|
sel.innerHTML = '';
|
|
sel.appendChild(firstOpt);
|
|
(d.profiles || []).forEach(function(p) {
|
|
var o = document.createElement('option');
|
|
o.value = p.id || p.name;
|
|
o.textContent = p.name;
|
|
if ((p.id || p.name) === selectedId) o.selected = true;
|
|
sel.appendChild(o);
|
|
});
|
|
});
|
|
}
|
|
|
|
function schedLoadHistory() {
|
|
var el = document.getElementById('schedHistory');
|
|
if (!el) return;
|
|
fetch('/api/scheduler/history?limit=10').then(function(r){ return r.json(); }).then(function(d) {
|
|
var runs = d.runs || [];
|
|
if (!runs.length) { el.innerHTML = '<em>' + t('m365_sched_no_runs','No scheduled runs yet') + '</em>'; return; }
|
|
var html = '';
|
|
runs.forEach(function(r) {
|
|
var ts = r.started_at ? new Date(r.started_at * 1000).toLocaleString() : '-';
|
|
var icon = r.status === 'completed' ? '\u2713' : r.status === 'failed' ? '\u2716' : '\u23f3';
|
|
var jname = r.job_name ? '<strong>' + _esc(r.job_name) + '</strong> - ' : '';
|
|
html += icon + ' ' + jname + ts + ' - ' + (r.flagged||0) + ' flagged';
|
|
if (r.emailed) html += ' \u2709';
|
|
if (r.error) html += ' <span style="color:var(--danger)">' + _esc(r.error.substring(0,60)) + '</span>';
|
|
html += '<br>';
|
|
});
|
|
el.innerHTML = html;
|
|
});
|
|
}
|
|
|
|
function schedUpdateSidebarIndicator(d) {
|
|
var wrap = document.getElementById('schedNextIndicator');
|
|
var txt = document.getElementById('schedNextText');
|
|
if (!wrap || !txt) return;
|
|
if (d && d.enabled && d.next_run) {
|
|
try {
|
|
var dt = new Date(d.next_run);
|
|
txt.textContent = t('m365_sched_next', 'Next') + ': ' + dt.toLocaleString(undefined, {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
|
|
wrap.style.display = 'inline-flex';
|
|
} catch(e) { wrap.style.display = 'none'; }
|
|
} else {
|
|
wrap.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Poll scheduler status every 60s
|
|
setInterval(function() {
|
|
fetch('/api/scheduler/status').then(function(r){ return r.json(); }).then(function(d) {
|
|
schedUpdateSidebarIndicator(d);
|
|
}).catch(function(){});
|
|
}, 60000);
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
fetch('/api/scheduler/status').then(function(r){ return r.json(); }).then(function(d) {
|
|
schedUpdateSidebarIndicator(d);
|
|
}).catch(function(){});
|
|
});
|
|
|
|
// ── General tab ───────────────────────────────────────────────────────────────
|
|
|
|
function stPopulateGeneral() {
|
|
stLoadPinStatus();
|
|
// Populate language selector (mirrors the hidden langSelect)
|
|
const src = document.getElementById('langSelect');
|
|
const dst = document.getElementById('langSelectSettings');
|
|
if (src && dst && dst.options.length === 0) {
|
|
Array.from(src.options).forEach(function(opt) {
|
|
const o = document.createElement('option');
|
|
o.value = opt.value; o.textContent = opt.textContent;
|
|
if (opt.selected) o.selected = true;
|
|
dst.appendChild(o);
|
|
});
|
|
} else if (src && dst) {
|
|
dst.value = src.value;
|
|
}
|
|
// Populate About rows
|
|
fetch('/api/about').then(function(r){ return r.json(); }).then(function(d) {
|
|
const set = function(id, val) { const el=document.getElementById(id); if(el) el.textContent=val||'\u2014'; };
|
|
set('st-about-python', d.python);
|
|
set('st-about-msal', d.msal);
|
|
set('st-about-requests',d.requests);
|
|
set('st-about-openpyxl',d.openpyxl);
|
|
}).catch(function(){});
|
|
}
|
|
|
|
// ── Email tab ─────────────────────────────────────────────────────────────────
|
|
|
|
function stLoadSmtp() {
|
|
fetch('/api/smtp/config').then(function(r){ return r.json(); }).then(function(d) {
|
|
const set = function(id, val) { const el=document.getElementById(id); if(el) el.value=val||''; };
|
|
set('st-smtpHost', d.host);
|
|
set('st-smtpPort', d.port || 587);
|
|
set('st-smtpUser', d.username);
|
|
set('st-smtpFrom', d.from_addr);
|
|
set('st-smtpTo', Array.isArray(d.recipients) ? d.recipients.join(', ') : (d.recipients||''));
|
|
const tls = document.getElementById('st-smtpTls');
|
|
if (tls) tls.checked = d.use_tls !== false;
|
|
const pw = document.getElementById('st-smtpPw');
|
|
if (pw) pw.value = d.has_password ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : '';
|
|
const ae = document.getElementById('st-smtpAutoEmail');
|
|
if (ae) ae.checked = !!d.auto_email_manual;
|
|
}).catch(function(){});
|
|
}
|
|
|
|
async function stSmtpSave() {
|
|
const st = document.getElementById('st-smtpStatus');
|
|
const rawPw = document.getElementById('st-smtpPw').value;
|
|
const pw = rawPw === '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' ? null : rawPw;
|
|
const body = {
|
|
host: document.getElementById('st-smtpHost').value.trim(),
|
|
port: parseInt(document.getElementById('st-smtpPort').value) || 587,
|
|
// Backend (routes/email.py) reads these exact keys — `username`/`use_tls`,
|
|
// not `user`/`starttls`. Sending the wrong keys leaves username empty so
|
|
// server.login() is skipped and the SMTP server rejects the send.
|
|
username: document.getElementById('st-smtpUser').value.trim(),
|
|
from_addr: document.getElementById('st-smtpFrom').value.trim(),
|
|
recipients: document.getElementById('st-smtpTo').value.split(/[,;]/).map(function(s){return s.trim();}).filter(Boolean),
|
|
use_tls: document.getElementById('st-smtpTls').checked,
|
|
auto_email_manual: !!(document.getElementById('st-smtpAutoEmail') || {}).checked,
|
|
};
|
|
if (pw !== null) body.password = pw;
|
|
st.style.color = 'var(--muted)'; st.textContent = t('m365_smtp_saving','Saving...');
|
|
try {
|
|
const r = await fetch('/api/smtp/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
|
|
const d = await r.json();
|
|
if (d.error) { st.style.color='var(--danger)'; st.textContent=d.error; return; }
|
|
st.style.color='var(--accent)'; st.textContent='\u2714 '+t('m365_smtp_saved','Saved');
|
|
} catch(e){ st.style.color='var(--danger)'; st.textContent=e.message; }
|
|
}
|
|
|
|
async function stSmtpTest() {
|
|
const st = document.getElementById('st-smtpStatus');
|
|
await stSmtpSave();
|
|
if (st) { st.style.color='var(--muted)'; st.textContent=t('m365_smtp_testing','Testing connection\u2026'); }
|
|
try {
|
|
const r = await fetch('/api/smtp/test', {method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({})});
|
|
const d = await r.json();
|
|
if (d.ok) {
|
|
let msg;
|
|
if (d.method === 'graph') {
|
|
msg = t('m365_smtp_test_ok_graph','Test email sent via Microsoft Graph to') + ' ' + (d.recipients||[]).join(', ');
|
|
} else if (d.method === 'smtp') {
|
|
msg = t('m365_smtp_test_ok_smtp','Test email sent via SMTP to') + ' ' + (d.recipients||[]).join(', ');
|
|
if (d.graph_also_failed) msg += ' ' + t('m365_smtp_graph_also_failed','(⚠ Graph also failed — Mail.Send not granted)');
|
|
} else {
|
|
msg = d.message || t('m365_smtp_test_ok','Test email sent');
|
|
}
|
|
if (st) { st.style.color='var(--accent)'; st.textContent='\u2714 ' + msg; }
|
|
} else {
|
|
if (st) { st.style.color='var(--danger)'; st.textContent='\u2717 ' + (d.error || t('m365_smtp_test_fail','Connection failed')); }
|
|
}
|
|
} catch(e) {
|
|
if (st) { st.style.color='var(--danger)'; st.textContent='\u2717 ' + e.message; }
|
|
}
|
|
}
|
|
|
|
async function stSmtpSend() {
|
|
const st = document.getElementById('st-smtpStatus');
|
|
// First save current field values
|
|
await stSmtpSave();
|
|
// Check we have recipients
|
|
const recipStr = document.getElementById('st-smtpTo').value.trim();
|
|
if (!recipStr) {
|
|
if (st) { st.style.color='var(--danger)'; st.textContent=t('m365_smtp_no_recipients','Enter at least one recipient.'); }
|
|
return;
|
|
}
|
|
const recipients = recipStr.split(/[,;]/).map(function(s){return s.trim();}).filter(Boolean);
|
|
const rawPw = document.getElementById('st-smtpPw').value;
|
|
const cfg = {
|
|
host: document.getElementById('st-smtpHost').value.trim(),
|
|
port: parseInt(document.getElementById('st-smtpPort').value) || 587,
|
|
username: document.getElementById('st-smtpUser').value.trim(),
|
|
password: rawPw === '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' ? null : rawPw,
|
|
from_addr: document.getElementById('st-smtpFrom').value.trim(),
|
|
use_tls: document.getElementById('st-smtpTls').checked,
|
|
use_ssl: false,
|
|
};
|
|
if (st) { st.style.color='var(--muted)'; st.textContent=t('m365_smtp_sending','Sending\u2026'); }
|
|
try {
|
|
const r = await fetch('/api/send_report', {method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({recipients, smtp:cfg})});
|
|
const d = await r.json();
|
|
if (d.status === 'sent') {
|
|
if (st) { st.style.color='var(--accent)'; st.textContent=t('m365_smtp_sent','\u2714 Sent'); }
|
|
log(t('m365_smtp_sent','Report sent to') + ' ' + recipients.join(', '), 'ok');
|
|
} else {
|
|
if (st) { st.style.color='var(--danger)'; st.textContent=d.error||'Send failed'; }
|
|
log('Email send failed: '+(d.error||''),'err');
|
|
}
|
|
} catch(e){
|
|
if (st) { st.style.color='var(--danger)'; st.textContent=e.message; }
|
|
}
|
|
}
|
|
|
|
// ── Database tab ──────────────────────────────────────────────────────────────
|
|
|
|
function stLoadDbStats() {
|
|
fetch('/api/db/stats').then(function(r){ return r.json(); }).then(function(d) {
|
|
const el = document.getElementById('st-dbStats');
|
|
if (!el) return;
|
|
if (d.error) { el.textContent = d.error; return; }
|
|
el.innerHTML =
|
|
'<span>' + t('m365_stat_scanned','Scanned items') + '</span>: <strong>' + (d.total_items||0) + '</strong><br>' +
|
|
'<span>' + t('m365_stat_flagged','Flagged items') + '</span>: <strong>' + (d.flagged_items||0) + '</strong><br>' +
|
|
'<span>' + t('m365_db_scans','Scans') + '</span>: <strong>' + (d.total_scans||0) + '</strong>';
|
|
}).catch(function(){ });
|
|
}
|
|
|
|
function stResetDB() {
|
|
if (!confirm(t('m365_db_reset_confirm','Reset database? All scan results will be deleted.'))) return;
|
|
requirePin(t('m365_settings_enter_pin_reset','Enter admin PIN to reset the database.'), function(pin) {
|
|
fetch('/api/db/reset', {method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({confirm:'yes', pin:pin})
|
|
}).then(function(r){ return r.json(); }).then(function(d) {
|
|
if (d.error === 'incorrect_pin') { log(t('m365_settings_pin_wrong','Incorrect PIN \u2014 reset cancelled.'), 'err'); return; }
|
|
if (d.error) { log('Reset failed: '+d.error, 'err'); return; }
|
|
stLoadDbStats();
|
|
log(t('m365_db_reset_done','Database reset'));
|
|
}).catch(function(e){ log('Reset failed: '+e,'err'); });
|
|
});
|
|
}
|
|
|
|
// Redirect old openSmtpModal to Settings email tab
|
|
function openSmtpModal(send) {
|
|
openSettings('email');
|
|
}
|
|
|
|
// ── Window exports (HTML handlers + cross-module calls) ─────────────────────
|
|
window.schedLoad = schedLoad;
|
|
window.schedRenderJobs = schedRenderJobs;
|
|
window.schedToggleEnabled = schedToggleEnabled;
|
|
window.schedAddJob = schedAddJob;
|
|
window.schedEditJob = schedEditJob;
|
|
window.schedCancelEdit = schedCancelEdit;
|
|
window.schedSaveJob = schedSaveJob;
|
|
window.schedDeleteJob = schedDeleteJob;
|
|
window.schedRunJob = schedRunJob;
|
|
window.schedToggleFreqRows = schedToggleFreqRows;
|
|
window.schedToggleReportOnly = schedToggleReportOnly;
|
|
window.schedPopulateProfiles = schedPopulateProfiles;
|
|
window.schedLoadHistory = schedLoadHistory;
|
|
window.schedUpdateSidebarIndicator = schedUpdateSidebarIndicator;
|
|
window.stPopulateGeneral = stPopulateGeneral;
|
|
window.stLoadSmtp = stLoadSmtp;
|
|
window.stSmtpSave = stSmtpSave;
|
|
window.stSmtpTest = stSmtpTest;
|
|
window.stSmtpSend = stSmtpSend;
|
|
window.stLoadDbStats = stLoadDbStats;
|
|
window.stResetDB = stResetDB;
|
|
window.openSmtpModal = openSmtpModal;
|
|
window._schedJobs = _schedJobs;
|