SafeAIScan / frontend /dashboard.html
TafadzwaTaps
fix: bugs fixed
4bc490b
Raw
History Blame Contribute Delete
29.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SafeAIScan β€” Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<style>
/* ---- DASHBOARD LAYOUT ---- */
.dash-layout {
display: grid;
grid-template-columns: 260px 1fr 260px;
grid-template-rows: auto 1fr;
min-height: 100vh;
gap: 0;
}
.dash-topbar {
grid-column: 1 / -1;
background: var(--bg-1);
border-bottom: 1px solid var(--border);
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(12px);
}
.dash-sidebar {
background: var(--bg-1);
border-right: 1px solid var(--border);
padding: 16px;
overflow-y: auto;
height: calc(100vh - 53px);
position: sticky;
top: 53px;
}
.dash-main {
padding: 20px;
overflow-y: auto;
max-height: calc(100vh - 53px);
}
.dash-aside {
background: var(--bg-1);
border-left: 1px solid var(--border);
padding: 16px;
overflow-y: auto;
height: calc(100vh - 53px);
position: sticky;
top: 53px;
}
/* ---- SCAN PANEL ---- */
.scan-panel {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 18px;
transition: border-color 0.2s;
}
.scan-panel-title {
font-family: 'Syne', sans-serif;
font-size: 15px;
font-weight: 700;
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
}
/* ---- NAV LINK ---- */
.side-nav-item {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 12px;
border-radius: var(--radius-sm);
font-size: 13px;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
text-decoration: none;
margin-bottom: 2px;
}
.side-nav-item:hover { background: var(--bg-3); color: var(--text-primary); }
.side-nav-item.active { background: rgba(91,123,254,0.12); color: #93aaff; border: 1px solid rgba(91,123,254,0.2); }
.side-nav-item i { font-size: 15px; }
.side-nav-item.locked { opacity: 0.5; cursor: not-allowed; }
.side-nav-item.locked:hover { background: transparent; color: var(--text-muted); }
/* ---- SECTION LABEL ---- */
.side-label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-faint);
padding: 10px 12px 4px;
font-weight: 600;
}
/* ---- SCAN ACTIONS ---- */
.scan-actions { display: flex; gap: 10px; margin-top: 12px; }
/* ---- CHART TABS ---- */
.chart-tabs { display: flex; gap: 6px; margin-bottom: 14px; }
.chart-tab {
font-size: 11px;
padding: 5px 12px;
border-radius: 99px;
cursor: pointer;
color: var(--text-muted);
border: 1px solid var(--border);
background: transparent;
transition: all 0.15s;
}
.chart-tab.active { background: rgba(91,123,254,0.12); color: #93aaff; border-color: rgba(91,123,254,0.3); }
/* ---- VULN CARDS ---- */
.vuln-card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 14px 16px;
margin-bottom: 10px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.vuln-card:hover { background: var(--bg-3); border-color: var(--border-bright); }
.vuln-card.active { background: rgba(91,123,254,0.04); border-color: rgba(91,123,254,0.25); }
.vuln-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 3px; }
.vuln-meta { font-size: 11px; color: var(--text-faint); display: flex; gap: 4px; flex-wrap: wrap; }
.vuln-details { display: none; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
.vuln-card.active .vuln-details { display: block; }
.fix-box {
background: rgba(4,12,24,0.6);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--success);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
/* ---- LOCKED FEATURE ---- */
.locked-feature {
position: relative;
pointer-events: auto !important;
}
/* ---- API KEY BOX ---- */
.apikey-box {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-muted);
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
word-break: break-all;
letter-spacing: 0.02em;
min-height: 36px;
}
.copy-confirm {
font-size: 11px;
color: var(--success);
opacity: 0;
transition: opacity 0.2s;
}
.copy-confirm.show { opacity: 1; }
/* ---- SCAN LOADER ---- */
.scan-loader {
display: none;
align-items: center;
gap: 12px;
padding: 10px 0;
}
.scan-loader.active { display: flex; }
.pulse-ring {
width: 20px; height: 20px;
border: 2px solid var(--accent);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ---- PROGRESS BAR ---- */
.scan-progress-label { font-size: 11px; color: var(--text-faint); font-family: 'JetBrains Mono', monospace; margin-bottom: 4px; min-height: 16px; }
.progress { height: 4px; background: var(--bg-3); border-radius: 99px; overflow: hidden; border: none; }
.progress-bar { background: linear-gradient(90deg, var(--accent), #38bdf8); transition: width 0.35s ease; }
/* ---- HISTORY ITEMS ---- */
.history-item:hover { border-color: var(--border-bright) !important; }
/* ---- RESPONSIVE ---- */
@media (max-width: 1024px) {
.dash-layout { grid-template-columns: 1fr; }
.dash-sidebar, .dash-aside { display: none; }
.dash-main { padding: 14px; max-height: unset; }
}
</style>
<script>
// ---- DEV MODE β€” must be set BEFORE api.js / app.js load ----
// true = all Pro/Enterprise features unlocked (default for local dev)
// false = production gating
window.DEV_MODE = window.DEV_MODE !== undefined ? window.DEV_MODE : true;
// Auth guard β€” skip in DEV_MODE so devs can load dashboard directly
if (!window.DEV_MODE) {
const _t = localStorage.getItem("access_token");
if (!_t || _t === "undefined" || _t === "null") {
window.location.replace("login.html");
}
}
</script>
</head>
<body>
<div class="dash-layout">
<!-- ============================================================
TOP BAR
============================================================ -->
<header class="dash-topbar">
<div style="display:flex;align-items:center;gap:12px;">
<span style="font-family:'Syne',sans-serif;font-size:16px;font-weight:800;letter-spacing:-0.03em;">
<span style="color:var(--accent);">Safe</span>AIScan
<span style="color:var(--border-bright);font-size:14px;">⬑</span>
</span>
<span class="badge-soft" style="font-size:10px;">
<span class="status-dot online"></span>AI Engine Online
</span>
</div>
<div style="display:flex;align-items:center;gap:10px;">
<div id="severityTabs" style="display:flex;gap:5px;"></div>
<button class="btn btn-sm btn-outline-light" onclick="logout()" style="font-size:12px;">
<i class="bi bi-box-arrow-right me-1"></i>Logout
</button>
</div>
</header>
<!-- ============================================================
LEFT SIDEBAR
============================================================ -->
<aside class="dash-sidebar">
<!-- Plan / User -->
<div class="scan-panel" style="margin-bottom:14px;">
<div style="font-size:9px;text-transform:uppercase;letter-spacing:0.8px;color:var(--text-faint);margin-bottom:8px;">
Account
</div>
<div id="plan">
<div class="skeleton" style="height:24px;border-radius:6px;"></div>
</div>
</div>
<!-- API Key -->
<div class="scan-panel" style="margin-bottom:14px;">
<div style="font-size:9px;text-transform:uppercase;letter-spacing:0.8px;color:var(--text-faint);margin-bottom:8px;">
API Key
</div>
<div class="apikey-box" id="apiKeyDisplay">Loading…</div>
<div style="display:flex;gap:6px;margin-top:8px;">
<button class="btn btn-sm btn-outline-light flex-fill" onclick="toggleApiKey()">
<i class="bi bi-eye" id="toggleKeyIcon"></i> Show
</button>
<button class="btn btn-sm btn-outline-light flex-fill" onclick="copyKey()">
<i class="bi bi-copy me-1"></i>Copy
</button>
</div>
<div style="margin-top:6px;">
<button class="btn btn-sm btn-outline-light w-100" onclick="rotateApiKey()" style="font-size:11px;opacity:0.7;">
<i class="bi bi-arrow-clockwise me-1"></i>Rotate Key
</button>
</div>
<div id="copyConfirm" class="copy-confirm mt-1 text-center">
<i class="bi bi-check-circle me-1"></i>Copied!
</div>
</div>
<!-- Navigation -->
<div class="side-label">Tools</div>
<a class="side-nav-item active" onclick="window.scrollTo(0,0)" style="cursor:pointer;">
<i class="bi bi-shield-shaded"></i> Code Scanner
</a>
<a class="side-nav-item" id="nav-repo" style="cursor:pointer;">
<i class="bi bi-github"></i> Repo Scanner
<span id="repoLockBadge" class="badge-pill sev-low" style="margin-left:auto;font-size:9px;display:none;">PRO</span>
</a>
<a class="side-nav-item" id="nav-cve" style="cursor:pointer;">
<i class="bi bi-bug"></i> CVE Lookup
<span id="cveLockBadge" class="badge-pill sev-low" style="margin-left:auto;font-size:9px;display:none;">PRO</span>
</a>
<!-- Upgrade section β€” hidden for Pro/Enterprise and DEV_MODE -->
<div id="upgradeSection">
<div class="side-label" style="margin-top:6px;">Upgrade</div>
<div class="scan-panel" style="margin-bottom:6px;">
<!-- Trial offer for free users -->
<div id="sidebarTrialOffer" style="display:none;margin-bottom:10px;padding:10px;
background:linear-gradient(135deg,rgba(0,255,163,.08),rgba(91,123,254,.06));
border:1px solid rgba(0,255,163,.2);border-radius:10px;">
<div style="font-size:11px;font-weight:700;color:#00ffa3;margin-bottom:4px;">
🎁 30-Day Free Trial
</div>
<div style="font-size:10px;color:var(--text-muted);line-height:1.5;">
Unlimited scans, full AI, PDF reports β€” no card needed.
</div>
</div>
<div style="font-size:11px;color:var(--text-muted);margin-bottom:10px;line-height:1.45;" id="upgradeDesc">
Unlock repo scanning, deep AI analysis, CVE enrichment and more.
</div>
<a href="checkout.html" class="btn btn-primary w-100" style="font-size:13px;text-decoration:none;" id="upgradeBtn">
<i class="bi bi-gift-fill me-1"></i>Start Free Trial
</a>
<div style="text-align:center;font-size:10px;color:var(--text-faint);margin-top:5px;">
Then $1.99/mo Β· Cancel anytime
</div>
</div>
</div>
</aside>
<!-- ============================================================
MAIN CONTENT
============================================================ -->
<main class="dash-main">
<!-- SCAN PANEL -->
<div class="scan-panel">
<div class="scan-panel-title">
<i class="bi bi-shield-shaded" style="color:var(--accent);"></i>
AI Code Scanner
</div>
<textarea
id="code"
class="form-control code-area"
rows="7"
placeholder="# Paste your code here…&#10;# Example:&#10;query = f&quot;SELECT * FROM users WHERE id = {user_input}&quot;&#10;eval(request.data)"
></textarea>
<!-- Scan progress -->
<div style="margin:12px 0 8px;">
<div class="scan-progress-label" id="scanProgressText"></div>
<div class="progress">
<div id="scanProgressBar" class="progress-bar" style="width:0%;"></div>
</div>
</div>
<!-- Loader -->
<div class="scan-loader" id="loader">
<div class="pulse-ring"></div>
<div style="font-size:12px;color:var(--text-muted);font-family:'JetBrains Mono',monospace;">
Analyzing…
</div>
</div>
<div class="scan-actions">
<button class="btn btn-primary flex-fill" id="codeScanBtn">
<i class="bi bi-play-circle me-1"></i>Analyze Code
</button>
<button class="btn btn-warning" id="repoScanBtn">
<i class="bi bi-github me-1"></i>Scan Repo
</button>
<button class="btn btn-outline-light" id="exportPdfBtn" title="Export PDF Report">
<i class="bi bi-file-earmark-pdf"></i>
</button>
</div>
</div>
<!-- AI INSIGHTS -->
<div id="aiInsights"></div>
<!-- VULN CARDS -->
<div id="vulnCards" class="mt-2"></div>
<!-- CHARTS ROW -->
<div class="row g-3 mt-1">
<div class="col-md-8">
<div class="scan-panel">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div class="scan-panel-title mb-0">
<i class="bi bi-bar-chart-line" style="color:var(--accent);"></i>
Usage Analytics
</div>
</div>
<div class="chart-wrap" style="padding:8px;">
<canvas id="usageChart" height="110"></canvas>
</div>
</div>
</div>
<div class="col-md-4">
<div class="scan-panel h-100" style="display:flex;flex-direction:column;">
<div class="scan-panel-title">
<i class="bi bi-pie-chart" style="color:var(--accent);"></i>
Risk Distribution
</div>
<div class="chart-wrap" style="flex:1;display:flex;align-items:center;">
<canvas id="riskChart"></canvas>
</div>
</div>
</div>
</div>
</main>
<!-- ============================================================
RIGHT ASIDE
============================================================ -->
<aside class="dash-aside">
<!-- Scans Today -->
<div class="metric">
<h6><i class="bi bi-activity me-1"></i>Scans Today</h6>
<div id="usage">
<div class="skeleton" style="height:28px;border-radius:4px;width:60px;"></div>
</div>
</div>
<!-- System Status -->
<div class="metric">
<h6><i class="bi bi-circle-fill me-1" style="color:var(--success);font-size:8px;"></i>System Status</h6>
<h3 style="color:var(--success);font-size:15px;font-family:'Syne',sans-serif;">
<span class="status-dot online"></span>All Systems Online
</h3>
</div>
<!-- Security Status -->
<div class="metric">
<h6><i class="bi bi-shield me-1"></i>Security Status</h6>
<h6 id="statusText" class="status-safe" style="margin:4px 0 0;font-family:'Syne',sans-serif;font-size:13px;">
<span class="status-dot online"></span>Secure
</h6>
</div>
<!-- Scan History -->
<div style="margin-top:14px;">
<div class="side-label" style="padding:0 0 6px;">Recent Scans</div>
<div id="history">
<div class="skeleton mb-2" style="height:38px;border-radius:8px;"></div>
<div class="skeleton mb-2" style="height:38px;border-radius:8px;"></div>
<div class="skeleton mb-2" style="height:38px;border-radius:8px;"></div>
</div>
</div>
<!-- Severity Heatmap -->
<div style="margin-top:14px;">
<div class="side-label" style="padding:0 0 6px;">Severity Heatmap</div>
<div id="heatmap" class="heatmap"></div>
<div id="heatmapEmpty" style="font-size:11px;color:var(--text-faint);padding:6px 0;display:none;">
Run a scan to see the heatmap
</div>
</div>
<!-- Quick Actions -->
<div style="margin-top:16px;display:flex;flex-direction:column;gap:6px;">
<a href="pro.html" class="btn btn-outline-light btn-sm">
<i class="bi bi-grid-3x3-gap me-1"></i>Advanced Scanner
</a>
<button class="btn btn-danger btn-sm" onclick="logout()">
<i class="bi bi-box-arrow-right me-1"></i>Logout
</button>
</div>
</aside>
</div>
<!-- SIDE DRAWER (vuln detail) -->
<div id="side" class="side-drawer">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h5 id="sideTitle" style="font-family:'Syne',sans-serif;font-size:15px;margin:0;"></h5>
<button onclick="closeSide()" style="background:none;border:none;color:var(--text-muted);font-size:22px;cursor:pointer;padding:0 4px;">
<i class="bi bi-x"></i>
</button>
</div>
<div id="sideDesc"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="api.js"></script>
<script src="app.js"></script>
<script>
// ============================================================
// HEATMAP
// ============================================================
function updateHeatmap(findings) {
const map = document.getElementById("heatmap");
const empty = document.getElementById("heatmapEmpty");
if (!map) return;
if (!findings?.length) {
map.innerHTML = "";
if (empty) empty.style.display = "block";
return;
}
if (empty) empty.style.display = "none";
window._heatmapFindings = findings;
map.innerHTML = findings.map((v, i) => `
<div class="heat-cell ${(v.severity||"low").toLowerCase()}"
title="${escHtml(v.title || "")}"
data-idx="${i}"
style="cursor:pointer;"></div>
`).join("");
map.querySelectorAll(".heat-cell").forEach(cell => {
cell.addEventListener("click", function () {
const idx = parseInt(this.dataset.idx, 10);
const item = (window._heatmapFindings || [])[idx];
if (item && typeof window.openSide === "function") window.openSide(item);
});
});
}
// Patch renderVulnerabilities to also update heatmap + risk chart
const _origRender = window.renderVulnerabilities;
window.renderVulnerabilities = function (data) {
_origRender && _origRender(data);
const list = Array.isArray(data) ? data : (data?.findings || []);
updateHeatmap(list);
window.loadRiskChart && loadRiskChart(list);
};
// ============================================================
// REPO SCAN MODAL β€” proper UX instead of browser prompt()
// ============================================================
function showRepoModal() {
document.getElementById("repoModal")?.remove();
const modal = document.createElement("div");
modal.id = "repoModal";
modal.style.cssText = `
position:fixed;inset:0;z-index:99998;display:flex;align-items:center;justify-content:center;
background:rgba(0,0,0,0.72);backdrop-filter:blur(8px);padding:16px;
`;
modal.innerHTML = `
<div style="background:var(--bg-2);border:1px solid var(--border-bright);border-radius:18px;
padding:28px 32px;max-width:460px;width:100%;
box-shadow:0 32px 80px rgba(0,0,0,0.6);
animation:popIn 0.25s cubic-bezier(0.34,1.56,0.64,1) both;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;">
<div style="font-family:'Syne',sans-serif;font-size:17px;font-weight:800;">
<i class="bi bi-github" style="color:var(--accent);margin-right:8px;"></i>Scan GitHub Repo
</div>
<button id="repoModalClose" style="background:var(--bg-3);border:1px solid var(--border);
color:var(--text-muted);border-radius:8px;width:30px;height:30px;cursor:pointer;
font-size:16px;display:flex;align-items:center;justify-content:center;">
<i class="bi bi-x"></i>
</button>
</div>
<div style="font-size:12px;color:var(--text-muted);margin-bottom:14px;line-height:1.5;">
Enter a public GitHub repository URL. SafeAIScan will clone, validate and run a full security analysis.
</div>
<div style="position:relative;margin-bottom:8px;">
<i class="bi bi-github" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);
color:var(--text-faint);font-size:14px;pointer-events:none;"></i>
<input id="repoModalInput" type="text" class="form-control" style="padding-left:36px;"
placeholder="https://github.com/owner/repo" value="https://github.com/"/>
</div>
<div style="font-size:11px;color:var(--text-faint);margin-bottom:18px;">
<i class="bi bi-info-circle me-1"></i>Only public HTTPS GitHub repos are supported.
</div>
<div style="display:flex;gap:10px;">
<button id="repoModalScan" class="btn btn-primary flex-fill" style="height:42px;font-weight:600;">
<i class="bi bi-play-circle me-1"></i>Start Scan
</button>
<button id="repoModalCancel" class="btn btn-outline-light" style="height:42px;">Cancel</button>
</div>
<div id="repoModalError" style="display:none;margin-top:10px;font-size:12px;color:var(--danger);
padding:8px 12px;background:rgba(244,63,94,0.08);border-radius:8px;
border:1px solid rgba(244,63,94,0.2);"></div>
</div>`;
document.body.appendChild(modal);
const input = document.getElementById("repoModalInput");
if (input) { input.focus(); input.setSelectionRange(input.value.length, input.value.length); }
modal.addEventListener("click", e => { if (e.target === modal) modal.remove(); });
document.getElementById("repoModalClose")?.addEventListener("click", () => modal.remove());
document.getElementById("repoModalCancel")?.addEventListener("click", () => modal.remove());
document.getElementById("repoModalScan")?.addEventListener("click", () => submitRepoScan());
input?.addEventListener("keydown", e => { if (e.key === "Enter") submitRepoScan(); });
}
window.showRepoModal = showRepoModal;
async function submitRepoScan() {
const input = document.getElementById("repoModalInput");
const errEl = document.getElementById("repoModalError");
const btn = document.getElementById("repoModalScan");
const url = input?.value?.trim();
const showErr = (msg) => { if (errEl) { errEl.textContent = msg; errEl.style.display = "block"; } };
if (!url) { showErr("Please enter a GitHub URL."); return; }
if (!url.startsWith("https://github.com/")) { showErr("Only https://github.com/ URLs are supported."); return; }
if (url.split("/").filter(Boolean).length < 4) { showErr("Please enter a full repo URL: https://github.com/owner/repo"); return; }
if (btn) { btn.disabled = true; btn.innerHTML = `<span class="spinner-border spinner-border-sm me-1"></span>Queuing…`; }
if (errEl) errEl.style.display = "none";
try {
const data = await scanRepoAPI(url);
document.getElementById("repoModal")?.remove();
const taskId = data.task_id || data?.data?.task_id;
showToast(`Repo scan queued Β· Task: ${taskId}`, "success");
pollTask(taskId);
} catch (err) {
if (btn) { btn.disabled = false; btn.innerHTML = `<i class="bi bi-play-circle me-1"></i>Start Scan`; }
if (err instanceof PlanError) {
document.getElementById("repoModal")?.remove();
showUpgradePrompt(err.message);
} else {
showErr(err.message || "Scan failed. Please try again.");
}
}
}
// ============================================================
// NAV GATING β€” respects DEV_MODE
// ============================================================
function applyNavGating(plan) {
plan = (plan || "free").toLowerCase();
if (window.DEV_MODE) plan = "pro";
// pro_trial has identical access to paid Pro β€” never show lock/PRO badges
const isPro = ["pro", "pro_trial"].includes(plan);
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";
const repoBtn = document.getElementById("repoScanBtn");
if (repoBtn) {
if (!isPro) {
repoBtn.innerHTML = `<i class="bi bi-lock me-1"></i>Scan Repo`;
repoBtn.style.opacity = "0.6";
repoBtn.title = "Requires Pro plan";
} else {
repoBtn.innerHTML = `<i class="bi bi-github me-1"></i>Scan Repo`;
repoBtn.style.opacity = "";
repoBtn.title = "";
}
}
const upgradeSection = document.getElementById("upgradeSection");
if (upgradeSection) upgradeSection.style.display = isPro ? "none" : "";
}
window.applyNavGating = applyNavGating;
// ============================================================
// BOOT β€” bind buttons + apply gating on DOMContentLoaded
// ============================================================
document.addEventListener("DOMContentLoaded", () => {
// Wire scan action buttons via addEventListener (no fragile onclick attrs)
document.getElementById("codeScanBtn")?.addEventListener("click", () => scan());
document.getElementById("exportPdfBtn")?.addEventListener("click", () => exportPDF());
const repoBtn = document.getElementById("repoScanBtn");
if (repoBtn) {
repoBtn.addEventListener("click", () => {
const plan = (localStorage.getItem("user_plan") || "free").toLowerCase();
const hasPro = window.DEV_MODE ||
["pro","pro_trial"].includes(plan) ||
localStorage.getItem("is_pro") === "true" ||
localStorage.getItem("trial_active") === "true";
if (hasPro) {
showRepoModal();
} else {
showUpgradePrompt("GitHub repo scanning is available on the Pro plan. Start your free 30-day trial to unlock it.");
}
});
}
// CVE nav gating
const navCve = document.getElementById("nav-cve");
if (navCve) {
navCve.addEventListener("click", e => {
const plan = (localStorage.getItem("user_plan") || "free").toLowerCase();
const hasPro = window.DEV_MODE ||
["pro","pro_trial"].includes(plan) ||
localStorage.getItem("is_pro") === "true" ||
localStorage.getItem("trial_active") === "true";
if (hasPro) {
// CVE lookup scrolls to the CVE panel in the dashboard
const cvePanel = document.getElementById("cvePanel") ||
document.getElementById("cveSection");
if (cvePanel) {
cvePanel.scrollIntoView({ behavior: "smooth" });
cvePanel.querySelector("input")?.focus();
} else {
// Fallback: show the CVE input in main area
if (typeof fetchCVE === "function") fetchCVE();
}
} else {
e.preventDefault();
showUpgradePrompt("CVE lookup is available on the Pro plan. Start your free 30-day trial to unlock it.");
}
});
}
// Apply nav gating from cached plan (will be refreshed by loadPlan() shortly)
const cachedPlan = window.DEV_MODE ? "pro" : (localStorage.getItem("user_plan") || "free");
applyNavGating(cachedPlan);
// Sidebar upgrade widget β€” show trial offer for free users, countdown for trial users
(function initSidebarWidget() {
const plan = (localStorage.getItem("user_plan") || "free").toLowerCase();
const trial = localStorage.getItem("trial_active") === "true";
const daysLeft = parseInt(localStorage.getItem("trial_days_left") || "30");
const offerEl = document.getElementById("sidebarTrialOffer");
const descEl = document.getElementById("upgradeDesc");
const btnEl = document.getElementById("upgradeBtn");
if (plan === "free") {
// Show the trial offer block
if (offerEl) offerEl.style.display = "block";
if (btnEl) btnEl.innerHTML = '<i class="bi bi-gift-fill me-1"></i>Start Free 30-Day Trial';
if (descEl) descEl.style.display = "none";
} else if (plan === "pro_trial" && trial) {
// Trial user β€” show countdown, keep upgrade button
if (offerEl) {
offerEl.style.display = "block";
offerEl.style.background = daysLeft <= 5
? "rgba(251,146,60,.08)" : "linear-gradient(135deg,rgba(0,255,163,.08),rgba(91,123,254,.06))";
offerEl.style.borderColor = daysLeft <= 5 ? "rgba(251,146,60,.3)" : "rgba(0,255,163,.2)";
offerEl.innerHTML =
'<div style="font-size:11px;font-weight:700;color:' + (daysLeft<=5?"var(--warning)":"#00ffa3") + ';margin-bottom:4px;">' +
(daysLeft<=5 ? "⚠ Trial ending soon!" : "βœ“ Pro Trial Active") +
'</div>' +
'<div style="font-size:10px;color:var(--text-muted);">' + daysLeft + ' day' + (daysLeft!==1?'s':'') + ' remaining</div>';
}
if (btnEl) btnEl.innerHTML = '<i class="bi bi-lightning-charge me-1"></i>Keep Pro β€” $1.99/mo';
if (descEl) descEl.style.display = "none";
}
})();
});
</script>
</body>
</html>