Spaces:
Runtime error
Runtime error
| <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 ; | |
| } | |
| /* ---- 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) ; } | |
| /* ---- 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⦠# Example: query = f"SELECT * FROM users WHERE id = {user_input}" 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> | |