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;