let selectedFiles = []; let scanResults = []; let selectedIndex = -1; let sortField = null; let sortDir = 'asc'; const fileInput = document.getElementById('fileInput'); const uploadInfo = document.getElementById('uploadInfo'); const resultsBody = document.getElementById('resultsBody'); const detailsContent = document.getElementById('detailsContent'); const emptyState = document.getElementById('emptyState'); const btnScan = document.getElementById('btnScan'); const btnSanitize = document.getElementById('btnSanitize'); const statusText = document.getElementById('statusText'); const dropZone = document.getElementById('dropZone'); const progressBar = document.getElementById('progressBar'); const progressFill = document.getElementById('progressFill'); const progressText = document.getElementById('progressText'); fileInput.addEventListener('change', handleFileSelect); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleFileSelect(); } }); document.querySelectorAll('#resultsTable th.sortable').forEach(th => { th.addEventListener('click', () => { const field = th.dataset.sort; if (sortField === field) { sortDir = sortDir === 'asc' ? 'desc' : 'asc'; } else { sortField = field; sortDir = 'asc'; } document.querySelectorAll('#resultsTable th.sortable').forEach(h => { h.classList.remove('asc', 'desc'); }); th.classList.add(sortDir); renderTable(); }); }); function handleFileSelect() { selectedFiles = Array.from(fileInput.files); if (selectedFiles.length === 0) { uploadInfo.textContent = 'No files selected'; } else if (selectedFiles.length === 1) { uploadInfo.textContent = selectedFiles[0].name; } else { uploadInfo.textContent = `${selectedFiles.length} files selected`; } } async function uploadFiles() { if (!selectedFiles.length) return false; setStatus('Uploading files...'); const formData = new FormData(); for (const file of selectedFiles) { formData.append('files', file); } try { const resp = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await resp.json(); if (data.error) { setStatus('Upload failed: ' + data.error); return false; } setStatus(`Uploaded ${data.count} file(s)`); return true; } catch (e) { setStatus('Upload failed: ' + e.message); return false; } } async function startScan() { const context = document.getElementById('context').value; const recursive = document.getElementById('recursive').checked; const uploaded = await uploadFiles(); if (!uploaded && !selectedFiles.length) { setStatus('No files to scan. Select files first.'); return; } btnScan.disabled = true; btnScan.textContent = 'Scanning...'; progressBar.style.display = 'flex'; progressFill.style.width = '0%'; progressText.textContent = 'Starting...'; try { const resp = await fetch('/api/scan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: '', recursive, context }) }); const data = await resp.json(); if (data.error) { setStatus('Scan failed: ' + data.error); btnScan.disabled = false; btnScan.textContent = 'Scan'; progressBar.style.display = 'none'; return; } await pollProgress(); await fetchResults(); } catch (e) { setStatus('Scan failed: ' + e.message); btnScan.disabled = false; btnScan.textContent = 'Scan'; progressBar.style.display = 'none'; } } async function pollProgress() { let done = false; while (!done) { await new Promise(r => setTimeout(r, 300)); try { const resp = await fetch('/api/scan/progress'); const p = await resp.json(); const pct = p.total > 0 ? Math.round((p.current / p.total) * 100) : 0; progressFill.style.width = Math.min(pct, 100) + '%'; const name = p.current_file ? p.current_file.split('/').pop().split('\\').pop() : ''; progressText.textContent = name ? `${p.current} / ${p.total} - ${name}` : `${p.current} / ${p.total}`; if (p.status === 'completed' || p.status === 'error') { progressFill.style.width = p.status === 'completed' ? '100%' : progressFill.style.width; progressText.textContent = p.status === 'completed' ? `Complete - ${p.total} file(s)` : 'Error: ' + (p.current_file || 'unknown'); done = true; await new Promise(r => setTimeout(r, 500)); } } catch (e) { done = true; } } } async function fetchResults() { try { const resp = await fetch('/api/results'); const data = await resp.json(); if (data.error) { setStatus('Failed to load results: ' + data.error); } else { scanResults = data.results; selectedIndex = -1; sortField = null; sortDir = 'asc'; document.querySelectorAll('#resultsTable th.sortable').forEach(h => h.classList.remove('asc', 'desc')); renderTable(); setStatus(`Scan complete: ${data.count} file(s) analyzed`); } } catch (e) { setStatus('Failed to load results: ' + e.message); } finally { btnScan.disabled = false; btnScan.textContent = 'Scan'; setTimeout(() => { progressBar.style.display = 'none'; }, 1000); } } function getSortValue(r, field) { switch (field) { case 'level': { const order = { 'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3 }; return order[r.level] ?? 99; } case 'score': return r.total_risk; case 'type': return r.file_type || ''; case 'path': return r.path || ''; case 'author': return (r.metadata.author || r.metadata.last_modified_by || '').toLowerCase(); default: return ''; } } function renderTable() { resultsBody.innerHTML = ''; emptyState.style.display = scanResults.length ? 'none' : 'block'; let items = scanResults.map((r, i) => ({ r, i })); if (sortField) { items.sort((a, b) => { const va = getSortValue(a.r, sortField); const vb = getSortValue(b.r, sortField); if (typeof va === 'number' && typeof vb === 'number') { return sortDir === 'asc' ? va - vb : vb - va; } const cmp = String(va).localeCompare(String(vb)); return sortDir === 'asc' ? cmp : -cmp; }); } items.forEach(({ r, i }) => { const tr = document.createElement('tr'); tr.dataset.index = i; const author = r.metadata.author || r.metadata.last_modified_by || ''; tr.innerHTML = `