Added SFTP to sources

This commit is contained in:
StyxX65 committed 2026-04-25 08:48:54 +02:00
1 parent 360eb1caed
commit e35bbe78a5
20 files changed
+826 -120

No files matched your search

+1
View File
@@ -31,6 +31,7 @@ Never revert to `!!window._googleConnected` / `_fileSources.length > 0` — thos
## Gotchas
- **`scheduler.js` strings must use `t()`** — frequency labels (`m365_sched_freq_daily/weekly/monthly`), "Next" (`m365_sched_next`), "Running..." (`m365_sched_running`), "Disabled" (`m365_sched_disabled`), empty-job text (`m365_sched_no_jobs`), and empty-history text (`m365_sched_no_runs`) all have translation keys. Do not hard-code English strings in `schedLoad()` or `schedRenderJobs()`.
- **Profile editor accounts** — default to unchecked. Only explicitly saved `user_ids` are checked.
- **Date presets** — stored as `years * 365` (integer days). Do not use `* 365.25`.
- **`copyTokenLink` is async** — called from `onclick` attributes as a fire-and-forget (the Promise is unhandled, which is fine). It `await`s `_getShareBaseUrl()` to get the machine's LAN IP before building the URL. Do not make it synchronous or revert to `window.location.origin` directly.
+142 -35
View File
@@ -378,6 +378,19 @@ function getGoogleScanOptions() {
// ── 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;
@@ -386,9 +399,8 @@ function srcFileRenderList() {
return;
}
list.innerHTML = S._fileSources.map(function(s) {
const isSmb = s.path && (s.path.startsWith('//') || s.path.startsWith('\\\\'));
const icon = isSmb ? '\uD83C\uDF10' : '\uD83D\uDCC1';
const sid = _esc(s.id||'');
const icon = _srcIcon(s);
const sid = _esc(s.id||'');
const slabel = _esc(s.label||s.path||'');
return '<div class="fsrc-row">'
+'<div class="fsrc-row-head">'
@@ -398,11 +410,47 @@ function srcFileRenderList() {
+'<button class="btn-edit" onclick="srcFileEdit(\''+sid+'\')" style="background:none;border:1px solid var(--border);color:var(--muted);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer">'+t('m365_fsrc_edit_btn','Edit')+'</button>'
+'<button class="btn-del" onclick="srcFileDelete(\''+sid+'\',\''+slabel+'\')">'+t('m365_profile_delete','Delete')+'</button>'
+'</div></div>'
+'<div class="fsrc-row-path">'+_esc(s.path||'')+(s.smb_user?' \u00b7 \uD83D\uDC64 '+_esc(s.smb_user):'')+'</div>'
+'<div class="fsrc-row-path">'+_srcSubtitle(s)+'</div>'
+'</div>';
}).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('\\\\');
@@ -427,30 +475,80 @@ function srcFileAutoName() {
}
async function srcFileAdd() {
const label = document.getElementById('srcFileLabel').value.trim();
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;
const stat = document.getElementById('srcFileStatus');
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; }
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){}
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 editId = document.getElementById('srcFileEditId');
const existingId = editId ? editId.value : '';
const body = {label, path, smb_host:smbHost, smb_user:smbUser};
if (existingId) body.id = existingId;
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; }
['srcFileLabel','srcFilePath','srcFileSmbHost','srcFileSmbUser','srcFileSmbPw'].forEach(function(id){const el=document.getElementById(id);if(el){el.value='';el._userEdited=false;}});
if (editId) editId.value='';
// 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');
document.getElementById('srcFileSmbFields').style.display='none';
srcFileTypeSelect('local');
stat.style.color='var(--accent)'; stat.textContent='\u2714 '+t('m365_fsrc_saved','Source saved');
await _loadFileSources();
srcFileRenderList();
@@ -462,20 +560,28 @@ function srcFileEdit(id) {
const s = S._fileSources.find(function(x){return x.id===id;});
if (!s) return;
const labelEl = document.getElementById('srcFileLabel');
const pathEl = document.getElementById('srcFilePath');
const hostEl = document.getElementById('srcFileSmbHost');
const userEl = document.getElementById('srcFileSmbUser');
const pwEl = document.getElementById('srcFileSmbPw');
const editId = document.getElementById('srcFileEditId');
if (labelEl) { labelEl.value = s.label||''; labelEl._userEdited = true; }
if (pathEl) pathEl.value = s.path||'';
if (hostEl) hostEl.value = s.smb_host||'';
if (userEl) userEl.value = s.smb_user||'';
if (pwEl) pwEl.value = s.smb_user ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : '';
if (editId) editId.value = id;
const isSmb = (s.path||'').startsWith('//') || (s.path||'').startsWith('\\\\');
const smbFields = document.getElementById('srcFileSmbFields');
if (smbFields) smbFields.style.display = isSmb ? 'flex' : 'none';
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');
@@ -547,9 +653,7 @@ function _renderFileSources() {
return;
}
list.innerHTML = S._fileSources.map(function(s) {
const isSmb = s.path && (s.path.startsWith('//') || s.path.startsWith('\\\\'));
const icon = isSmb ? '\uD83C\uDF10' : '\uD83D\uDCC1';
const userPart = s.smb_user ? ' \u00b7 \uD83D\uDC64 ' + _esc(s.smb_user) : '';
const icon = _srcIcon(s);
const sid = _esc(s.id || '');
const slabel = _esc(s.label || s.path || '');
return '<div class="fsrc-row">'
@@ -559,7 +663,7 @@ function _renderFileSources() {
+ '<button class="btn-scan" onclick="fsrcScan(\'' + sid + '\')">&#9654; ' + t('m365_fsrc_scan_btn','Scan') + '</button>'
+ '<button class="btn-del" onclick="fsrcDelete(\'' + sid + '\',\'' + slabel + '\')">' + t('m365_profile_delete','Delete') + '</button>'
+ '</div></div>'
+ '<div class="fsrc-row-path">' + _esc(s.path || '') + userPart + '</div>'
+ '<div class="fsrc-row-path">' + _srcSubtitle(s) + '</div>'
+ '</div>';
}).join('');
}
@@ -667,6 +771,9 @@ 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;
+7 -7
View File
@@ -18,19 +18,19 @@ function schedLoad() {
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' ? 'Weekly' : j2.frequency === 'monthly' ? 'Monthly' : 'Daily');
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 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 Next: ' + dt.toLocaleString(undefined,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
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 Disabled');
descEl.textContent = base + (js.enabled ? '' : ' \u00b7 ' + t('m365_sched_disabled','Disabled'));
if (runBtn) { runBtn.style.borderColor='var(--border)'; runBtn.style.color='var(--muted)'; }
}
});
@@ -41,13 +41,13 @@ function schedRenderJobs() {
var list = document.getElementById('schedJobList');
if (!list) return;
if (!_schedJobs.length) {
list.innerHTML = '<div style="font-size:11px;color:var(--muted);padding:4px 0">No scheduled scans yet.</div>';
list.innerHTML = '<div style="font-size:11px;color:var(--muted);padding:4px 0">' + t('m365_sched_no_jobs','No scheduled scans yet.') + '</div>';
return;
}
list.innerHTML = _schedJobs.map(function(j) {
var sid = _esc(j.id);
var sname = _esc(j.name || 'Unnamed');
var freqLabel = j.frequency === 'weekly' ? 'Weekly' : j.frequency === 'monthly' ? 'Monthly' : 'Daily';
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' : '';
@@ -217,7 +217,7 @@ function schedLoadHistory() {
if (!el) return;
fetch('/api/scheduler/history?limit=10').then(function(r){ return r.json(); }).then(function(d) {
var runs = d.runs || [];
if (!runs.length) { el.innerHTML = '<em>No scheduled runs yet</em>'; return; }
if (!runs.length) { el.innerHTML = '<em>' + t('m365_sched_no_runs','No scheduled runs yet') + '</em>'; return; }
var html = '';
runs.forEach(function(r) {
var ts = r.started_at ? new Date(r.started_at * 1000).toLocaleString() : '-';
+5 -4
View File
@@ -62,14 +62,15 @@ function renderSourcesPanel() {
S._pendingGoogleSources = null;
}
// File sources (local / SMB) — one entry per saved source
// File sources (local / SMB / SFTP) — one entry per saved source
if (S._fileSources.length > 0) {
html += '<div style="margin:6px 0 2px;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em">'
+ '<hr style="border:none;border-top:1px solid var(--border);margin:1px 0 2px">';
S._fileSources.forEach(function(s) {
const isSmb = s.path && (s.path.startsWith('//') || s.path.startsWith('\\\\'));
const icon = isSmb ? '\uD83C\uDF10' : '\uD83D\uDCC1';
const label = s.label || s.path || s.id;
const isSftp = s.source_type === 'sftp';
const isSmb = !isSftp && s.path && (s.path.startsWith('//') || s.path.startsWith('\\\\'));
const icon = isSftp ? '\uD83D\uDD12' : (isSmb ? '\uD83C\uDF10' : '\uD83D\uDCC1');
const label = s.label || s.path || s.id;
const isChecked = (s.id in checked) ? checked[s.id] : true;
html += '<label class="source-check">'
+ '<input type="checkbox" data-source-id="' + _esc(s.id) + '" data-source-type="file"' + (isChecked ? ' checked' : '') + '>'