// ============================================================ // SafeAIScan — Dashboard App Logic v2.0 // ============================================================ let scanProgressInterval = null; let findings = []; let currentContext = ""; let usageChart = null; let riskChart = null; // ============================================================ // CODE SCAN // ============================================================ async function scan() { const code = document.getElementById("code")?.value?.trim(); if (!code) { showToast("Paste some code to analyze", "warning"); return; } setLoader(true); startLiveProgress(); try { const data = await analyzeCode(code); findings = data.findings || []; renderAIInsights(data); renderVulnerabilities(data); updateStatus(findings); renderSeverityTabs(data); // Update usage counter from scan response — no extra request needed const plan = (localStorage.getItem("user_plan") || "free").toLowerCase(); const limits = getUserLimits() || {}; const dailyLimit = limits.daily_scans ?? (plan === "free" ? 10 : -1); updateUsageMeter(data.usage_today ?? 0, data.usage_limit ?? dailyLimit); // Refresh usage chart with new data point, then render risk distribution loadUsageChart(); loadRiskChart(findings); if (findings.length > 0) enrichCVE(findings); stopLiveProgress(); showToast( `Scan complete — ${findings.length} issue(s) found`, findings.length > 0 ? "warning" : "success" ); } catch (err) { console.error(err); stopLiveProgress(); if (err instanceof PlanError) { showUpgradePrompt(err.message); } else if (err instanceof LimitError) { showToast(err.message, "warning"); showLimitBanner(); } else { showToast("Scan failed: " + err.message, "error"); } } finally { setLoader(false); } } // ============================================================ // REPO SCAN // ============================================================ async function scanRepo() { // Check plan before even prompting // All users can scan repos — backend limits by daily scan count // Pro trial and Pro get unlimited; free gets 2 repos/day const repoUrl = prompt("Enter GitHub repo URL (https://github.com/user/repo):"); if (!repoUrl?.trim()) return; if (!repoUrl.startsWith("https://github.com/")) { showToast("Only GitHub HTTPS URLs are supported", "warning"); return; } setLoader(true); showToast("Queuing repo scan…", "info"); try { const data = await scanRepoAPI(repoUrl); showToast(`Scan queued · Task: ${data.task_id}`, "success"); pollTask(data.task_id); } catch (err) { console.error(err); if (err instanceof PlanError) { showUpgradePrompt(err.message); } else { showToast("Repo scan failed: " + err.message, "error"); } } finally { setLoader(false); } } // ============================================================ // TASK POLLING // ============================================================ async function pollTask(taskId) { const states = { CLONING: 15, VALIDATING: 35, SCANNING: 65, FINALIZING: 88, DONE: 100, FAILED: 0 }; const bar = document.getElementById("scanProgressBar"); const text = document.getElementById("scanProgressText"); let attempts = 0; const interval = setInterval(async () => { attempts++; if (attempts > 120) { // 5 min max clearInterval(interval); showToast("Scan is taking too long — check back later", "warning"); return; } try { const data = await getTaskStatus(taskId); const pct = states[data.state] ?? 50; if (bar) bar.style.width = pct + "%"; if (text) text.innerText = data.message || data.state || "Processing…"; if (data.state === "DONE") { clearInterval(interval); findings = data.result?.findings || data.findings || []; renderVulnerabilities({ findings }); updateStatus(findings); renderSeverityTabs({ findings }); stopLiveProgress(); showToast("Repo scan complete!", "success"); } if (data.state === "FAILED") { clearInterval(interval); if (text) text.innerText = "Scan failed"; stopLiveProgress(); showToast("Scan failed: " + (data.result?.error || "Unknown error"), "error"); } } catch (err) { console.error("Poll error:", err); clearInterval(interval); if (text) text.innerText = "Poll error"; } }, 2500); } // ============================================================ // LOADERS / PROGRESS // ============================================================ function setLoader(active) { const el = document.getElementById("loader"); if (!el) return; el.classList.toggle("active", active); // Disable scan buttons during scan const btns = document.querySelectorAll(".scan-actions .btn"); btns.forEach(b => { if (active) { b.disabled = true; b.style.opacity = "0.6"; } else { b.disabled = false; b.style.opacity = ""; } }); } function startLiveProgress() { let progress = 2; const bar = document.getElementById("scanProgressBar"); const text = document.getElementById("scanProgressText"); const steps = [ "Parsing code…", "Running static analysis…", "Checking vulnerability patterns…", "AI risk modeling…", "Mapping CVE database…", "Finalizing report…" ]; let stepIdx = 0; clearInterval(scanProgressInterval); scanProgressInterval = setInterval(() => { if (progress >= 95) { return; } progress += Math.random() * 5 + 1.5; if (bar) bar.style.width = Math.min(95, progress) + "%"; if (text && Math.floor(stepIdx) < steps.length) { text.innerText = steps[Math.floor(stepIdx)]; stepIdx += 0.35; } }, 340); } function stopLiveProgress() { clearInterval(scanProgressInterval); const bar = document.getElementById("scanProgressBar"); const text = document.getElementById("scanProgressText"); if (bar) bar.style.width = "100%"; if (text) text.innerText = "Complete"; setTimeout(() => { if (bar) bar.style.width = "0%"; if (text) text.innerText = ""; }, 2200); } // ============================================================ // USAGE METER // ============================================================ function updateUsageMeter(used, limit) { const el = document.getElementById("usage"); if (!el) return; // -1 or very large = unlimited (Pro / pro_trial / enterprise) const isUnlimited = !limit || limit < 0 || limit > 9000; if (isUnlimited) { // Show raw count with ∞ label — no progress bar el.innerHTML = ` ${used ?? 0} / ∞
Unlimited scans
`; } else { const safeUsed = Math.max(0, used ?? 0); const pct = Math.min(100, Math.round((safeUsed / limit) * 100)); const color = pct >= 90 ? "var(--danger)" : pct >= 70 ? "var(--warning)" : "var(--success)"; el.innerHTML = ` ${safeUsed} / ${limit}
${safeUsed} / ${limit} used today
`; } } function showLimitBanner() { const plan = getUserPlan(); const existing = document.getElementById("limitBanner"); if (existing) return; const banner = document.createElement("div"); banner.id = "limitBanner"; banner.style.cssText = ` background:rgba(251,146,60,0.08);border:1px solid rgba(251,146,60,0.25); border-radius:12px;padding:12px 16px;margin-bottom:14px; display:flex;align-items:center;justify-content:space-between;gap:12px; animation:popIn 0.3s ease both; `; banner.innerHTML = `
Daily scan limit reached
${plan === "free" ? "Free plan: 10 scans/day. " : ""}Upgrade for more scans.
`; const scanPanel = document.querySelector(".scan-panel"); if (scanPanel) scanPanel.parentNode.insertBefore(banner, scanPanel); } // ============================================================ // DATA LOADERS // ============================================================ async function loadUsage() { const el = document.getElementById("usage"); if (!el) return; try { // Primary: /api/me gives us today's usage + limits in one request // (avoids the /api/usage 403 for users without usage_tracking table access) const data = await getMe(); const plan = (data.plan || "free").toLowerCase(); const limits = data.limits || {}; const dailyLimit = limits.daily_scans ?? (plan === "free" ? 10 : -1); // Fetch today's usage count separately — fall back to 0 gracefully let todayCount = 0; try { const usageData = await getUsage(); const arr = Array.isArray(usageData) ? usageData : []; const today = new Date().toISOString().slice(0, 10); const record = arr.find(d => (d.date || "").startsWith(today)); todayCount = record?.request_count ?? record?.count ?? 0; } catch { // /api/usage may 403 — that's OK, just show 0 } updateUsageMeter(todayCount, dailyLimit); } catch (err) { console.warn("[SafeAIScan] loadUsage:", err.message); el.innerHTML = ``; } } async function loadHistory() { const list = document.getElementById("history"); if (!list) return; list.innerHTML = [1,2,3].map(() => `
` ).join(""); try { const data = await getHistory(); const arr = Array.isArray(data) ? data : []; if (!arr.length) { list.innerHTML = `
No scans yet — run your first scan!
`; return; } list.innerHTML = arr.slice(0, 8).map(item => { const risk = (item.risk || "LOW").toUpperCase(); const count = item.findings_count ?? item.score ?? "—"; const time = item.timestamp ? new Date(item.timestamp).toLocaleDateString() : ""; const sevClass = risk === "HIGH" || risk === "CRITICAL" ? "sev-high" : risk === "MEDIUM" ? "sev-medium" : "sev-low"; return `
${risk} ${typeof count === "number" ? `${count} issue${count !== 1 ? "s" : ""}` : count} ${time}
`; }).join(""); } catch (err) { console.warn("[SecretScan] loadHistory:", err.message); list.innerHTML = `
No scan history yet — run your first scan!
`; } } async function loadPlan() { const el = document.getElementById("plan"); if (!el) return; try { const data = await getMe(); const plan = (data.plan || "free").toLowerCase(); const limits = data.limits || {}; const isPro = data.is_pro || isProUser(); const isTrial = plan === "pro_trial"; const daysLeft = data.days_left || 0; const trialActive= data.trial_active || false; // Store everything for offline use localStorage.setItem("user_plan", plan); localStorage.setItem("is_pro", isPro ? "true" : "false"); localStorage.setItem("trial_active", trialActive ? "true" : "false"); localStorage.setItem("trial_days_left", String(daysLeft)); if (data.email) localStorage.setItem("user_email", data.email); // Build plan badge const badgeMap = { free: { cls: "sev-low", icon: "bi-person", label: "FREE" }, pro_trial: { cls: "", icon: "bi-gift-fill", label: "PRO TRIAL", style: "background:linear-gradient(135deg,rgba(0,255,163,.2),rgba(91,123,254,.15));color:#00ffa3;border:1px solid rgba(0,255,163,.35);" }, pro: { cls: "sev-high", icon: "bi-lightning-charge", label: "PRO" }, enterprise: { cls: "sev-critical", icon: "bi-building", label: "ENTERPRISE" }, }; const badge = badgeMap[plan] || badgeMap.free; const badgeStyle = badge.style ? `style="padding:4px 10px;font-size:11px;${badge.style}"` : `class="badge-pill ${badge.cls}" style="padding:4px 10px;font-size:11px;"`; const scanLabel = (limits.daily_scans === -1 || limits.daily_scans > 900) ? "Unlimited scans" : `${limits.daily_scans} scans/day`; // Trial countdown line const trialLine = isTrial && trialActive ? `
${daysLeft <= 5 ? `Trial ends in ${daysLeft} day${daysLeft !== 1 ? "s" : ""}!` : `Trial ends in ${daysLeft} days`}  ·  Upgrade →
` : ""; el.innerHTML = `
${badge.label} ${escHtml(data.email || "")}
${scanLabel}
${trialLine}`; applyPlanGating(plan, limits); if (typeof window.applyNavGating === "function") window.applyNavGating(plan); // Trial info shown inline in sidebar — no layout-breaking banner needed } catch (err) { console.warn("[SecretScan] loadPlan failed:", err.message); el.innerHTML = `FREE`; } } /* ── Trial banner — intentionally disabled to avoid layout distortion. Trial status is shown inline in the sidebar plan widget instead. ── */ function renderTrialBanner() { // No-op: banner removed to prevent sticky overlay breaking the dashboard layout. document.getElementById("trialBannerGlobal")?.remove(); } function applyPlanGating(plan, limits) { // pro_trial = full Pro access if (plan === "pro_trial") plan = "pro"; plan = (plan || "free").toLowerCase(); const isPro = ["pro", "pro_trial", "enterprise"].includes(plan) || isProUser?.(); // Repo scan button — id-based const repoBtn = document.getElementById("repoScanBtn"); if (repoBtn) { if (!isPro) { repoBtn.innerHTML = `Scan Repo`; repoBtn.style.opacity = "0.6"; repoBtn.title = "Requires Pro plan"; } else { repoBtn.innerHTML = `Scan Repo`; repoBtn.style.opacity = ""; repoBtn.title = ""; } } // Nav lock badges const repoBadge = document.getElementById("repoLockBadge"); const cveBadge = document.getElementById("cveLockBadge"); if (repoBadge) repoBadge.style.display = isPro ? "none" : "inline-flex"; if (cveBadge) cveBadge.style.display = isPro ? "none" : "inline-flex"; // Upgrade section const upgradeSection = document.getElementById("upgradeSection"); if (upgradeSection) upgradeSection.style.display = (isPro || plan==="pro_trial") ? "none" : ""; } async function loadTeam() { const list = document.getElementById("teamList"); if (!list) return; const plan = getUserPlan(); if (plan === "free") { list.innerHTML = `
  • Team management requires Pro
  • `; return; } try { const res = await apiRequest("/api/org/users"); const data = await safeJson(res); list.innerHTML = (data || []).map(u => `
  • ${escHtml(u.email)} ${escHtml(u.plan || "free")}
  • `).join("") || `
  • No team members yet
  • `; } catch { list.innerHTML = `
  • Team unavailable
  • `; } } // ============================================================ // RENDER VULNERABILITIES // ============================================================ function renderVulnerabilities(data) { const container = document.getElementById("vulnCards"); if (!container) return; const list = Array.isArray(data) ? data : (data.findings || []); if (!list.length) { container.innerHTML = `
    All Clear!
    No security issues detected in this code.
    `; return; } const sevOrder = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 }; const sorted = [...list].sort((a, b) => (sevOrder[b.severity] || 0) - (sevOrder[a.severity] || 0)); container.innerHTML = sorted.map((vuln, i) => { const sev = (vuln.severity || "LOW").toUpperCase(); const badgeClass = sev === "CRITICAL" ? "sev-critical" : sev === "HIGH" ? "sev-high" : sev === "MEDIUM" ? "sev-medium" : "sev-low"; const borderColor = sev === "CRITICAL" ? "rgba(192,38,211,0.3)" : sev === "HIGH" ? "rgba(244,63,94,0.25)" : sev === "MEDIUM" ? "rgba(251,146,60,0.2)" : "var(--border)"; return `
    ${escHtml(vuln.title || "Security Issue")}
    ${vuln.file ? `${escHtml(vuln.file)}` : ""} ${vuln.line ? `· line ${vuln.line}` : ""} ${vuln.source ? `· ${escHtml(vuln.source)}` : ""}
    ${sev}
    ${escHtml(vuln.description || "No description provided.")}
    ${vuln.fix && vuln.fix !== "No auto-fix available" ? `
    Recommended Fix
    ${escHtml(vuln.fix)}
    ` : ""} ${vuln.cve && vuln.cve !== "N/A" ? `
    ${escHtml(vuln.cve)} ${vuln.cvss != null ? `CVSS ${vuln.cvss}` : ""}
    ` : ""}
    `; }).join(""); } function toggleVuln(card, idx) { const wasActive = card.classList.contains("active"); document.querySelectorAll(".vuln-card.active").forEach(c => { c.classList.remove("active"); const ch = c.querySelector('[id^="chevron-"]'); if (ch) ch.style.transform = ""; }); if (!wasActive) { card.classList.add("active"); const chevron = document.getElementById(`chevron-${idx}`); if (chevron) chevron.style.transform = "rotate(180deg)"; } } // ============================================================ // AI INSIGHTS // ============================================================ function renderAIInsights(data) { const container = document.getElementById("aiInsights"); if (!container) return; const ai = data.ai || {}; const explain = ai.explanation || ""; const fixes = Array.isArray(ai.fixes) ? ai.fixes : []; const findings = data.findings || []; const plan = (localStorage.getItem("user_plan") || getUserPlan() || "free").toLowerCase(); const isPro = plan === "pro" || plan === "pro_trial" || plan === "enterprise"; // Always show something useful — even if AI is empty, show scan summary const risk = data.risk || "LOW"; const score = data.score || 0; const riskColors = { CRITICAL:"#f43f5e", HIGH:"#fb7185", MEDIUM:"#fdba74", LOW:"#fbbf24" }; const riskColor = riskColors[risk] || "#fbbf24"; // Build severity counts from findings const sevCounts = { CRITICAL:0, HIGH:0, MEDIUM:0, LOW:0 }; findings.forEach(f => { sevCounts[f.severity] = (sevCounts[f.severity]||0)+1; }); const sevSummary = Object.entries(sevCounts) .filter(([,v]) => v > 0) .map(([k,v]) => `${v} ${k}`).join(" "); container.innerHTML = `
    AI Security Insights ${!isPro ? 'Basic' : ""}
    ${risk}
    Risk level
    ${sevSummary || '✓ No issues detected'}
    ${score > 0 ? `
    =40?"var(--warning)":"var(--success)"};">${score}
    SCORE
    ` : ""}
    ${explain ? `

    ${escHtml(explain)}

    ` : ""} ${fixes.length > 0 ? `
    Recommended Actions
    ${fixes.slice(0, isPro ? 999 : 3).map(f => `
    ${escHtml(f)}
    `).join("")}
    ${!isPro && fixes.length > 3 ? `
    +${fixes.length - 3} more fix suggestions with Pro
    ` : ""} ` : ""} ${!isPro && findings.length > 0 ? `
    🎁 30-day Pro trial free — unlimited scans, full AI, PDF reports
    ` : ""}
    `; } // ============================================================ // STATUS + SEVERITY TABS // ============================================================ function updateStatus(findings) { const statusEl = document.getElementById("statusText"); if (!statusEl) return; const hasCritical = findings.some(f => ["HIGH","CRITICAL"].includes(f.severity)); const hasMedium = findings.some(f => f.severity === "MEDIUM"); statusEl.innerHTML = hasCritical ? `Vulnerable` : hasMedium ? `Caution` : `Secure`; statusEl.className = hasCritical ? "status-risk" : hasMedium ? "" : "status-safe"; } function renderSeverityTabs(data) { const el = document.getElementById("severityTabs"); if (!el) return; const f = data.findings || []; if (!f.length) { el.innerHTML = ""; return; } const counts = { CRITICAL: f.filter(x => x.severity === "CRITICAL").length, HIGH: f.filter(x => x.severity === "HIGH").length, MEDIUM: f.filter(x => x.severity === "MEDIUM").length, LOW: f.filter(x => x.severity === "LOW").length }; el.innerHTML = Object.entries(counts) .filter(([, v]) => v > 0) .map(([sev, cnt]) => ` ${sev} ${cnt} `).join(""); } // ============================================================ // CVE ENRICHMENT — Pro, pro_trial, and Enterprise only; background, non-blocking // ============================================================ async function enrichCVE(findingsList) { const plan = getUserPlan(); if (!["pro", "pro_trial", "enterprise"].includes(plan)) return; for (let i = 0; i < findingsList.length && i < 5; i++) { const vuln = findingsList[i]; const box = document.getElementById(`cve-${i}`); if (!box || !vuln.title || (vuln.cve && vuln.cve !== "N/A")) continue; try { const res = await apiRequest(`/api/cve/search?query=${encodeURIComponent(vuln.title)}`); const data = await safeJson(res); const cves = data?.cves || []; if (cves.length) { const top = cves[0]; box.innerHTML = `
    ${escHtml(top.id || "")} ${top.cvss != null ? `· CVSS ${top.cvss}` : ""} ${top.description ? ` · ${escHtml(top.description.substring(0, 100))}…` : ""}
    `; } } catch { /* silent — CVE enrichment is optional */ } } } // ============================================================ // CHARTS // ============================================================ async function loadUsageChart() { const ctx = document.getElementById("usageChart"); if (!ctx) return; // Build a 7-day window ending today as a fallback skeleton const todayStr = new Date().toISOString().slice(0, 10); const days = Array.from({ length: 7 }, (_, i) => { const d = new Date(); d.setDate(d.getDate() - (6 - i)); return d.toISOString().slice(0, 10); }); const labelMap = {}; days.forEach(d => { labelMap[d] = new Date(d).toLocaleDateString("en", { weekday: "short" }); }); let dataMap = {}; days.forEach(d => { dataMap[d] = 0; }); try { const raw = await getUsage(); const arr = Array.isArray(raw) ? raw : []; arr.forEach(r => { const key = (r.date || "").slice(0, 10); if (key in dataMap) dataMap[key] = r.request_count ?? r.count ?? 0; }); } catch { // /api/usage may 403 — keep zeroed defaults; chart still renders } const labels = days.map(d => labelMap[d]); const values = days.map(d => dataMap[d]); if (usageChart) { usageChart.destroy(); usageChart = null; } const gradient = ctx.getContext("2d").createLinearGradient(0, 0, 0, 200); gradient.addColorStop(0, "rgba(91,123,254,0.45)"); gradient.addColorStop(1, "rgba(91,123,254,0)"); usageChart = new Chart(ctx, { type: "line", data: { labels, datasets: [{ label: "Scans", data: values, borderColor: "#5b7bfe", borderWidth: 2.5, backgroundColor: gradient, fill: true, tension: 0.45, pointBackgroundColor: "#5b7bfe", pointRadius: 4, pointHoverRadius: 7 }] }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { backgroundColor: "#0f1a2e", titleColor: "#e8edf8", bodyColor: "#8296b3", borderColor: "#1e3a5f", borderWidth: 1, callbacks: { label: (ctx) => ` ${ctx.parsed.y} scan${ctx.parsed.y !== 1 ? "s" : ""}` } } }, scales: { x: { grid: { color: "rgba(255,255,255,0.04)" }, ticks: { color: "#8296b3", font: { size: 11 } } }, y: { grid: { color: "rgba(255,255,255,0.04)" }, ticks: { color: "#8296b3", font: { size: 11 }, stepSize: 1, // Only show integers on y-axis — no 0.5, 1.5 etc. callback: (v) => Number.isInteger(v) ? v : null }, beginAtZero: true, // Ensure minimum range of 1 so the line shows up even with 0 data suggestedMax: Math.max(...values, 1) } } } }); } function loadRiskChart(findingsList) { const ctx = document.getElementById("riskChart"); if (!ctx) return; const counts = { Critical: 0, High: 0, Medium: 0, Low: 0 }; (findingsList || []).forEach(f => { const s = (f.severity || "low").toUpperCase(); if (s === "CRITICAL") counts.Critical++; else if (s === "HIGH") counts.High++; else if (s === "MEDIUM") counts.Medium++; else counts.Low++; }); const total = Object.values(counts).reduce((a, b) => a + b, 0); if (riskChart) { riskChart.destroy(); riskChart = null; } // Show empty state if no findings — don't silently leave the panel blank if (!total) { const container = ctx.closest(".chart-container, div") || ctx.parentElement; // Only show placeholder if canvas parent is visible const placeholder = document.getElementById("riskChartEmpty"); if (placeholder) placeholder.style.display = "flex"; ctx.style.display = "none"; return; } // Hide placeholder, show canvas const placeholder = document.getElementById("riskChartEmpty"); if (placeholder) placeholder.style.display = "none"; ctx.style.display = ""; riskChart = new Chart(ctx, { type: "doughnut", data: { labels: Object.keys(counts), datasets: [{ data: Object.values(counts), backgroundColor: ["#c026d3","#f43f5e","#fb923c","#34d399"], borderWidth: 0, hoverOffset: 6 }] }, options: { responsive: true, cutout: "68%", plugins: { legend: { position: "right", labels: { color: "#8296b3", font: { size: 11 }, padding: 12 } }, tooltip: { backgroundColor: "#0f1a2e", titleColor: "#e8edf8", bodyColor: "#8296b3" } } } }); } // ============================================================ // API KEY UI // ============================================================ function initApiKey() { const el = document.getElementById("apiKeyDisplay"); if (!el) return; const key = localStorage.getItem("api_key") || ""; el.innerText = key ? maskKey(key) : "Not available"; el.dataset.full = key; el.dataset.masked = key ? maskKey(key) : ""; el.dataset.shown = "false"; } function maskKey(key) { if (!key || key.length < 10) return "••••••••••••"; return key.substring(0, 8) + "••••••••" + key.substring(key.length - 4); } function toggleApiKey() { const el = document.getElementById("apiKeyDisplay"); const icon = document.getElementById("toggleKeyIcon"); if (!el) return; const shown = el.dataset.shown === "true"; el.innerText = shown ? el.dataset.masked : el.dataset.full; el.dataset.shown = String(!shown); if (icon) icon.className = shown ? "bi bi-eye" : "bi bi-eye-slash"; } function copyKey() { const key = localStorage.getItem("api_key"); if (!key || key === "undefined") { showToast("No API key available", "warning"); return; } navigator.clipboard.writeText(key).then(() => { showToast("API key copied to clipboard!", "success"); const confirm = document.getElementById("copyConfirm"); if (confirm) { confirm.classList.add("show"); setTimeout(() => confirm.classList.remove("show"), 1800); } }).catch(() => { // Fallback for older browsers const el = document.createElement("textarea"); el.value = key; el.style.position = "fixed"; el.style.opacity = "0"; document.body.appendChild(el); el.select(); document.execCommand("copy"); document.body.removeChild(el); showToast("API key copied!", "success"); }); } // ============================================================ // UTILITY // FIX: guarded to avoid duplicate declarations with api.js // ============================================================ function logout() { localStorage.clear(); window.location.replace("login.html"); } // Use window.escHtml if already defined by api.js (loaded first), else define it function escHtml(str) { if (window.escHtml && window.escHtml !== escHtml) return window.escHtml(str); if (!str) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function cvssSev(score) { if (window.cvssSev && window.cvssSev !== cvssSev) return window.cvssSev(score); if (score == null) return "low"; if (score >= 9) return "critical"; if (score >= 7) return "high"; if (score >= 4) return "medium"; return "low"; } async function exportPDF() { if (!findings.length) { showToast("Run a scan first to export results", "warning"); return; } showToast("Generating PDF report…", "info"); try { const res = await apiRequest("/api/report/pdf", { method: "POST", body: JSON.stringify({ findings }) }); const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "safeaiscan-report.pdf"; a.click(); window.URL.revokeObjectURL(url); showToast("PDF report downloaded!", "success"); } catch (err) { showToast("PDF export failed: " + err.message, "error"); } } function openSide(v) { currentContext = JSON.stringify(v); const side = document.getElementById("side"); const title = document.getElementById("sideTitle"); const desc = document.getElementById("sideDesc"); if (!side) return; if (title) title.innerText = v.title || v.match || "Finding"; if (desc) desc.innerHTML = `

    ${escHtml(v.description || "No description available.")}

    ${v.fix && v.fix !== "No auto-fix available" ? `
    Recommended Fix
    ${escHtml(v.fix)}
    ` : ""} ${v.cve && v.cve !== "N/A" ? `
    ${escHtml(v.cve)} ${v.cvss != null ? `CVSS ${v.cvss}` : ""}
    ` : ""} `; side.classList.add("open"); } function closeSide() { document.getElementById("side")?.classList.remove("open"); } async function askAI() { const q = document.getElementById("aiInput")?.value?.trim(); const chat = document.getElementById("aiChat"); if (!q || !chat) return; chat.innerHTML += `
    You: ${escHtml(q)}
    `; document.getElementById("aiInput").value = ""; try { const res = await apiRequest("/api/ai/explain", { method: "POST", body: JSON.stringify({ question: q, context: currentContext }) }); const data = await safeJson(res); const text = data.explanation || data.data?.explanation || ""; chat.innerHTML += `
    ${escHtml(text)}
    `; chat.scrollTop = chat.scrollHeight; } catch (err) { chat.innerHTML += `
    ${escHtml(err.message)}
    `; } } function renderTimeline(data) { const el = document.getElementById("timeline"); if (!el) return; const steps = data.timeline || ["Code received","Parsing syntax","AI analysis","CVE lookup","Report ready"]; el.innerHTML = steps.map(s => `
    ${escHtml(s)}
    `).join(""); } function renderTree(nodes) { if (!Array.isArray(nodes)) return ""; return nodes.map(n => `
    ${n.type === "dir" ? "📁" : "📄"} ${escHtml(n.name)} ${n.children ? renderTree(n.children) : ""}
    `).join(""); } // ============================================================ // SCROLL FADE-IN // ============================================================ function initScrollFade() { const observer = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) e.target.classList.add("show"); }); }, { threshold: 0.1 }); document.querySelectorAll(".fade-in").forEach(el => observer.observe(el)); } // ============================================================ // PLAN EVENT LISTENER // ============================================================ document.addEventListener("planUpdated", (e) => { const d = e.detail; if (d.usage_today != null) updateUsageMeter(d.usage_today, d.usage_limit); }); // ============================================================ // INIT // ============================================================ async function init() { initApiKey(); initScrollFade(); const has = (id) => !!document.getElementById(id); // Show risk chart empty state immediately — it populates after first scan if (has("riskChart")) loadRiskChart([]); // Load all data in parallel for speed const tasks = []; if (has("plan")) tasks.push(loadPlan()); if (has("usage")) tasks.push(loadUsage()); if (has("history")) tasks.push(loadHistory()); if (has("usageChart")) tasks.push(loadUsageChart()); if (has("teamList")) tasks.push(loadTeam()); await Promise.allSettled(tasks); } document.addEventListener("DOMContentLoaded", init); // ---- GLOBAL EXPORTS ---- window.scan = scan; window.scanRepo = scanRepo; window.copyKey = copyKey; window.toggleApiKey = toggleApiKey; window.initApiKey = initApiKey; // FIX: expose so api.js rotateApiKey can call it window.logout = logout; window.exportPDF = exportPDF; window.openSide = openSide; window.closeSide = closeSide; window.askAI = askAI; // FIX: fetchCVE is defined in api.js — don't re-export an undefined ref here // window.fetchCVE = fetchCVE; ← removed window.toggleVuln = toggleVuln; window.renderVulnerabilities = renderVulnerabilities; window.loadRiskChart = loadRiskChart; window.escHtml = escHtml; window.cvssSev = cvssSev; window.rotateApiKey = rotateApiKey;