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 _srcIcon(s) { if (s.source_type === 'sftp') return '\uD83D\uDD12'; const isSmb = s.path && (s.path.startsWith('//') || s.path.startsWith('\\\\')); return isSmb ? '\uD83C\uDF10' : '\uD83D\uDCC1'; } function _srcSubtitle(s) { if (s.source_type === 'sftp') { return _esc((s.sftp_user||'')+'@'+(s.sftp_host||'')+(s.path||'/')); } return _esc(s.path||'')+(s.smb_user?' \u00b7 \uD83D\uDC64 '+_esc(s.smb_user):''); } function srcFileRenderList() { const list = document.getElementById('srcFileList'); if (!list) return; if (!S._fileSources.length) { list.innerHTML = '
'+t('m365_file_sources_empty','No file sources yet.')+'
'; return; } list.innerHTML = S._fileSources.map(function(s) { const icon = _srcIcon(s); const sid = _esc(s.id||''); const slabel = _esc(s.label||s.path||''); return '
' +'
' +''+icon+' '+slabel+'' +'
' +'' +'' +'' +'
' +'
'+_srcSubtitle(s)+'
' +'
'; }).join(''); } function srcFileTypeSelect(type) { document.getElementById('srcFileSourceType').value = type; var pathRow = document.getElementById('srcFilePathRow'); var smbFields = document.getElementById('srcFileSmbFields'); var sftpFields= document.getElementById('srcFileSftpFields'); if (pathRow) pathRow.style.display = type === 'sftp' ? 'none' : ''; if (smbFields) smbFields.style.display = type === 'smb' ? 'flex' : 'none'; if (sftpFields)sftpFields.style.display= type === 'sftp' ? 'flex' : 'none'; ['srcTypeLocal','srcTypeSmb','srcTypeSftp'].forEach(function(id) { var btn = document.getElementById(id); if (!btn) return; var active = (id === 'srcType' + type.charAt(0).toUpperCase() + type.slice(1)); btn.style.background = active ? 'var(--accent)' : 'none'; btn.style.color = active ? '#fff' : 'var(--muted)'; }); } function srcFileAutoNameSftp() { var labelEl = document.getElementById('srcFileLabel'); if (labelEl && labelEl._userEdited) return; var host = (document.getElementById('srcFileSftpHost')||{}).value || ''; if (labelEl && host) labelEl.value = host; } function srcFileSftpAuthSelect(authType) { document.getElementById('srcFileSftpAuth').value = authType; var pwFields = document.getElementById('srcSftpPwFields'); var keyFields = document.getElementById('srcSftpKeyFields'); var btnPw = document.getElementById('srcSftpAuthPw'); var btnKey = document.getElementById('srcSftpAuthKey'); if (pwFields) pwFields.style.display = authType === 'password' ? '' : 'none'; if (keyFields) keyFields.style.display = authType === 'key' ? 'flex' : 'none'; if (btnPw) { btnPw.style.background = authType==='password'?'var(--accent)':'none'; btnPw.style.color = authType==='password'?'#fff':'var(--muted)'; } if (btnKey) { btnKey.style.background = authType==='key'?'var(--accent)':'none'; btnKey.style.color = authType==='key'?'#fff':'var(--muted)'; } } function srcFileDetectSmb() { const p = document.getElementById('srcFilePath').value; const isSmb = p.startsWith('//') || p.startsWith('\\\\'); document.getElementById('srcFileSmbFields').style.display = isSmb ? 'flex' : 'none'; if (isSmb && !document.getElementById('srcFileSmbHost').value) { document.getElementById('srcFileSmbHost').value = p.replace(/^[\/\\]+/,'').split(/[\/\\]/)[0]; } } function srcFileAutoName() { const labelEl = document.getElementById('srcFileLabel'); if (labelEl._userEdited) return; const p = document.getElementById('srcFilePath').value.trim(); if (!p) { labelEl.value=''; return; } const parts = p.replace(/[\/\\]+$/,'').split(/[\/\\]/); if ((p.startsWith('//')||p.startsWith('\\\\')) && parts.filter(function(x){return x;}).length>=2) { const segs = parts.filter(function(x){return x;}); labelEl.value = segs[0]+(segs[1]?' / '+segs[1]:''); } else { labelEl.value = parts[parts.length-1]||p; } } async function srcFileAdd() { const label = document.getElementById('srcFileLabel').value.trim(); const sourceType = (document.getElementById('srcFileSourceType')||{}).value || 'local'; const stat = document.getElementById('srcFileStatus'); const editIdEl = document.getElementById('srcFileEditId'); const existingId = editIdEl ? editIdEl.value : ''; if (!label) { stat.style.color='var(--danger)'; stat.textContent=t('m365_fsrc_name_required','Name is required.'); document.getElementById('srcFileLabel').focus(); return; } stat.style.color='var(--muted)'; stat.textContent=t('m365_fsrc_saving','Saving...'); var body = {label, source_type: sourceType}; if (existingId) body.id = existingId; if (sourceType === 'sftp') { const sftpHost = document.getElementById('srcFileSftpHost').value.trim(); const sftpUser = document.getElementById('srcFileSftpUser').value.trim(); const sftpPath = document.getElementById('srcFileSftpPath').value.trim() || '/'; const sftpPort = parseInt(document.getElementById('srcFileSftpPort').value) || 22; const sftpAuth = document.getElementById('srcFileSftpAuth').value || 'password'; if (!sftpHost) { stat.style.color='var(--danger)'; stat.textContent=t('m365_fsrc_sftp_host_required','SFTP host is required.'); return; } if (!sftpUser) { stat.style.color='var(--danger)'; stat.textContent=t('m365_fsrc_sftp_user_required','SFTP username is required.'); return; } Object.assign(body, {sftp_host:sftpHost, sftp_port:sftpPort, sftp_user:sftpUser, sftp_auth:sftpAuth, path:sftpPath}); if (sftpAuth === 'password') { const sftpPw = document.getElementById('srcFileSftpPw').value; if (sftpPw) { try { await fetch('/api/file_sources/store_creds',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source_type:'sftp',sftp_host:sftpHost,sftp_user:sftpUser,password:sftpPw})}); } catch(e){} } } else { // Upload key file if one is selected const keyFileEl = document.getElementById('srcFileSftpKeyFile'); const keyStatusEl = document.getElementById('srcFileSftpKeyStatus'); const keyPathEl = document.getElementById('srcFileSftpKeyPath'); if (keyFileEl && keyFileEl.files.length && !keyPathEl.value) { try { const fd = new FormData(); fd.append('key_file', keyFileEl.files[0]); const kr = await fetch('/api/file_sources/upload_key',{method:'POST',body:fd}); const kd = await kr.json(); if (kd.error) { stat.style.color='var(--danger)'; stat.textContent=kd.error; return; } keyPathEl.value = kd.key_path; if (keyStatusEl) keyStatusEl.textContent = t('m365_fsrc_sftp_key_uploaded','Key uploaded'); } catch(e){ stat.style.color='var(--danger)'; stat.textContent=e.message; return; } } body.sftp_key_path = keyPathEl ? keyPathEl.value : ''; const passphrase = (document.getElementById('srcFileSftpPassphrase')||{}).value || ''; if (passphrase) { const passphraseKey = sftpHost+':'+sftpUser+':passphrase'; try { await fetch('/api/file_sources/store_creds',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source_type:'sftp',sftp_host:sftpHost,sftp_user:sftpUser,password:passphrase,keychain_key:passphraseKey})}); } catch(e){} body.keychain_key = passphraseKey; } } } else { const path = document.getElementById('srcFilePath').value.trim(); const smbHost = document.getElementById('srcFileSmbHost').value.trim(); const smbUser = document.getElementById('srcFileSmbUser').value.trim(); const smbPw = document.getElementById('srcFileSmbPw').value; if (!path) { stat.style.color='var(--danger)'; stat.textContent=t('m365_fsrc_path_required','Path is required.'); return; } Object.assign(body, {path, smb_host:smbHost, smb_user:smbUser}); if (smbPw && smbUser) { try { await fetch('/api/file_sources/store_creds',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source_type:'smb',smb_host:smbHost,smb_user:smbUser,password:smbPw})}); } catch(e){} } } try { const r = await fetch('/api/file_sources/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); const d = await r.json(); if (d.error) { stat.style.color='var(--danger)'; stat.textContent=d.error; return; } // Reset form ['srcFileLabel','srcFilePath','srcFileSmbHost','srcFileSmbUser','srcFileSmbPw', 'srcFileSftpHost','srcFileSftpUser','srcFileSftpPw','srcFileSftpPassphrase','srcFileSftpKeyPath'].forEach(function(id){const el=document.getElementById(id);if(el){el.value='';if(el._userEdited!==undefined)el._userEdited=false;}}); var portEl = document.getElementById('srcFileSftpPort'); if(portEl) portEl.value='22'; if (editIdEl) editIdEl.value=''; const addBtn=document.getElementById('srcFileAddBtn'); if(addBtn) addBtn.textContent=t('m365_fsrc_add_btn','Add'); srcFileTypeSelect('local'); stat.style.color='var(--accent)'; stat.textContent='\u2714 '+t('m365_fsrc_saved','Source saved'); await _loadFileSources(); srcFileRenderList(); log(t('m365_fsrc_saved','Source saved')+': '+label); } catch(e){ stat.style.color='var(--danger)'; stat.textContent=e.message; } } function srcFileEdit(id) { const s = S._fileSources.find(function(x){return x.id===id;}); if (!s) return; const labelEl = document.getElementById('srcFileLabel'); const editId = document.getElementById('srcFileEditId'); if (labelEl) { labelEl.value = s.label||''; labelEl._userEdited = true; } if (editId) editId.value = id; var sourceType = s.source_type || (((s.path||'').startsWith('//')||(s.path||'').startsWith('\\\\')) ? 'smb' : 'local'); srcFileTypeSelect(sourceType); if (sourceType === 'sftp') { var hostEl = document.getElementById('srcFileSftpHost'); if(hostEl) hostEl.value = s.sftp_host||''; var portEl = document.getElementById('srcFileSftpPort'); if(portEl) portEl.value = s.sftp_port||22; var userEl = document.getElementById('srcFileSftpUser'); if(userEl) userEl.value = s.sftp_user||''; var pathEl = document.getElementById('srcFileSftpPath'); if(pathEl) pathEl.value = s.path||'/'; var authEl = document.getElementById('srcFileSftpAuth'); if(authEl) authEl.value = s.sftp_auth||'password'; srcFileSftpAuthSelect(s.sftp_auth||'password'); if (s.sftp_key_path) { var kp = document.getElementById('srcFileSftpKeyPath'); if(kp) kp.value=s.sftp_key_path; } } else { var pathEl2 = document.getElementById('srcFilePath'); if(pathEl2) pathEl2.value = s.path||''; var smbHostEl = document.getElementById('srcFileSmbHost'); if(smbHostEl) smbHostEl.value = s.smb_host||''; var smbUserEl = document.getElementById('srcFileSmbUser'); if(smbUserEl) smbUserEl.value = s.smb_user||''; var smbPwEl = document.getElementById('srcFileSmbPw'); if(smbPwEl) smbPwEl.value = s.smb_user ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''; } const btn = document.getElementById('srcFileAddBtn'); if (btn) btn.textContent = t('m365_fsrc_save_changes','Save changes'); const stat = document.getElementById('srcFileStatus'); if (stat) { stat.style.color='var(--muted)'; stat.textContent='Editing: '+_esc(s.label||s.path||''); } } async function srcFileDelete(id, label) { if (!confirm(t('m365_profile_delete_confirm','Delete')+' "'+label+'"?')) return; await fetch('/api/file_sources/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})}); await _loadFileSources(); srcFileRenderList(); } async function srcFileScan(id) { const source = S._fileSources.find(function(s){ return s.id===id; }); if (!source) return; closeSourcesMgmt(); log(t('m365_fsrc_scan_start','Starting file scan')+': '+(source.label||source.path)); try { const r = await fetch('/api/file_scan/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(source)}); const d = await r.json(); if (d.error) log('File scan error: '+d.error,'err'); } catch(e){ log('File scan error: '+e.message,'err'); } } // Redirect old openFileSourcesModal() to the new unified modal function openFileSourcesModal() { openSourcesMgmt('files'); } function closeFileSourcesModal() { closeSourcesMgmt(); } // ── File Sources (#8) ───────────────────────────────────────────────────────── async function _loadFileSources() { try { const r = await fetch('/api/file_sources'); const d = await r.json(); S._fileSources = d.sources || []; _renderFileSources(d.smb_available); renderSourcesPanel(); // Re-apply any pending profile source selection (file sources render after load) if (S._pendingProfileSources.length) { document.querySelectorAll('#sourcesPanel input[data-source-type="file"]').forEach(function(cb) { cb.checked = S._pendingProfileSources.includes(cb.dataset.sourceId); }); S._pendingProfileSources = []; } // If the profile editor is open and has no file checkboxes yet, re-render it now. if (document.getElementById('pmgmtEditor')?.classList.contains('open') && !document.querySelector('#peSourcesPanel input[data-source-type="file"]') && S._fileSources.length > 0) { 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)); } } } catch(e) { const s = document.getElementById('fsrcStatus'); if (s) { s.style.color = 'var(--danger)'; s.textContent = 'Error: ' + e.message; } } } function _renderFileSources() { const list = document.getElementById('fsrcList'); if (!list) return; if (!S._fileSources.length) { list.innerHTML = '
' + t('m365_file_sources_empty','No file sources yet.') + '
'; return; } list.innerHTML = S._fileSources.map(function(s) { const icon = _srcIcon(s); const sid = _esc(s.id || ''); const slabel = _esc(s.label || s.path || ''); return '
' + '
' + '' + icon + ' ' + slabel + '' + '
' + '' + '' + '
' + '
' + _srcSubtitle(s) + '
' + '
'; }).join(''); } function fsrcDetectSmb() { const p = document.getElementById('fsrcPath').value; const isSmb = p.startsWith('//') || p.startsWith('\\\\'); document.getElementById('fsrcSmbFields').style.display = isSmb ? 'flex' : 'none'; if (isSmb && !document.getElementById('fsrcSmbHost').value) { document.getElementById('fsrcSmbHost').value = p.replace(/^[\/\\]+/,'').split(/[\/\\]/)[0]; } } function fsrcAutoName() { // Suggest a name from the path only if the user hasn't typed one yet const labelEl = document.getElementById('fsrcLabel'); if (labelEl._userEdited) return; const p = document.getElementById('fsrcPath').value.trim(); if (!p) { labelEl.value = ''; return; } // Extract last meaningful path segment const parts = p.replace(/[/\\]+$/, '').split(/[/\\]/); const last = parts[parts.length - 1] || parts[parts.length - 2] || p; // For SMB paths like //nas/share use "nas / share" if ((p.startsWith('//') || p.startsWith('\\\\')) && parts.length >= 3) { const host = parts.find(function(x){ return x.length > 0; }) || ''; const share = parts.filter(function(x){ return x.length > 0; })[1] || ''; labelEl.value = share ? host + ' / ' + share : host; } else { labelEl.value = last; } } document.addEventListener('DOMContentLoaded', function() { const labelEl = document.getElementById('fsrcLabel'); if (labelEl) { labelEl.addEventListener('input', function() { labelEl._userEdited = !!labelEl.value; }); } const srcFileLabelEl = document.getElementById('srcFileLabel'); if (srcFileLabelEl) { srcFileLabelEl.addEventListener('input', function() { srcFileLabelEl._userEdited = !!srcFileLabelEl.value; }); } }); async function fsrcAddSource() { const path = document.getElementById('fsrcPath').value.trim(); const label = document.getElementById('fsrcLabel').value.trim() || path; const smbHost = document.getElementById('fsrcSmbHost').value.trim(); const smbUser = document.getElementById('fsrcSmbUser').value.trim(); const smbPw = document.getElementById('fsrcSmbPw').value; const stat = document.getElementById('fsrcStatus'); if (!label) { stat.style.color='var(--danger)'; stat.textContent=t('m365_fsrc_name_required','Name is required.'); document.getElementById('fsrcLabel').focus(); return; } if (!path) { stat.style.color='var(--danger)'; stat.textContent=t('m365_fsrc_path_required','Path is required.'); return; } stat.style.color='var(--muted)'; stat.textContent=t('m365_fsrc_saving','Saving...'); if (smbPw && smbUser) { try { await fetch('/api/file_sources/store_creds',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({smb_host:smbHost,smb_user:smbUser,password:smbPw})}); } catch(e){} } try { const r = await fetch('/api/file_sources/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({label,path,smb_host:smbHost,smb_user:smbUser})}); const d = await r.json(); if (d.error) { stat.style.color='var(--danger)'; stat.textContent=d.error; return; } ['fsrcLabel','fsrcPath','fsrcSmbHost','fsrcSmbUser','fsrcSmbPw'].forEach(function(id){const el=document.getElementById(id);if(el){el.value='';el._userEdited=false;}}); document.getElementById('fsrcSmbFields').style.display='none'; stat.style.color='var(--accent)'; stat.textContent='\u2714 '+t('m365_fsrc_saved','Source saved'); await _loadFileSources(); log(t('m365_fsrc_saved','Source saved')+': '+label); } catch(e){ stat.style.color='var(--danger)'; stat.textContent=e.message; } } async function fsrcDelete(id, label) { if (!confirm(t('m365_profile_delete_confirm','Delete')+' "'+label+'"?')) return; try { await fetch('/api/file_sources/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})}); await _loadFileSources(); log(t('m365_profile_deleted','Deleted')+': '+label); } catch(e){ const s=document.getElementById('fsrcStatus'); if(s) s.textContent=e.message; } } async function fsrcScan(id) { const source = S._fileSources.find(function(s){ return s.id===id; }); if (!source) return; closeFileSourcesModal(); log(t('m365_fsrc_scan_start','Starting file scan')+': '+(source.label||source.path)); try { const r = await fetch('/api/file_scan/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(source)}); const d = await r.json(); if (d.error) log('File scan error: '+d.error,'err'); } catch(e){ log('File scan error: '+e.message,'err'); } } // ── Window exports (HTML handlers + cross-module calls) ───────────────────── window.openSourcesMgmt = openSourcesMgmt; window.closeSourcesMgmt = closeSourcesMgmt; window.switchSrcTab = switchSrcTab; window.smRefreshStatus = smRefreshStatus; window.smConnect = smConnect; window.smDisconnect = smDisconnect; window.smGoogleSetMode = smGoogleSetMode; window.smGoogleRefreshStatus = smGoogleRefreshStatus; window.smGoogleConnect = smGoogleConnect; window.smGoogleDisconnect = smGoogleDisconnect; window.smGooglePersonalStart = smGooglePersonalStart; window.smGooglePersonalPoll = smGooglePersonalPoll; window.smGooglePersonalSignOut = smGooglePersonalSignOut; window.getGoogleScanOptions = getGoogleScanOptions; window.srcFileRenderList = srcFileRenderList; window.srcFileDetectSmb = srcFileDetectSmb; window.srcFileAutoName = srcFileAutoName; window.srcFileAutoNameSftp = srcFileAutoNameSftp; window.srcFileTypeSelect = srcFileTypeSelect; window.srcFileSftpAuthSelect = srcFileSftpAuthSelect; window.srcFileAdd = srcFileAdd; window.srcFileEdit = srcFileEdit; window.srcFileDelete = srcFileDelete; window.srcFileScan = srcFileScan; window.openFileSourcesModal = openFileSourcesModal; window.closeFileSourcesModal = closeFileSourcesModal; window._loadFileSources = _loadFileSources; window._renderFileSources = _renderFileSources; window.fsrcDetectSmb = fsrcDetectSmb; window.fsrcAutoName = fsrcAutoName; window.fsrcAddSource = fsrcAddSource; window.fsrcDelete = fsrcDelete; window.fsrcScan = fsrcScan; window._googleKeyDict = _googleKeyDict; window._googleAuthMode = _googleAuthMode;