import { S } from './state.js'; // ── Unified Source Management (#17) ────────────────────────────────────────── function openSourcesMgmt(tab) { document.getElementById('srcMgmtBackdrop').classList.add('open'); switchSrcTab(tab || 'm365'); smRefreshStatus(); smGoogleRefreshStatus(); srcFileRenderList(); } function closeSourcesMgmt() { document.getElementById('srcMgmtBackdrop').classList.remove('open'); } function switchSrcTab(tab) { ['m365','google','files'].forEach(function(t) { document.getElementById('srcPane' + t.charAt(0).toUpperCase() + t.slice(1)) .classList.toggle('active', t === tab); const btn = document.getElementById('srcTab' + t.charAt(0).toUpperCase() + t.slice(1)); if (btn) btn.classList.toggle('active', t === tab); }); // Capitalise pane ids correctly: srcPaneM365, srcPaneGoogle, srcPaneFiles const paneMap = {m365:'M365', google:'Google', files:'Files'}; ['m365','google','files'].forEach(function(t) { const pane = document.getElementById('srcPane' + paneMap[t]); if (pane) pane.classList.toggle('active', t === tab); const btn = document.getElementById('srcTab' + paneMap[t]); if (btn) btn.classList.toggle('active', t === tab); }); } // ── M365 pane ───────────────────────────────────────────────────────────────── function smRefreshStatus() { const dot = document.getElementById('srcM365StatusDot'); const label = document.getElementById('srcM365StatusLabel'); const sub = document.getElementById('srcM365StatusSub'); const disc = document.getElementById('smDisconnectBtn'); const st = document.getElementById('smConnStatus'); if (!dot) return; // Load saved credentials and auth status from the correct endpoints fetch('/api/auth/status').then(function(r){ return r.json(); }).then(function(d) { // Pre-fill credential fields const cidEl = document.getElementById('smClientId'); const tidEl = document.getElementById('smTenantId'); const secEl = document.getElementById('smClientSecret'); if (cidEl && d.client_id) cidEl.value = d.client_id; if (tidEl && d.tenant_id) tidEl.value = d.tenant_id; if (secEl && d.client_secret) secEl.value = d.client_secret.length > 4 ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''; if (d.authenticated) { dot.className = 'srcmgmt-status green'; const who = d.display_name || d.email || ''; const mode = d.app_mode ? t('m365_mode_app_short','App mode') : t('m365_mode_delegated_short','Delegated'); label.textContent = who || t('m365_srcmgmt_connected','Connected'); sub.textContent = mode + (d.email && d.display_name ? ' \u00b7 ' + d.email : ''); if (disc) disc.style.display = ''; if (st) st.textContent = ''; } else { dot.className = 'srcmgmt-status grey'; label.textContent = t('m365_srcmgmt_not_connected','Not connected'); sub.textContent = ''; if (disc) disc.style.display = 'none'; if (st) st.textContent = ''; } }).catch(function(){ if (dot) dot.className = 'srcmgmt-status grey'; }); } async function smConnect() { const cid = document.getElementById('smClientId').value.trim(); const tid = document.getElementById('smTenantId').value.trim(); const rawSec = document.getElementById('smClientSecret').value; // If field shows placeholder dots and user hasn't changed it, use saved secret (send empty to keep it) const sec = (rawSec === '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022') ? '' : rawSec.trim(); const st = document.getElementById('smConnStatus'); if (!cid || !tid) { st.style.color='var(--danger)'; st.textContent=t('m365_err_creds_required','Client ID and Tenant ID required'); return; } st.style.color='var(--muted)'; st.textContent=t('m365_connecting','Connecting...'); // Persist credentials await fetch('/api/auth/config', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({client_id:cid, tenant_id:tid, client_secret:sec}) }); // Start auth — same as the auth screen flow try { const r = await fetch('/api/auth/start', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({client_id:cid, tenant_id:tid, client_secret:sec}) }); const d = await r.json(); if (d.error) { st.style.color='var(--danger)'; st.textContent=d.error; return; } if (d.mode === 'application') { // App mode — no device code needed st.style.color='var(--accent)'; st.textContent='\u2714 '+t('m365_connected','Connected'); closeSourcesMgmt(); setTimeout(onAuthenticated, 400); } else { // Delegated — show device code flow, close modal closeSourcesMgmt(); document.getElementById('clientId').value = cid; document.getElementById('tenantId').value = tid; document.getElementById('clientSecret').value = sec; document.getElementById('configForm').style.display = 'none'; document.getElementById('authScreen').style.display = 'flex'; document.getElementById('deviceCodeBackdrop').classList.add('open'); document.getElementById('deviceCode').textContent = d.user_code || '\u2014'; pollAuth(); } } catch(e) { st.style.color='var(--danger)'; st.textContent=e.message; } } function smDisconnect() { if (!confirm(t('m365_signout_confirm','Disconnect and clear credentials?'))) return; fetch('/api/auth/signout', {method:'POST'}).then(function(){ closeSourcesMgmt(); signOut(); }); } // ── Google Workspace pane ───────────────────────────────────────────────────── // Parsed key dict held in memory while the pane is open — cleared on disconnect var _googleKeyDict = null; var _googleAuthMode = 'workspace'; function smGoogleSetMode(mode) { _googleAuthMode = mode; var saSection = document.getElementById('smGoogleSaSection'); var personalSection = document.getElementById('smGooglePersonalSection'); var wsSetup = document.getElementById('smGoogleWorkspaceSetup'); var btnWs = document.getElementById('smGoogleModeWorkspace'); var btnPl = document.getElementById('smGoogleModePersonal'); var isPersonal = (mode === 'personal'); if (saSection) saSection.style.display = isPersonal ? 'none' : ''; if (personalSection) personalSection.style.display = isPersonal ? '' : 'none'; if (wsSetup) wsSetup.style.display = isPersonal ? 'none' : ''; if (btnWs) { btnWs.style.background = isPersonal ? 'var(--surface)' : 'var(--accent)'; btnWs.style.color = isPersonal ? 'var(--text)' : '#fff'; } if (btnPl) { btnPl.style.background = isPersonal ? 'var(--accent)' : 'var(--surface)'; btnPl.style.color = isPersonal ? '#fff' : 'var(--text)'; } } function smGoogleRefreshStatus() { var wsPromise = fetch('/api/google/auth/status').then(function(r){ return r.json(); }).catch(function(){ return {}; }); var personalPromise = fetch('/api/google/personal/status').then(function(r){ return r.json(); }).catch(function(){ return {connected: false}; }); Promise.all([wsPromise, personalPromise]).then(function(results) { var ws = results[0]; var personal = results[1]; var dot = document.getElementById('srcGoogleStatusDot'); var label = document.getElementById('srcGoogleStatusLabel'); var sub = document.getElementById('srcGoogleStatusSub'); var disc = document.getElementById('smGoogleDisconnectBtn'); var srcs = document.getElementById('smGoogleSourcesGroup'); var signOutBtn = document.getElementById('smGooglePersonalSignOutBtn'); var signInBtn = document.getElementById('smGooglePersonalSignInBtn'); if (!dot) return; if (ws.libs_ok === false) { dot.className = 'srcmgmt-status amber'; label.textContent = t('m365_google_libs_missing', 'Libraries not installed'); sub.textContent = 'pip install google-auth google-auth-httplib2 google-api-python-client'; if (disc) disc.style.display = 'none'; if (srcs) srcs.style.display = 'none'; return; } if (personal.connected) { smGoogleSetMode('personal'); window._googleConnected = true; dot.className = 'srcmgmt-status green'; label.textContent = personal.email || personal.displayName || t('m365_srcmgmt_connected', 'Connected'); sub.textContent = t('m365_google_mode_personal', 'Personal account'); if (disc) disc.style.display = 'none'; if (srcs) srcs.style.display = ''; if (signOutBtn) signOutBtn.style.display = ''; if (signInBtn) signInBtn.style.display = 'none'; } else if (ws.connected) { smGoogleSetMode('workspace'); window._googleConnected = true; dot.className = 'srcmgmt-status green'; label.textContent = ws.sa_email || t('m365_srcmgmt_connected', 'Connected'); sub.textContent = (ws.project_id ? ws.project_id + ' · ' : '') + (ws.admin_email || ''); if (disc) disc.style.display = ''; if (srcs) srcs.style.display = ''; if (signOutBtn) signOutBtn.style.display = 'none'; if (signInBtn) signInBtn.style.display = ''; var ae = document.getElementById('smGoogleAdminEmail'); if (ae && ws.admin_email && !ae.value) ae.value = ws.admin_email; var gm = document.getElementById('smGoogleSrcGmail'); var gd = document.getElementById('smGoogleSrcDrive'); if (gm && ws.src_gmail !== undefined) gm.checked = !!ws.src_gmail; if (gd && ws.src_drive !== undefined) gd.checked = !!ws.src_drive; } else { window._googleConnected = false; dot.className = 'srcmgmt-status grey'; label.textContent = t('m365_srcmgmt_not_connected', 'Not connected'); sub.textContent = ws.error || personal.error || ''; if (disc) disc.style.display = 'none'; if (srcs) srcs.style.display = 'none'; if (signOutBtn) signOutBtn.style.display = 'none'; if (signInBtn) signInBtn.style.display = ''; } renderSourcesPanel(); // If the profile editor is open and its source panel has no Google checkboxes yet, // re-render it now that connection status is known. if (document.getElementById('pmgmtEditor')?.classList.contains('open') && !document.querySelector('#peSourcesPanel input[data-source-type="google"]')) { var _peCheckedIds = Array.from(document.querySelectorAll('#peSourcesPanel input[type=checkbox]')) .filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.dataset.sourceId; }); var _peProfile = window._pmgmtEditId ? (S._profiles.find(function(p) { return p.id === window._pmgmtEditId; }) || window._pmgmtNewDraft) : window._pmgmtNewDraft; if (_peProfile) { var _peSavedIds = (_peProfile.sources||[]).concat(_peProfile.google_sources||[]).concat(_peProfile.file_sources||[]); _renderEditorSources(_peCheckedIds.concat(_peSavedIds)); } } if (window._googleConnected) { _mergeGoogleUsers(); } else { // Remove standalone Google users; reset merged 'both' users back to M365 S._allUsers = S._allUsers.filter(function(u){ return (u.platform||'m365') !== 'google'; }); S._allUsers.forEach(function(u) { if (u.platform === 'both') { u.platform = 'm365'; delete u.googleEmail; } }); renderAccountList(); } }).catch(function() { var dot = document.getElementById('srcGoogleStatusDot'); if (dot) dot.className = 'srcmgmt-status grey'; }); } // Wire up file input to read + validate JSON immediately (function() { document.addEventListener('DOMContentLoaded', function() { var fi = document.getElementById('smGoogleKeyFile'); if (!fi) return; fi.addEventListener('change', function() { var f = fi.files && fi.files[0]; if (!f) { _googleKeyDict = null; return; } var reader = new FileReader(); reader.onload = function(e) { try { _googleKeyDict = JSON.parse(e.target.result); var nameEl = document.getElementById('smGoogleKeyName'); if (nameEl) nameEl.textContent = _googleKeyDict.client_email ? '✔ ' + _googleKeyDict.client_email.split('@')[0] : '✔ loaded'; } catch(err) { _googleKeyDict = null; var st = document.getElementById('smGoogleConnStatus'); if (st) { st.style.color='var(--danger)'; st.textContent = t('m365_google_invalid_json','Invalid JSON file'); } } }; reader.readAsText(f); }); }); })(); async function smGoogleConnect() { var st = document.getElementById('smGoogleConnStatus'); var adminEmail = (document.getElementById('smGoogleAdminEmail') || {}).value || ''; if (!_googleKeyDict) { if (st) { st.style.color='var(--danger)'; st.textContent = t('m365_google_key_required','Select a service account JSON key file'); } return; } if (st) { st.style.color='var(--muted)'; st.textContent = t('m365_connecting','Connecting...'); } try { var r = await fetch('/api/google/auth/connect', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({key_json: _googleKeyDict, admin_email: adminEmail}) }); var d = await r.json(); if (d.error) { if (st) { st.style.color='var(--danger)'; st.textContent = d.error; } return; } if (st) { st.style.color='var(--accent)'; st.textContent = '✔ ' + t('m365_connected','Connected'); } smGoogleRefreshStatus(); } catch(e) { if (st) { st.style.color='var(--danger)'; st.textContent = e.message; } } } function smGoogleDisconnect() { if (!confirm(t('m365_signout_confirm','Disconnect and clear credentials?'))) return; fetch('/api/google/auth/disconnect', {method:'POST'}).then(function() { _googleKeyDict = null; var fi = document.getElementById('smGoogleKeyFile'); if (fi) fi.value = ''; var nameEl = document.getElementById('smGoogleKeyName'); if (nameEl) nameEl.textContent = ''; var st = document.getElementById('smGoogleConnStatus'); if (st) st.textContent = ''; smGoogleRefreshStatus(); }); } async function smGooglePersonalStart() { var clientId = (document.getElementById('smGooglePersonalClientId') || {}).value || ''; var clientSecret = (document.getElementById('smGooglePersonalClientSecret') || {}).value || ''; var st = document.getElementById('smGooglePersonalConnStatus'); if (!clientId || !clientSecret) { if (st) { st.style.color = 'var(--danger)'; st.textContent = t('m365_google_personal_creds_required', 'Client ID and secret required'); } return; } if (st) { st.style.color = 'var(--muted)'; st.textContent = t('m365_connecting', 'Connecting...'); } try { var r = await fetch('/api/google/personal/start', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({client_id: clientId, client_secret: clientSecret}) }); var d = await r.json(); if (d.error) { if (st) { st.style.color = 'var(--danger)'; st.textContent = d.error; } return; } var box = document.getElementById('smGoogleDeviceBox'); var codeEl = document.getElementById('smGoogleDeviceCode'); var urlEl = document.getElementById('smGoogleDeviceUrl'); var pollSt = document.getElementById('smGooglePollStatus'); if (box) box.style.display = ''; if (codeEl) codeEl.textContent = d.user_code || '—'; if (urlEl) { urlEl.href = d.verification_url || 'https://google.com/device'; urlEl.textContent = (d.verification_url || 'https://google.com/device').replace('https://', ''); } if (pollSt) { pollSt.style.color = 'var(--muted)'; pollSt.textContent = '⏳ ' + t('m365_auth_waiting', 'Waiting for sign-in…'); } if (st) st.textContent = ''; smGooglePersonalPoll(); } catch(e) { if (st) { st.style.color = 'var(--danger)'; st.textContent = e.message; } } } function smGooglePersonalPoll() { fetch('/api/google/personal/poll', {method: 'POST'}) .then(function(r) { return r.json(); }) .then(function(d) { var pollSt = document.getElementById('smGooglePollStatus'); if (d.status === 'pending') { setTimeout(smGooglePersonalPoll, 3000); } else if (d.status === 'ok') { if (pollSt) { pollSt.style.color = 'var(--success)'; pollSt.textContent = '✓ ' + t('m365_connected', 'Connected'); } setTimeout(function() { var box = document.getElementById('smGoogleDeviceBox'); if (box) box.style.display = 'none'; smGoogleRefreshStatus(); }, 1000); } else { if (pollSt) { pollSt.style.color = 'var(--danger)'; pollSt.textContent = '✗ ' + (d.error || 'Sign-in failed'); } setTimeout(function() { var box = document.getElementById('smGoogleDeviceBox'); if (box) box.style.display = 'none'; }, 3000); } }) .catch(function() { setTimeout(smGooglePersonalPoll, 5000); }); } function smGooglePersonalSignOut() { if (!confirm(t('m365_signout_confirm', 'Disconnect and clear credentials?'))) return; fetch('/api/google/personal/signout', {method: 'POST'}).then(function() { smGoogleRefreshStatus(); }); } // Returns {sources, options} reflecting current Google pane state — used by scan launcher function getGoogleScanOptions() { var sources = []; if (document.getElementById('smGoogleSrcGmail') && document.getElementById('smGoogleSrcGmail').checked) sources.push('gmail'); if (document.getElementById('smGoogleSrcDrive') && document.getElementById('smGoogleSrcDrive').checked) sources.push('gdrive'); return {sources: sources, options: {}}; } // ── File sources pane ───────────────────────────────────────────────────────── function srcFileRenderList() { const list = document.getElementById('srcFileList'); if (!list) return; if (!S._fileSources.length) { list.innerHTML = '