// ============================================================
// 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;