// ── Scan history browser ────────────────────────────────────────────────────── // Lets the user load and browse results from any past scan session without // running a new scan. Sessions are groups of concurrent M365 + Google + File // scans (same 300-second window used by get_session_items on the server). import { S } from './state.js'; const _SRC_LABELS = { email: 'Outlook', onedrive: 'OneDrive', sharepoint: 'SharePoint', teams: 'Teams', gmail: 'Gmail', gdrive: 'Google Drive', local: 'Lokal', smb: 'SMB', }; let _sessions = null; // cached list; null = stale let _latestRefScanId = null; // ref_scan_id of the newest session // ── Session cache ───────────────────────────────────────────────────────────── async function _fetchSessions() { try { const r = await fetch('/api/db/sessions'); _sessions = await r.json(); } catch(e) { _sessions = []; } _latestRefScanId = _sessions.length ? _sessions[0].ref_scan_id : null; return _sessions; } function invalidateHistoryCache() { _sessions = null; _latestRefScanId = null; } // ── Load a session into the results grid ────────────────────────────────────── async function loadHistorySession(refScanId) { // refScanId: null → latest session, positive int → specific session let resolvedRef = refScanId; if (resolvedRef === null) { const sessions = _sessions !== null ? _sessions : await _fetchSessions(); if (!sessions.length) { // No scans in DB — nothing to show window.loadLastScanSummary?.(); return; } resolvedRef = sessions[0].ref_scan_id; } try { const r = await fetch('/api/db/flagged?ref=' + resolvedRef); const items = await r.json(); closeHistoryPicker(); if (!Array.isArray(items) || items.length === 0) { S._historyRefScanId = null; _setHistoryBanner(false); window.loadLastScanSummary?.(); return; } S._historyRefScanId = resolvedRef; S.flaggedData = items; S.filteredData = []; const grid = document.getElementById('grid'); const emptyState = document.getElementById('emptyState'); const lastScan = document.getElementById('lastScanSummary'); if (emptyState) emptyState.style.display = 'none'; if (lastScan) lastScan.style.display = 'none'; if (grid) { grid.innerHTML = ''; grid.style.display = 'grid'; } window.renderGrid(items); try { window.markOverdueCards(); } catch(_) {} try { window.loadTrend(); } catch(_) {} _setHistoryBanner(true, resolvedRef); } catch(e) { console.error('[history] failed to load session:', e); } } // ── Banner ──────────────────────────────────────────────────────────────────── function _setHistoryBanner(visible, resolvedRef) { const banner = document.getElementById('historyBanner'); const bannerTxt = document.getElementById('historyBannerText'); const latestBtn = document.getElementById('historyLatestBtn'); if (!banner) return; if (!visible) { banner.style.display = 'none'; return; } const sess = (_sessions || []).find(s => s.ref_scan_id === resolvedRef); let label = ''; if (sess) { const date = new Date(sess.started_at * 1000).toLocaleDateString(undefined, {day: 'numeric', month: 'short', year: 'numeric'}); const time = new Date(sess.started_at * 1000).toLocaleTimeString(undefined, {hour: '2-digit', minute: '2-digit'}); const srcStr = (sess.sources || []).map(s => _SRC_LABELS[s] || s).join(' · '); label = date + ' ' + time + (srcStr ? ' · ' + srcStr : '') + ' · ' + sess.flagged_count + ' ' + t('history_items', 'items'); } else { label = S.flaggedData.length + ' ' + t('history_items', 'items'); } if (bannerTxt) bannerTxt.textContent = label; if (latestBtn) latestBtn.style.display = (resolvedRef !== _latestRefScanId) ? '' : 'none'; banner.style.display = 'flex'; } function exitHistoryMode() { S._historyRefScanId = null; const banner = document.getElementById('historyBanner'); if (banner) banner.style.display = 'none'; closeHistoryPicker(); } // ── Session picker dropdown ─────────────────────────────────────────────────── async function openHistoryPicker() { const drop = document.getElementById('historyDropdown'); if (!drop) return; // Toggle if (drop.style.display !== 'none') { drop.style.display = 'none'; return; } drop.innerHTML = '