// ── 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 = '
' + t('m365_sched_no_jobs','No scheduled scans yet.') + '
'; 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 ? '' + t('m365_sched_report_only','Report only') + '' : ''; return '
' + '' + '
' + '
'+sname+roBadge+'
' + '
'+desc+'
' + '
' + '' + '' + '' + '
'; }).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 = '' + t('m365_sched_no_runs','No scheduled runs yet') + ''; 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 ? '' + _esc(r.job_name) + ' - ' : ''; html += icon + ' ' + jname + ts + ' - ' + (r.flagged||0) + ' flagged'; if (r.emailed) html += ' \u2709'; if (r.error) html += ' ' + _esc(r.error.substring(0,60)) + ''; html += '
'; }); 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 = '' + t('m365_stat_scanned','Scanned items') + ': ' + (d.total_items||0) + '
' + '' + t('m365_stat_flagged','Flagged items') + ': ' + (d.flagged_items||0) + '
' + '' + t('m365_db_scans','Scans') + ': ' + (d.total_scans||0) + ''; }).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;