SafeAIScan / frontend /pro.html
TafadzwaTaps
frontend upgrade
58b7a59
Raw
History Blame Contribute Delete
102 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>SafeAIScan Pro</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=DM+Sans:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<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>
:root { --sg:#00ffa3; --kr:#ff4d6d; }
/* ── Layout ──────────────────────────────────────────────────── */
.pro-layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 54px 1fr;
min-height: 100vh;
}
/* ── Topbar ──────────────────────────────────────────────────── */
.pro-topbar {
grid-column: 1 / -1;
background: var(--bg-1);
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
justify-content: space-between;
padding: 0 20px; gap: 12px;
position: sticky; top: 0; z-index: 200;
}
.pro-brand {
font-family: 'Syne', sans-serif;
font-size: 15px; font-weight: 800;
letter-spacing: -0.03em;
display: flex; align-items: center; gap: 8px;
text-decoration: none; color: var(--text-primary);
}
.pro-badge-tag {
font-size: 9px; font-weight: 700;
text-transform: uppercase; letter-spacing: 1.2px;
padding: 3px 8px; border-radius: 99px;
background: linear-gradient(135deg,rgba(91,123,254,.25),rgba(0,255,163,.2));
color: var(--sg); border: 1px solid rgba(0,255,163,.3);
}
.topbar-center {
flex: 1; display: flex; align-items: center;
justify-content: center; gap: 8px;
}
.scan-mode-btn {
display: flex; align-items: center; gap: 6px;
background: var(--bg-2); border: 1px solid var(--border);
color: var(--text-muted); border-radius: 8px;
padding: 7px 14px; font-size: 12px; font-weight: 500;
cursor: pointer; transition: all .2s; white-space: nowrap;
}
.scan-mode-btn.active, .scan-mode-btn:hover {
border-color: var(--accent);
background: rgba(91,123,254,.1);
color: #93aaff;
}
.scan-mode-btn.active { font-weight: 600; }
.topbar-right {
display: flex; align-items: center; gap: 10px;
}
.usage-pill {
font-size: 11px; color: var(--text-muted);
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 99px; padding: 4px 12px;
display: flex; align-items: center; gap: 5px;
}
.usage-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--sg); }
/* ── Sidebar ──────────────────────────────────────────────────── */
.pro-sidebar {
background: var(--bg-1);
border-right: 1px solid var(--border);
padding: 16px 12px;
overflow-y: auto;
display: flex; flex-direction: column; gap: 4px;
}
.side-section-label {
font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: 1px;
color: var(--text-faint); padding: 10px 8px 4px;
}
.side-nav-item {
display: flex; align-items: center; gap: 10px;
padding: 9px 10px; border-radius: 10px;
font-size: 13px; color: var(--text-muted);
cursor: pointer; transition: all .15s;
text-decoration: none; border: none; background: none;
width: 100%; text-align: left;
}
.side-nav-item:hover { background: var(--bg-2); color: var(--text-primary); }
.side-nav-item.active {
background: rgba(91,123,254,.12);
color: #93aaff; font-weight: 600;
border: 1px solid rgba(91,123,254,.2);
}
.side-nav-item i { font-size: 15px; flex-shrink: 0; }
.pro-feature-badge {
margin-left: auto; font-size: 8px; font-weight: 700;
text-transform: uppercase; letter-spacing: .5px;
padding: 2px 6px; border-radius: 4px;
background: rgba(0,255,163,.1); color: var(--sg);
border: 1px solid rgba(0,255,163,.2);
}
/* ── Main content ─────────────────────────────────────────────── */
.pro-main {
background: var(--bg-base);
overflow-y: auto;
display: flex; flex-direction: column;
}
/* ── Panels ───────────────────────────────────────────────────── */
.pro-panel { display: none; flex: 1; }
.pro-panel.active { display: flex; flex-direction: column; }
/* ── Code scanner panel ───────────────────────────────────────── */
.scanner-area {
display: grid;
grid-template-columns: 1fr 340px;
gap: 0;
flex: 1;
overflow: hidden;
}
.scanner-left {
display: flex; flex-direction: column;
border-right: 1px solid var(--border);
overflow: hidden;
}
.editor-header {
display: flex; align-items: center; gap: 10px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
background: var(--bg-1);
flex-shrink: 0;
}
.lang-select {
font-size: 12px; background: var(--bg-2);
border: 1px solid var(--border); color: var(--text-muted);
border-radius: 6px; padding: 4px 8px; cursor: pointer;
}
.editor-actions { display: flex; gap: 6px; margin-left: auto; }
.btn-sm-action {
display: flex; align-items: center; gap: 5px;
font-size: 12px; font-weight: 500;
padding: 6px 12px; border-radius: 8px;
border: none; cursor: pointer; transition: all .15s;
}
.btn-scan-primary {
background: linear-gradient(135deg, #5b7bfe, #4361ee);
color: #fff;
box-shadow: 0 4px 14px rgba(91,123,254,.35);
}
.btn-scan-primary:hover { box-shadow: 0 6px 20px rgba(91,123,254,.5); }
.btn-scan-secondary {
background: var(--bg-2); color: var(--text-muted);
border: 1px solid var(--border);
}
.btn-scan-secondary:hover { border-color: var(--accent); color: #93aaff; }
.btn-scan-success {
background: rgba(0,255,163,.1); color: var(--sg);
border: 1px solid rgba(0,255,163,.25);
}
.btn-scan-success:hover { background: rgba(0,255,163,.18); }
textarea.code-editor {
flex: 1; width: 100%; resize: none;
background: #040c18;
color: #e8edf8;
font-family: 'JetBrains Mono', monospace;
font-size: 13px; line-height: 1.65;
border: none; outline: none;
padding: 16px 20px;
min-height: 0;
}
textarea.code-editor::placeholder { color: #2a3a55; }
/* ── Scan progress bar ────────────────────────────────────────── */
.scan-progress-bar {
height: 2px; width: 0%;
background: linear-gradient(90deg, #5b7bfe, #00ffa3);
transition: width .4s ease;
flex-shrink: 0;
}
/* ── Results sidebar ──────────────────────────────────────────── */
.scanner-right {
background: var(--bg-1);
display: flex; flex-direction: column;
overflow: hidden;
}
.results-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
flex-shrink: 0;
}
.results-title {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: .8px;
color: var(--text-muted);
}
.risk-badge {
margin-left: auto; font-size: 10px; font-weight: 700;
padding: 3px 10px; border-radius: 99px;
}
.risk-none { background: rgba(52,211,153,.12); color: #34d399; }
.risk-low { background: rgba(250,204,21,.1); color: #fbbf24; }
.risk-medium { background: rgba(251,146,60,.12); color: #fb923c; }
.risk-high { background: rgba(244,63,94,.12); color: #f43f5e; }
.risk-critical { background: rgba(192,38,211,.14); color: #c026d3; }
.results-list {
flex: 1; overflow-y: auto;
padding: 8px;
}
.finding-card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 10px; padding: 12px;
margin-bottom: 8px; cursor: pointer;
transition: border-color .15s, box-shadow .15s;
animation: fadeSlide .25s ease both;
}
.finding-card:hover {
border-color: var(--border-bright);
box-shadow: 0 4px 16px rgba(0,0,0,.3);
}
@keyframes fadeSlide {
from { opacity: 0; transform: translateX(8px); }
to { opacity: 1; transform: translateX(0); }
}
.finding-type {
font-size: 12px; font-weight: 600;
color: var(--text-primary); margin-bottom: 4px;
}
.finding-meta {
font-family: 'JetBrains Mono', monospace;
font-size: 10px; color: #38bdf8;
margin-bottom: 4px;
}
.finding-match {
font-family: 'JetBrains Mono', monospace;
font-size: 10px; color: var(--text-faint);
background: rgba(4,12,24,.8);
padding: 3px 6px; border-radius: 4px;
word-break: break-all;
}
.sev-chip {
display: inline-block; font-size: 9px; font-weight: 700;
text-transform: uppercase; letter-spacing: .5px;
padding: 2px 7px; border-radius: 4px; margin-bottom: 6px;
}
.sev-CRITICAL { background: rgba(192,38,211,.16); color: #e879f9; }
.sev-HIGH { background: rgba(244,63,94,.15); color: #fb7185; }
.sev-MEDIUM { background: rgba(251,146,60,.12); color: #fdba74; }
.sev-LOW { background: rgba(250,204,21,.1); color: #fbbf24; }
/* PHASE 1: badge-pill β€” used for compliance (OWASP/NIST) and auto-fix badges */
.badge-pill {
display: inline-flex; align-items: center; gap: 4px;
font-size: 10px; font-weight: 600;
padding: 2px 8px; border-radius: 99px;
line-height: 1.6;
}
.results-empty {
padding: 32px 16px; text-align: center;
color: var(--text-faint); font-size: 13px;
}
.results-summary {
padding: 10px 16px;
border-top: 1px solid var(--border);
background: var(--bg-2);
display: flex; gap: 12px; flex-shrink: 0;
}
.sum-stat {
text-align: center; flex: 1;
}
.sum-num {
font-family: 'Syne', sans-serif;
font-size: 20px; font-weight: 800; line-height: 1;
}
.sum-lbl { font-size: 9px; text-transform: uppercase; color: var(--text-faint); margin-top: 2px; }
/* ── Repo scan panel ──────────────────────────────────────────── */
.repo-panel-inner {
max-width: 700px; margin: 40px auto;
padding: 0 24px; width: 100%;
}
.panel-card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 18px; padding: 28px;
}
.panel-card-title {
font-family: 'Syne', sans-serif;
font-size: 18px; font-weight: 800;
margin-bottom: 6px; display: flex; align-items: center; gap: 10px;
}
.panel-card-sub { font-size: 13px; color: var(--text-muted); margin-bottom: 22px; }
.url-input-wrap { position: relative; }
.url-input-wrap i {
position: absolute; left: 12px; top: 50%;
transform: translateY(-50%);
color: var(--text-faint); font-size: 14px; pointer-events: none;
}
.url-input-wrap input { padding-left: 36px !important; }
/* Task progress */
.task-progress {
display: none; margin-top: 16px;
padding: 16px; background: var(--bg-1);
border: 1px solid var(--border); border-radius: 12px;
}
.task-progress.show { display: block; }
.task-state-label {
font-size: 12px; font-weight: 600;
color: var(--text-primary); margin-bottom: 8px;
display: flex; align-items: center; gap: 6px;
}
.progress-track {
height: 6px; background: var(--bg-3);
border-radius: 99px; overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #5b7bfe, #00ffa3);
border-radius: 99px;
transition: width .5s ease;
}
/* ── ZIP panel ────────────────────────────────────────────────── */
.zip-drop-zone {
border: 2px dashed var(--border-bright);
border-radius: 16px; padding: 48px;
text-align: center; cursor: pointer;
transition: all .2s;
background: var(--bg-1);
}
.zip-drop-zone:hover, .zip-drop-zone.drag-over {
border-color: var(--sg);
background: rgba(0,255,163,.04);
}
.zip-icon {
font-size: 48px; color: var(--text-faint);
margin-bottom: 12px; display: block;
}
.zip-title { font-size: 16px; font-weight: 600; margin-bottom: 6px; }
.zip-sub { font-size: 13px; color: var(--text-muted); }
input[type=file]#zipInput { display: none; }
/* ── CVE lookup panel ─────────────────────────────────────────── */
.cve-result-card {
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 10px; padding: 14px; margin-bottom: 10px;
animation: fadeSlide .2s ease both;
}
.cve-id { font-size: 13px; font-weight: 700; color: #38bdf8; }
.cve-score-badge {
font-size: 10px; font-weight: 700;
padding: 2px 8px; border-radius: 99px; float: right;
}
.cve-desc { font-size: 12px; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }
/* ── AI Analysis panel ────────────────────────────────────────── */
.ai-panel-inner {
max-width: 800px; margin: 0 auto; padding: 24px;
width: 100%;
}
.ai-input-section {
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 14px; overflow: hidden; margin-bottom: 20px;
}
.ai-result-section {
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 14px; padding: 20px;
}
.ai-explanation {
font-size: 14px; color: var(--text-muted);
line-height: 1.7; margin-bottom: 16px;
}
.ai-fix-item {
display: flex; gap: 10px; align-items: flex-start;
margin-bottom: 10px; padding: 10px 12px;
background: rgba(91,123,254,.06);
border-left: 3px solid var(--accent);
border-radius: 0 8px 8px 0;
}
.fix-num {
background: var(--accent); color: #fff;
font-size: 10px; font-weight: 700;
width: 18px; height: 18px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; margin-top: 1px;
}
.fix-text { font-size: 13px; color: var(--text-muted); line-height: 1.6; }
/* ── PDF / Report panel ───────────────────────────────────────── */
.reports-grid {
display: grid; gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
padding: 24px;
}
.report-card {
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 14px; padding: 18px;
display: flex; flex-direction: column; gap: 10px;
transition: border-color .2s;
}
.report-card:hover { border-color: var(--border-bright); }
.report-meta { font-size: 11px; color: var(--text-faint); }
.report-risk { font-size: 12px; font-weight: 700; }
.report-source {
font-family: 'JetBrains Mono', monospace;
font-size: 11px; color: #38bdf8;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ── Log strip ────────────────────────────────────────────────── */
.log-strip {
padding: 4px 16px;
background: #040c18;
border-top: 1px solid rgba(99,130,255,.08);
font-family: 'JetBrains Mono', monospace;
font-size: 11px; color: var(--text-faint);
flex-shrink: 0;
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.log-ok { color: var(--sg); }
.log-warn { color: var(--warning); }
.log-err { color: var(--danger); }
/* ── Toast ────────────────────────────────────────────────────── */
.toast-area {
position: fixed; bottom: 20px; right: 20px;
z-index: 9999; display: flex;
flex-direction: column; gap: 8px; align-items: flex-end;
}
.toast-item {
padding: 10px 16px; border-radius: 10px;
font-size: 13px; font-family: 'DM Sans', sans-serif;
font-weight: 500; backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0,0,0,.4);
display: flex; align-items: center; gap: 8px;
animation: toastIn .25s ease both;
cursor: pointer; max-width: 340px;
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(12px) scale(.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.toast-success { background: rgba(52,211,153,.12); color: #34d399; border: 1px solid rgba(52,211,153,.25); }
.toast-error { background: rgba(244,63,94,.12); color: #f43f5e; border: 1px solid rgba(244,63,94,.25); }
.toast-info { background: rgba(91,123,254,.12); color: #93aaff; border: 1px solid rgba(91,123,254,.25); }
.toast-warning { background: rgba(251,146,60,.12); color: #fdba74; border: 1px solid rgba(251,146,60,.25); }
/* ── Detail drawer ────────────────────────────────────────────── */
.detail-drawer {
position: fixed; right: 0; top: 54px; bottom: 0;
width: 420px; background: var(--bg-1);
border-left: 1px solid var(--border-bright);
z-index: 300; transform: translateX(100%);
transition: transform .3s cubic-bezier(.4,0,.2,1);
overflow-y: auto; padding: 20px;
display: flex; flex-direction: column; gap: 14px;
}
.detail-drawer.open { transform: translateX(0); }
.detail-drawer { touch-action: pan-y; }
.drawer-close {
position: absolute; top: 12px; right: 14px;
background: var(--bg-3); border: 1px solid var(--border);
color: var(--text-muted); border-radius: 8px;
width: 28px; height: 28px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 16px; line-height: 1;
}
.drawer-field-label {
font-size: 10px; text-transform: uppercase;
letter-spacing: .8px; color: var(--text-faint);
margin-bottom: 4px; font-weight: 600;
}
.drawer-field-value {
font-size: 13px; color: var(--text-muted);
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 8px; padding: 10px 12px; line-height: 1.6;
}
.drawer-fix-box {
background: rgba(91,123,254,.08);
border: 1px solid rgba(91,123,254,.2);
border-radius: 8px; padding: 12px;
font-size: 13px; color: var(--text-muted); line-height: 1.6;
}
/* ════════════════════════════════════════════════════════════
MOBILE & TABLET OPTIMIZATION β€” "On the go coders"
Tiers: β‰₯1025px desktop (unchanged) Β· 769-1024px tablet Β·
481-768px phone-landscape/small-tablet Β· ≀480px phone
All additive β€” desktop layout above is untouched.
═══════════════════════════════════════════════════════════════ */
/* ── Mobile menu button (hidden on desktop) ─────────────────── */
.mobile-menu-btn {
display: none;
background: var(--bg-2);
border: 1px solid var(--border);
color: var(--text-muted);
border-radius: 8px;
width: 36px; height: 36px;
align-items: center; justify-content: center;
font-size: 18px; cursor: pointer;
flex-shrink: 0;
}
.mobile-sidebar-close { display: none; }
.mobile-sidebar-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(4,12,24,.6);
backdrop-filter: blur(2px);
z-index: 250;
opacity: 0;
transition: opacity .25s ease;
}
.mobile-sidebar-overlay.show {
display: block;
opacity: 1;
}
body.mobile-nav-open { overflow: hidden; }
/* ════════════════════════════════════════════════════════════
TABLET TIER β€” 769px to 1024px
Narrower sidebar, narrower results column, larger touch targets
═══════════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {
.pro-layout { grid-template-columns: 76px 1fr; }
.pro-sidebar { padding: 12px 6px; }
.side-section-label { display: none; }
.side-nav-item {
justify-content: center;
padding: 12px 6px;
flex-direction: column;
gap: 4px;
font-size: 9px;
text-align: center;
}
.side-nav-item i { font-size: 18px; }
.side-nav-item span:not(.pro-feature-badge) { display: none; }
.pro-feature-badge { position: absolute; top: 4px; right: 4px; }
.side-nav-item { position: relative; }
#sidebarTrialCTA, #sidebarEmail, #sidebarEmail + a,
div[style*="Signed in as"] { display: none !important; }
.scanner-area { grid-template-columns: 1fr 300px; }
.topbar-center { gap: 4px; }
.scan-mode-btn {
padding: 7px 10px;
font-size: 11px;
}
.scan-mode-btn i { font-size: 13px; }
.detail-drawer { width: 360px; }
.ai-panel-inner, .repo-panel-inner { padding: 0 20px; }
}
/* ════════════════════════════════════════════════════════════
PHONE / SMALL TABLET TIER β€” ≀768px
Sidebar becomes slide-out drawer, topbar mode tabs scroll
horizontally, scanner stacks vertically, drawer goes full-screen
═══════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
html, body { overflow-x: hidden; }
/* ── Layout: single column, sidebar off-canvas ── */
.pro-layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
/* ── Topbar ── */
.pro-topbar {
padding: 0 10px;
gap: 8px;
height: 52px;
flex-wrap: nowrap;
}
.mobile-menu-btn { display: flex; }
.pro-brand { font-size: 13px; gap: 6px; }
.pro-brand span:first-of-type { display: none; } /* hide "SafeAIScan" text wordmark, keep icon+badge */
.pro-brand .pro-badge-tag { display: none; }
/* Topbar mode tabs: horizontal scroll instead of center-flex/wrap */
.topbar-center {
position: absolute;
left: 0; right: 0; top: 52px;
flex: none;
justify-content: flex-start;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
gap: 6px;
padding: 8px 10px;
background: var(--bg-1);
border-bottom: 1px solid var(--border);
scrollbar-width: none;
}
.topbar-center::-webkit-scrollbar { display: none; }
.scan-mode-btn {
flex-shrink: 0;
scroll-snap-align: start;
padding: 8px 12px;
font-size: 12px;
min-height: 40px;
}
/* Push main content down to clear the now-wrapped topbar+tabs */
.pro-layout { grid-template-rows: 96px 1fr; }
.topbar-right { gap: 6px; }
.usage-pill { font-size: 10px; padding: 4px 8px; }
.usage-pill span:not(.usage-dot) { max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hide-on-mobile { display: none !important; }
/* ── Sidebar becomes a slide-out drawer ── */
.pro-sidebar {
position: fixed;
top: 0; bottom: 0; left: 0;
width: 78vw;
max-width: 300px;
z-index: 260;
transform: translateX(-100%);
transition: transform .25s ease;
box-shadow: 8px 0 32px rgba(0,0,0,.5);
padding-top: 56px;
}
.pro-sidebar.mobile-open { transform: translateX(0); }
.mobile-sidebar-close {
display: flex;
position: absolute; top: 10px; right: 10px;
background: var(--bg-3); border: 1px solid var(--border);
color: var(--text-muted); border-radius: 8px;
width: 32px; height: 32px;
align-items: center; justify-content: center;
font-size: 14px; cursor: pointer;
}
.side-section-label { display: block; }
.side-nav-item {
flex-direction: row;
justify-content: flex-start;
font-size: 14px;
padding: 12px 10px;
min-height: 44px;
}
.side-nav-item i { font-size: 16px; }
.side-nav-item span:not(.pro-feature-badge) { display: inline; }
#sidebarTrialCTA, #sidebarEmail, div[style*="Signed in as"] { display: block !important; }
/* ── Scanner area: stack editor above results ── */
.scanner-area {
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
overflow: visible;
}
.scanner-left {
border-right: none;
border-bottom: 1px solid var(--border);
min-height: 280px;
}
.scanner-right {
height: auto;
max-height: 60vh;
}
textarea.code-editor {
font-size: 16px; /* prevents iOS auto-zoom on focus */
padding: 12px 14px;
min-height: 220px;
}
/* ── Editor header: wrap actions onto their own row ── */
.editor-header {
flex-wrap: wrap;
gap: 8px;
padding: 8px 12px;
}
.editor-actions {
width: 100%;
margin-left: 0;
justify-content: flex-end;
}
.btn-sm-action {
padding: 8px 12px;
font-size: 12px;
min-height: 40px;
}
.lang-select { min-height: 36px; }
/* ── Results summary: 2x2 grid instead of 1x4 row ── */
.results-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 12px;
}
.sum-stat {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 4px;
}
/* ── Security score card ── */
#securityScoreCard { padding: 12px; }
/* ── Repo health: 2-column grid on phones ── */
#repoHealthCard div[style*="grid-template-columns:repeat(auto-fit"],
div[style*="grid-template-columns:repeat(auto-fit,minmax(90px"] {
grid-template-columns: repeat(2, 1fr) !important;
}
/* ── Detail drawer: full screen with swipe handle ── */
.detail-drawer {
top: 0;
width: 100%;
padding: 20px 16px 24px;
border-left: none;
}
.detail-drawer::before {
content: '';
display: block;
width: 40px; height: 4px;
background: var(--border-bright);
border-radius: 99px;
margin: 0 auto 12px;
}
.drawer-close {
width: 36px; height: 36px;
top: 16px; right: 16px;
font-size: 18px;
}
/* AI auto-fix before/after: stack into single column */
.vuln-details > div[style*="grid-template-columns:1fr 1fr"],
div[style*="display:grid;grid-template-columns:1fr 1fr;gap:8px"] {
grid-template-columns: 1fr !important;
}
/* ── Repo / ZIP / AI / CVE panels: full-width with safe padding ── */
.repo-panel-inner, .ai-panel-inner {
max-width: 100%;
margin: 16px auto;
padding: 0 14px;
}
.panel-card { padding: 18px; border-radius: 14px; }
.zip-drop-zone { padding: 28px 16px; }
.zip-icon { font-size: 36px; }
/* ── Reports grid: single column ── */
.reports-grid {
grid-template-columns: 1fr;
padding: 14px;
}
/* ── CVE / dependency tables: horizontal scroll wrapper ── */
.dep-table { display: block; overflow-x: auto; white-space: nowrap; }
/* ── Log strip: allow wrap, smaller font ── */
.log-strip { font-size: 10px; padding: 4px 12px; }
/* ── Toasts: full width, anchored to bottom ── */
.toast-area {
left: 12px; right: 12px; bottom: 12px;
align-items: stretch;
}
.toast-item { max-width: none; width: 100%; }
/* ── Touch target floor for any interactive element ── */
button, .btn, a.btn, select, input[type="text"], input[type="url"],
input[type="email"], input[type="password"], input[type="file"] + label {
min-height: 40px;
}
}
/* ════════════════════════════════════════════════════════════
SMALL PHONE TIER β€” ≀480px
Tighter spacing, single-column everything, smaller type scale
═══════════════════════════════════════════════════════════════ */
@media (max-width: 480px) {
.pro-brand { font-size: 12px; }
.pro-layout { grid-template-rows: 90px 1fr; }
.topbar-center { top: 48px; padding: 6px 8px; }
.pro-topbar { height: 48px; }
.scan-mode-btn {
padding: 7px 10px;
font-size: 11px;
}
.scan-mode-btn span, .scan-mode-btn { white-space: nowrap; }
.usage-pill span:not(.usage-dot) { max-width: 56px; }
textarea.code-editor { font-size: 15px; padding: 10px 12px; }
.results-summary { grid-template-columns: 1fr 1fr; gap: 6px; padding: 8px; }
.sum-num { font-size: 17px; }
#securityScoreCard svg { width: 48px; height: 48px; }
#repoHealthCard div[style*="grid-template-columns:repeat(auto-fit"],
div[style*="grid-template-columns:repeat(auto-fit,minmax(90px"] {
grid-template-columns: repeat(2, 1fr) !important;
}
.panel-card { padding: 14px; border-radius: 12px; }
.panel-card-title { font-size: 15px; }
.results-list { padding: 6px; }
.finding-card { padding: 10px; }
.drawer-fix-box, .drawer-field-value { font-size: 12px; padding: 8px 10px; }
.editor-actions { gap: 4px; }
.btn-sm-action { padding: 8px 10px; font-size: 11px; }
.btn-sm-action span { display: none; } /* icon-only buttons on tiny screens */
.btn-sm-action i { font-size: 14px; }
}
/* ════════════════════════════════════════════════════════════
LANDSCAPE PHONES β€” short viewport height
═══════════════════════════════════════════════════════════════ */
@media (max-width: 900px) and (orientation: landscape) and (max-height: 500px) {
.pro-layout { grid-template-rows: 48px 1fr; }
.topbar-center {
position: static;
top: auto;
flex: 1;
justify-content: center;
border-bottom: none;
padding: 0;
}
.pro-topbar { height: 48px; flex-wrap: nowrap; }
.scanner-left { min-height: 180px; }
.scanner-right { max-height: 40vh; }
textarea.code-editor { min-height: 140px; }
}
/* ════════════════════════════════════════════════════════════
TOUCH DEVICES β€” applies regardless of screen size
═══════════════════════════════════════════════════════════════ */
@media (pointer: coarse) {
.scan-mode-btn, .side-nav-item, .btn-sm-action,
.finding-card, .drawer-close, .mobile-menu-btn,
.copy-btn, .seats-btn, .method-tab {
min-height: 40px;
}
.finding-card { padding: 14px 12px; }
/* Slightly larger tap targets for chevrons / icon-only buttons */
.drawer-close, .mobile-menu-btn, .mobile-sidebar-close {
width: 40px; height: 40px;
}
}
</style>
</head>
<body>
<!-- Auth guard: allow pro AND active trial users; redirect only expired/free -->
<script>
(function() {
var token = localStorage.getItem('access_token');
var isPro = localStorage.getItem('is_pro') === 'true';
var plan = localStorage.getItem('user_plan') || 'free';
var trial = localStorage.getItem('trial_active') === 'true';
if (!token || token === 'undefined') {
window.location.replace('login.html');
return;
}
// Allow: paid pro, active trial β€” block only plain free
var hasAccess = isPro || trial || plan === 'pro' || plan === 'pro_trial' || plan === 'enterprise';
if (!hasAccess) {
window.location.replace('checkout.html?reason=pro_required');
}
})();
</script>
<div class="pro-layout">
<!-- ── TOPBAR ─────────────────────────────────────────────── -->
<header class="pro-topbar">
<button class="mobile-menu-btn" id="mobileMenuBtn" onclick="toggleMobileSidebar()" aria-label="Open menu">
<i class="bi bi-list"></i>
</button>
<a href="dashboard.html" class="pro-brand" style="text-decoration:none;">
<i class="bi bi-shield-lock-fill" style="color:var(--sg);font-size:16px;"></i>
<span><span style="color:var(--accent);">Safe</span><span>AI</span><span style="color:var(--sg);">Scan</span></span>
<span class="pro-badge-tag">PRO</span>
</a>
<div class="topbar-center">
<button class="scan-mode-btn active" id="mode-code" onclick="setMode('code')">
<i class="bi bi-code-slash"></i> Code Scan
</button>
<button class="scan-mode-btn" id="mode-zip" onclick="setMode('zip')">
<i class="bi bi-file-zip"></i> ZIP Upload
</button>
<button class="scan-mode-btn" id="mode-repo" onclick="setMode('repo')">
<i class="bi bi-github"></i> Repo Scan
</button>
<button class="scan-mode-btn" id="mode-ai" onclick="setMode('ai')">
<i class="bi bi-cpu"></i> AI Analysis
</button>
<button class="scan-mode-btn" id="mode-cve" onclick="setMode('cve')">
<i class="bi bi-bug"></i> CVE Lookup
</button>
<button class="scan-mode-btn" id="mode-reports" onclick="setMode('reports')">
<i class="bi bi-file-earmark-pdf"></i> Reports
</button>
</div>
<div class="topbar-right">
<div class="usage-pill">
<span class="usage-dot"></span>
<span id="usagePill">Unlimited scans</span>
</div>
<a href="dashboard.html" class="btn-sm-action btn-scan-secondary hide-on-mobile" style="text-decoration:none;font-size:12px;padding:6px 12px;border-radius:8px;">
<i class="bi bi-grid"></i> Dashboard
</a>
<button class="btn-sm-action btn-scan-secondary" onclick="doLogout()" style="padding:6px 10px;">
<i class="bi bi-box-arrow-right"></i>
</button>
</div>
</header>
<!-- ── MOBILE SIDEBAR OVERLAY ─────────────────────────────── -->
<div class="mobile-sidebar-overlay" id="mobileSidebarOverlay" onclick="closeMobileSidebar()"></div>
<!-- ── SIDEBAR ────────────────────────────────────────────── -->
<aside class="pro-sidebar" id="proSidebar">
<button class="mobile-sidebar-close" id="mobileSidebarClose" onclick="closeMobileSidebar()" aria-label="Close menu">
<i class="bi bi-x-lg"></i>
</button>
<div class="side-section-label">Scanner</div>
<button class="side-nav-item active" id="sb-code" onclick="setMode('code')"><i class="bi bi-code-slash"></i>Code Scanner</button>
<button class="side-nav-item" id="sb-zip" onclick="setMode('zip')"><i class="bi bi-file-zip"></i>ZIP Upload<span class="pro-feature-badge">PRO</span></button>
<button class="side-nav-item" id="sb-repo" onclick="setMode('repo')"><i class="bi bi-github"></i>Repo Scan<span class="pro-feature-badge">PRO</span></button>
<div class="side-section-label">Intelligence</div>
<button class="side-nav-item" id="sb-ai" onclick="setMode('ai')"><i class="bi bi-cpu"></i>Deep AI Analysis<span class="pro-feature-badge">PRO</span></button>
<button class="side-nav-item" id="sb-cve" onclick="setMode('cve')"><i class="bi bi-bug"></i>CVE Lookup<span class="pro-feature-badge">PRO</span></button>
<div class="side-section-label">Reports</div>
<button class="side-nav-item" id="sb-reports" onclick="setMode('reports')"><i class="bi bi-file-earmark-pdf"></i>PDF Reports<span class="pro-feature-badge">PRO</span></button>
<div style="margin-top:auto; padding-top:16px; border-top:1px solid var(--border);">
<!-- Trial upgrade CTA β€” shown only when on trial -->
<div id="sidebarTrialCTA" style="display:none;margin-bottom:12px;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:10px;font-weight:700;color:#00ffa3;text-transform:uppercase;letter-spacing:.7px;margin-bottom:4px;">
<i class="bi bi-gift-fill me-1"></i>Free Trial Active
</div>
<div id="sidebarTrialDays" style="font-size:11px;color:var(--text-muted);margin-bottom:8px;line-height:1.5;">
Enjoying all Pro features for free.
</div>
<a href="checkout.html" style="display:block;text-align:center;background:linear-gradient(135deg,#5b7bfe,#4361ee);color:#fff;border-radius:8px;padding:7px;font-size:11px;font-weight:600;text-decoration:none;">
Upgrade β€” $1.99/mo
</a>
</div>
<div style="font-size:11px;color:var(--text-faint);padding:8px 8px 4px;">Signed in as</div>
<div id="sidebarEmail" style="font-size:12px;color:var(--text-muted);padding:0 8px 8px;word-break:break-all;"></div>
<a href="index.html" class="side-nav-item"><i class="bi bi-arrow-left"></i>Back to Dashboard</a>
</div>
</aside>
<!-- ── MAIN ───────────────────────────────────────────────── -->
<main class="pro-main">
<!-- ════════════════════════════════════════════════════════
PANEL 1: CODE SCANNER
═══════════════════════════════════════════════════════════ -->
<div class="pro-panel active" id="panel-code">
<div class="scan-progress-bar" id="progressBar"></div>
<div class="scanner-area">
<div class="scanner-left">
<div class="editor-header">
<select class="lang-select" id="langSelect">
<option>Python</option><option>JavaScript</option><option>TypeScript</option>
<option>Go</option><option>Java</option><option>Ruby</option>
<option>PHP</option><option>Shell</option><option>YAML</option>
<option>JSON</option><option>Terraform</option><option>Other</option>
</select>
<span style="font-size:11px;color:var(--text-faint);">Paste code to scan for secrets</span>
<div class="editor-actions">
<button class="btn-sm-action btn-scan-secondary" onclick="clearCode()">
<i class="bi bi-trash"></i> Clear
</button>
<button class="btn-sm-action btn-scan-secondary" id="aiOnlyBtn" onclick="runAIOnly()">
<i class="bi bi-cpu"></i> AI Only
</button>
<button class="btn-sm-action btn-scan-primary" id="scanBtn" onclick="runCodeScan()">
<i class="bi bi-play-circle-fill"></i> Run Scan
</button>
</div>
</div>
<textarea class="code-editor" id="codeInput"
placeholder="# Paste your code here...&#10;&#10;# Example of what we detect:&#10;OPENAI_API_KEY = &quot;sk-abc123...&quot;&#10;GITHUB_TOKEN = &quot;ghp_xyz...&quot;&#10;AWS_ACCESS_KEY_ID = &quot;AKIA...&quot;"></textarea>
<div class="log-strip" id="logStrip">
<span class="log-ok">βœ“</span> SafeAIScan Pro ready β€” all features unlocked
</div>
</div>
<div class="scanner-right">
<!-- PHASE 1: Security Score (weighted scoring engine) -->
<div id="securityScoreCard" style="display:none;background:var(--bg-2);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:14px;">
<div style="position:relative;width:56px;height:56px;flex-shrink:0;">
<svg width="56" height="56" viewBox="0 0 56 56" style="transform:rotate(-90deg);">
<circle cx="28" cy="28" r="24" fill="none" stroke="var(--bg-3)" stroke-width="5"/>
<circle id="scoreRing" cx="28" cy="28" r="24" fill="none" stroke="#facc15" stroke-width="5"
stroke-dasharray="150.8" stroke-dashoffset="150.8" stroke-linecap="round"/>
</svg>
<div id="scoreRingValue" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:'Syne',sans-serif;font-weight:800;font-size:16px;">β€”</div>
</div>
<div>
<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.7px;color:var(--text-faint);">Security Score</div>
<div id="scoreRiskLabel" style="font-family:'Syne',sans-serif;font-weight:800;font-size:15px;">β€”</div>
</div>
</div>
</div>
<div class="results-header">
<span class="results-title">Findings</span>
<button onclick="exportCurrentPDF()" style="background:none;border:none;color:var(--text-faint);font-size:11px;cursor:pointer;display:flex;align-items:center;gap:4px;" title="Download PDF">
<i class="bi bi-file-earmark-pdf"></i> PDF
</button>
<button onclick="exportExecutivePDF()" style="background:none;border:none;color:var(--text-faint);font-size:11px;cursor:pointer;display:flex;align-items:center;gap:4px;" title="Download Executive Report">
<i class="bi bi-file-earmark-richtext"></i> Executive
</button>
<span class="risk-badge risk-none" id="riskBadge">NONE</span>
</div>
<div class="results-list" id="resultsList">
<div class="results-empty">
<i class="bi bi-shield-check" style="font-size:32px;color:var(--text-faint);display:block;margin-bottom:8px;"></i>
Run a scan to see findings
</div>
</div>
<div class="results-summary" id="resultsSummary">
<div class="sum-stat">
<div class="sum-num" id="sumTotal" style="color:var(--text-primary);">β€”</div>
<div class="sum-lbl">Total</div>
</div>
<div class="sum-stat">
<div class="sum-num" id="sumHigh" style="color:#fb7185;">β€”</div>
<div class="sum-lbl">HIGH</div>
</div>
<div class="sum-stat">
<div class="sum-num" id="sumMed" style="color:#fdba74;">β€”</div>
<div class="sum-lbl">MED</div>
</div>
<div class="sum-stat">
<div class="sum-num" id="sumLow" style="color:#fbbf24;">β€”</div>
<div class="sum-lbl">LOW</div>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
PANEL 2: ZIP UPLOAD
═══════════════════════════════════════════════════════════ -->
<div class="pro-panel" id="panel-zip">
<div class="scan-progress-bar" id="zipProgressBar"></div>
<div class="repo-panel-inner">
<div class="panel-card">
<div class="panel-card-title">
<i class="bi bi-file-zip" style="color:var(--sg);"></i>
ZIP File Upload
</div>
<div class="panel-card-sub">
Upload a ZIP archive of your project. We extract it, scan every eligible file for secrets, and return full results β€” nothing is stored.
</div>
<input type="file" id="zipInput" accept=".zip">
<div class="zip-drop-zone" id="dropZone" onclick="document.getElementById('zipInput').click()">
<i class="bi bi-file-zip zip-icon"></i>
<div class="zip-title">Drop your ZIP here</div>
<div class="zip-sub">or click to browse &mdash; max 50 MB, up to 1,000 files</div>
</div>
<div class="task-progress" id="zipProgress">
<div class="task-state-label">
<span class="spinner-border spinner-border-sm" style="border-color:var(--sg);border-right-color:transparent;"></span>
<span id="zipStateLabel">Scanning…</span>
</div>
<div class="progress-track">
<div class="progress-fill" id="zipProgressFill" style="width:0%;"></div>
</div>
</div>
</div>
<!-- ZIP results appear here -->
<div id="zipResults" style="margin-top:16px;"></div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
PANEL 3: REPO SCAN
═══════════════════════════════════════════════════════════ -->
<div class="pro-panel" id="panel-repo">
<div class="repo-panel-inner">
<div class="panel-card">
<div class="panel-card-title">
<i class="bi bi-github" style="color:var(--sg);"></i>
GitHub Repository Scan
</div>
<div class="panel-card-sub">
Paste a public GitHub repository URL. SecretScan clones it (depth=1), scans every file, then deletes the clone. Your code is never stored.
</div>
<div class="url-input-wrap mb-3">
<i class="bi bi-github"></i>
<input id="repoUrl" type="text" class="form-control"
placeholder="https://github.com/owner/repo"
value="https://github.com/">
</div>
<button class="btn-sm-action btn-scan-primary w-100" style="justify-content:center;padding:12px;"
onclick="runRepoScan()">
<i class="bi bi-play-circle-fill"></i> Start Repo Scan
</button>
<div class="task-progress" id="repoProgress" style="margin-top:16px;">
<div class="task-state-label">
<span class="spinner-border spinner-border-sm" style="border-color:var(--sg);border-right-color:transparent;"></span>
<span id="repoStateLabel">Queued…</span>
</div>
<div class="progress-track">
<div class="progress-fill" id="repoProgressFill" style="width:0%;"></div>
</div>
<div id="repoProgressMsg" style="font-size:11px;color:var(--text-faint);margin-top:6px;"></div>
</div>
</div>
<div id="repoResults" style="margin-top:16px;"></div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
PANEL 4: DEEP AI ANALYSIS
═══════════════════════════════════════════════════════════ -->
<div class="pro-panel" id="panel-ai">
<div class="ai-panel-inner">
<div style="margin-bottom:16px;">
<div style="font-family:'Syne',sans-serif;font-weight:800;font-size:18px;margin-bottom:4px;">
<i class="bi bi-cpu" style="color:var(--sg);margin-right:8px;"></i>Deep AI Analysis
</div>
<div style="font-size:13px;color:var(--text-muted);">
Full AI reasoning over your code β€” attack surface mapping, severity prediction, OWASP categorisation, and actionable fix steps.
</div>
</div>
<div class="ai-input-section">
<textarea id="aiCodeInput" style="width:100%;background:#040c18;border:none;outline:none;padding:16px;font-family:'JetBrains Mono',monospace;font-size:13px;color:#e8edf8;min-height:200px;resize:vertical;border-radius:14px 14px 0 0;"
placeholder="Paste code for deep AI security analysis…"></textarea>
<div style="padding:10px 14px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end;">
<button class="btn-sm-action btn-scan-primary" id="aiAnalyzeBtn" onclick="runDeepAI()">
<i class="bi bi-cpu"></i> Analyse with AI
</button>
</div>
</div>
<div class="ai-result-section" id="aiResult" style="display:none;">
<div style="font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--sg);font-weight:700;margin-bottom:12px;">
<i class="bi bi-cpu me-1"></i>AI Security Report
</div>
<div class="ai-explanation" id="aiExplanation"></div>
<div style="font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--accent);font-weight:700;margin-bottom:8px;" id="aiFixesLabel" style="display:none;">
<i class="bi bi-wrench-adjustable me-1"></i>Recommended Fixes
</div>
<div id="aiFixesList"></div>
<div id="aiRiskRow" style="margin-top:12px;font-size:12px;color:var(--text-muted);display:none;">
<i class="bi bi-shield-exclamation" style="color:var(--warning);"></i>
Predicted severity: <strong id="aiSeverity" style="color:var(--warning);"></strong>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
PANEL 5: CVE LOOKUP
═══════════════════════════════════════════════════════════ -->
<div class="pro-panel" id="panel-cve">
<div class="repo-panel-inner">
<div class="panel-card">
<div class="panel-card-title">
<i class="bi bi-bug" style="color:#fb7185;"></i>
CVE Lookup
</div>
<div class="panel-card-sub">
Search the NVD CVE database by CVE ID or keyword. Results show CVSS score, severity, and a description.
</div>
<div style="display:flex;gap:8px;">
<input id="cveInput" type="text" class="form-control"
placeholder="CVE-2024-12345 or sql injection"
onkeydown="if(event.key==='Enter') runCVELookup()">
<button class="btn-sm-action btn-scan-primary" onclick="runCVELookup()" style="white-space:nowrap;flex-shrink:0;">
<i class="bi bi-search"></i> Search
</button>
</div>
<div id="cveResults" style="margin-top:16px;"></div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
PANEL 6: PDF REPORTS
═══════════════════════════════════════════════════════════ -->
<div class="pro-panel" id="panel-reports">
<div style="padding:20px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;">
<div>
<div style="font-family:'Syne',sans-serif;font-weight:800;font-size:18px;">
<i class="bi bi-file-earmark-pdf" style="color:#c026d3;margin-right:8px;"></i>Scan History &amp; Reports
</div>
<div style="font-size:12px;color:var(--text-muted);margin-top:2px;">Download PDF reports for any completed scan</div>
</div>
<button class="btn-sm-action btn-scan-secondary" onclick="loadReports()">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
<!-- PHASE 1: Security Trend Analytics -->
<div style="padding:20px 24px;border-bottom:1px solid var(--border);">
<div style="font-family:'Syne',sans-serif;font-weight:800;font-size:15px;margin-bottom:4px;">
<i class="bi bi-graph-up-arrow me-2" style="color:#22c55e;"></i>Security Score Trend
</div>
<div style="font-size:12px;color:var(--text-muted);margin-bottom:12px;">Last 30 days</div>
<div style="position:relative;min-height:180px;">
<canvas id="securityTrendChart" height="80"></canvas>
<div id="securityTrendEmpty" style="display:none;align-items:center;justify-content:center;flex-direction:column;gap:8px;min-height:180px;color:var(--text-faint);font-size:12px;position:absolute;inset:0;">
<i class="bi bi-graph-up" style="font-size:28px;"></i>
Run a few scans to see your security score trend
</div>
</div>
<div class="trend-stats-row">
<span>Average: <strong id="trendAvgScore">β€”</strong></span>
<span>Best: <strong id="trendBestScore">β€”</strong></span>
<span>Worst: <strong id="trendWorstScore">β€”</strong></span>
</div>
</div>
<div class="reports-grid" id="reportsGrid">
<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--text-faint);">
<i class="bi bi-file-earmark-pdf" style="font-size:36px;display:block;margin-bottom:10px;"></i>
Loading your scan history…
</div>
</div>
</div>
</main>
</div>
<!-- Detail Drawer -->
<div class="detail-drawer" id="detailDrawer">
<button class="drawer-close" onclick="closeDrawer()">&#x2715;</button>
<div style="font-family:'Syne',sans-serif;font-weight:800;font-size:16px;margin-top:4px;" id="drawerTitle">Finding Details</div>
<div>
<div class="drawer-field-label">Type</div>
<div class="drawer-field-value" id="drawerType"></div>
</div>
<div>
<div class="drawer-field-label">Severity</div>
<div id="drawerSev"></div>
</div>
<div>
<div class="drawer-field-label">Location</div>
<div class="drawer-field-value" id="drawerFile" style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#38bdf8;"></div>
</div>
<div id="drawerMatchWrap">
<div class="drawer-field-label">Redacted Match</div>
<div class="drawer-field-value" id="drawerMatch" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#fb7185;"></div>
</div>
<div>
<div class="drawer-field-label">Description</div>
<div class="drawer-field-value" id="drawerDesc"></div>
</div>
<div>
<div class="drawer-field-label">&#x1F527; Remediation</div>
<div class="drawer-fix-box" id="drawerFix"></div>
</div>
<!-- PHASE 1: Compliance mapping (OWASP / NIST) -->
<div id="drawerCompliance" style="display:none;margin-top:6px;">
<div class="drawer-field-label">Compliance Mapping</div>
<div id="drawerComplianceBadges"></div>
</div>
<!-- PHASE 1: AI Auto-Fix before/after -->
<div id="drawerAutoFix" style="display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border);"></div>
</div>
<!-- Toast Area -->
<div class="toast-area" id="toastArea"></div>
<!-- PHASE 1: Chart.js for Security Trend Analytics -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="api.js"></script>
<script>
var API = 'https://rathious-safeaiscan.hf.space';
var TOKEN = localStorage.getItem('access_token') || '';
var lastScanId = null;
/* ── Helpers ──────────────────────────────────────────────── */
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function toast(msg, type) {
type = type || 'info';
var icons = {success:'bi-check-circle',error:'bi-x-circle',info:'bi-info-circle',warning:'bi-exclamation-triangle'};
var area = document.getElementById('toastArea');
var el = document.createElement('div');
el.className = 'toast-item toast-' + type;
el.innerHTML = '<i class="bi ' + (icons[type]||icons.info) + '"></i><span>' + esc(msg) + '</span>';
el.onclick = function() { el.remove(); };
area.appendChild(el);
setTimeout(function() { if (el.parentNode) el.remove(); }, 4000);
}
function setLog(msg, cls) {
var el = document.getElementById('logStrip');
if (!el) return;
cls = cls || '';
el.innerHTML = '<span class="' + cls + '">' + esc(msg) + '</span>';
}
function authHeaders() {
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN };
}
function authHeadersMultipart() {
return { 'Authorization': 'Bearer ' + TOKEN };
}
function setProgress(pct) {
var bar = document.getElementById('progressBar');
if (bar) bar.style.width = pct + '%';
}
/* ── Mode switcher ────────────────────────────────────────── */
var MODES = ['code','zip','repo','ai','cve','reports'];
function setMode(mode) {
MODES.forEach(function(m) {
var btn = document.getElementById('mode-' + m);
var sb = document.getElementById('sb-' + m);
var pan = document.getElementById('panel-' + m);
if (btn) btn.classList.toggle('active', m === mode);
if (sb) sb.classList.toggle('active', m === mode);
if (pan) pan.classList.toggle('active', m === mode);
});
if (mode === 'reports') {
loadReports();
loadSecurityTrendChart(); // PHASE 1
}
// Mobile: close the slide-out sidebar after navigating
closeMobileSidebar();
// Mobile: scroll the active topbar mode button into view (horizontal scroll tabs)
var activeBtn = document.getElementById('mode-' + mode);
if (activeBtn && activeBtn.scrollIntoView) {
activeBtn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}
}
/* ── MOBILE SIDEBAR TOGGLE ──────────────────────────────────── */
function toggleMobileSidebar() {
var sidebar = document.getElementById('proSidebar');
var overlay = document.getElementById('mobileSidebarOverlay');
if (!sidebar) return;
var isOpen = sidebar.classList.contains('mobile-open');
if (isOpen) closeMobileSidebar();
else {
sidebar.classList.add('mobile-open');
if (overlay) overlay.classList.add('show');
document.body.classList.add('mobile-nav-open');
}
}
function closeMobileSidebar() {
var sidebar = document.getElementById('proSidebar');
var overlay = document.getElementById('mobileSidebarOverlay');
if (sidebar) sidebar.classList.remove('mobile-open');
if (overlay) overlay.classList.remove('show');
document.body.classList.remove('mobile-nav-open');
}
// Auto-close mobile sidebar if the window is resized/rotated past the
// phone/tablet breakpoint (e.g. rotating a tablet to landscape)
window.addEventListener('resize', function() {
if (window.innerWidth > 768) closeMobileSidebar();
});
/* ── PHASE 1: Security Trend Analytics (Chart.js) ──────────── */
var securityTrendChart = null;
async function loadSecurityTrendChart() {
var ctx = document.getElementById('securityTrendChart');
if (!ctx || typeof Chart === 'undefined') return;
try {
var res = await fetch(API + '/api/analytics/security-trend', { headers: authHeaders() });
var raw = await res.json();
var data = raw.data || raw;
var points = data.points || [];
var avgEl = document.getElementById('trendAvgScore');
var bestEl = document.getElementById('trendBestScore');
var worstEl = document.getElementById('trendWorstScore');
if (avgEl) avgEl.textContent = data.average_score != null ? data.average_score : 'β€”';
if (bestEl) bestEl.textContent = data.best_score != null ? data.best_score : 'β€”';
if (worstEl) worstEl.textContent = data.worst_score != null ? data.worst_score : 'β€”';
if (securityTrendChart) { securityTrendChart.destroy(); securityTrendChart = null; }
var emptyEl = document.getElementById('securityTrendEmpty');
if (!points.length) {
if (emptyEl) emptyEl.style.display = 'flex';
ctx.style.display = 'none';
return;
}
if (emptyEl) emptyEl.style.display = 'none';
ctx.style.display = '';
var gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200);
gradient.addColorStop(0, 'rgba(34,197,94,0.35)');
gradient.addColorStop(1, 'rgba(34,197,94,0)');
securityTrendChart = new Chart(ctx, {
type: 'line',
data: {
labels: points.map(function(p){ return p.date; }),
datasets: [{
label: 'Security Score',
data: points.map(function(p){ return p.score; }),
borderColor: '#22c55e',
borderWidth: 2.5,
backgroundColor: gradient,
fill: true,
tension: 0.4,
pointBackgroundColor: '#22c55e',
pointRadius: 3,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#0f1a2e', titleColor: '#e8edf8', bodyColor: '#8296b3',
callbacks: { label: function(c) { return ' Score: ' + c.parsed.y + '/100'; } }
}
},
scales: {
x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#8296b3', font: { size: 10 } } },
y: {
min: 0, max: 100,
grid: { color: 'rgba(255,255,255,0.04)' },
ticks: { color: '#8296b3', font: { size: 10 }, stepSize: 25 }
}
}
}
});
} catch(e) {
console.debug('Security trend chart load failed (non-fatal):', e.message);
}
}
/* ── Init ─────────────────────────────────────────────────── */
(function init() {
var email = localStorage.getItem('user_email') || '';
var sideEl = document.getElementById('sidebarEmail');
if (sideEl && email) sideEl.textContent = email;
// Verify session and get live plan/trial data from backend
fetch(API + '/api/me', { headers: authHeaders() })
.then(function(r) {
if (r.status === 401) { window.location.replace('login.html'); return null; }
return r.json();
})
.then(function(raw) {
if (!raw) return;
var d = raw.data || raw;
// Update email display
if (d.email) {
if (sideEl) sideEl.textContent = d.email;
localStorage.setItem('user_email', d.email);
}
var plan = (d.plan || 'free').toLowerCase();
var isPro = d.is_pro || false;
var trialActive = d.trial_active || false;
var daysLeft = typeof d.days_left === 'number' ? d.days_left : 0;
var trialEnd = d.trial_end || '';
// Persist to localStorage 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));
// Access control: paid pro OR active trial = allowed
var hasAccess = (plan === 'pro' || plan === 'enterprise') ||
(plan === 'pro_trial' && trialActive);
if (!hasAccess) {
// Expired trial β†’ tell them exactly why
var reason = (plan === 'pro_trial' && !trialActive) ? 'trial_expired' : 'pro_required';
window.location.replace('checkout.html?reason=' + reason);
return;
}
// Update PRO badge in topbar to show trial status
updateProBadge(plan, trialActive, daysLeft);
// Show trial countdown banner if in active trial
if (plan === 'pro_trial' && trialActive) {
showTrialBanner(daysLeft, trialEnd);
}
// Update usage pill
var pill = document.getElementById('usagePill');
if (pill) {
if (plan === 'pro_trial' && trialActive) {
pill.textContent = 'Trial \u2022 ' + daysLeft + 'd left';
} else {
pill.textContent = 'Unlimited scans';
}
}
})
.catch(function(e) {
// Network error β€” don't redirect, let user keep working
console.warn('[SafeAIScan] Auth check failed (non-fatal):', e.message);
});
setupZipDrop();
})();
function updateProBadge(plan, trialActive, daysLeft) {
var badge = document.querySelector('.pro-badge-tag');
if (!badge) return;
if (plan === 'pro_trial' && trialActive) {
badge.textContent = 'TRIAL';
badge.style.background = 'linear-gradient(135deg,rgba(0,255,163,.2),rgba(91,123,254,.15))';
badge.style.color = '#00ffa3';
badge.style.border = '1px solid rgba(0,255,163,.35)';
badge.title = daysLeft + ' days left in your free trial';
// Show sidebar CTA
var cta = document.getElementById('sidebarTrialCTA');
var ctaDays = document.getElementById('sidebarTrialDays');
if (cta) cta.style.display = 'block';
if (ctaDays) {
var isUrgent = daysLeft <= 3;
ctaDays.innerHTML = isUrgent
? '<strong style="color:#f43f5e;">' + daysLeft + ' day' + (daysLeft===1?'':'s') + ' left!</strong> Upgrade to keep access.'
: daysLeft + ' day' + (daysLeft===1?'':'s') + ' remaining. All Pro features active.';
}
} else if (plan === 'enterprise') {
badge.textContent = 'ENTERPRISE';
badge.style.color = '#e879f9';
badge.style.background = 'linear-gradient(135deg,rgba(192,38,211,.2),rgba(91,123,254,.15))';
badge.style.border = '1px solid rgba(192,38,211,.35)';
} else {
badge.textContent = 'PRO';
}
}
function showTrialBanner(daysLeft, trialEnd) {
// Don't show if already dismissed this session
if (sessionStorage.getItem('trialBannerDismissed')) return;
if (document.getElementById('trialBanner')) return;
var isUrgent = daysLeft <= 3;
var isWarning = daysLeft <= 7;
var color = isUrgent ? '#f43f5e' : isWarning ? '#fb923c' : '#00ffa3';
var bgColor = isUrgent
? 'rgba(244,63,94,.08)' : isWarning
? 'rgba(251,146,60,.06)' : 'rgba(0,255,163,.06)';
var borderColor = isUrgent
? 'rgba(244,63,94,.3)' : isWarning
? 'rgba(251,146,60,.25)' : 'rgba(0,255,163,.2)';
var msg = isUrgent
? '<strong style="color:' + color + ';">Trial ends in ' + daysLeft + ' day' + (daysLeft === 1 ? '' : 's') + '!</strong> Upgrade now to keep all Pro features.'
: 'Your free Pro trial ends in <strong style="color:' + color + ';">' + daysLeft + ' days</strong>' + (trialEnd ? ' (' + trialEnd + ')' : '') + '. All features are fully unlocked.';
var banner = document.createElement('div');
banner.id = 'trialBanner';
banner.style.cssText = [
'position:fixed;bottom:16px;left:50%;transform:translateX(-50%);',
'background:' + bgColor + ';border:1px solid ' + borderColor + ';',
'border-radius:12px;padding:10px 16px;',
'display:flex;align-items:center;gap:12px;',
'font-family:"DM Sans",sans-serif;font-size:12px;color:var(--text-muted);',
'z-index:9000;box-shadow:0 8px 32px rgba(0,0,0,.5);backdrop-filter:blur(10px);',
'max-width:90vw;white-space:nowrap;',
'animation:toastIn .3s ease both;'
].join('');
banner.innerHTML =
'<i class="bi bi-clock" style="color:' + color + ';font-size:14px;flex-shrink:0;"></i>' +
'<span>' + msg + '</span>' +
'<a href="checkout.html" style="flex-shrink:0;background:linear-gradient(135deg,#5b7bfe,#4361ee);' +
'color:#fff;border-radius:8px;padding:5px 14px;font-size:11px;font-weight:600;' +
'text-decoration:none;white-space:nowrap;">Upgrade \u2014 $1.99/mo</a>' +
'<button onclick="dismissTrialBanner()" style="flex-shrink:0;background:none;border:none;' +
'color:var(--text-faint);cursor:pointer;font-size:18px;line-height:1;padding:0;">&times;</button>';
document.body.appendChild(banner);
}
function dismissTrialBanner() {
var el = document.getElementById('trialBanner');
if (el) el.remove();
sessionStorage.setItem('trialBannerDismissed', '1');
}
function doLogout() {
localStorage.clear();
window.location.href = 'login.html';
}
/* ── CODE SCANNER ─────────────────────────────────────────── */
async function runCodeScan() {
var code = (document.getElementById('codeInput') || {}).value || '';
if (!code.trim()) { toast('Paste some code first', 'warning'); return; }
var btn = document.getElementById('scanBtn');
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Scanning…'; }
setProgress(20); setLog('Scanning for secrets…');
try {
var res = await fetch(API + '/api/analyze', {
method: 'POST', headers: authHeaders(),
body: JSON.stringify({ text: code })
});
setProgress(70);
var raw = await res.json();
var data = raw.data || raw;
if (!res.ok) {
toast(data.error || data.detail || 'Scan failed', 'error');
setLog('Scan failed', 'log-err');
return;
}
var findings = data.findings || [];
lastScanId = data.id || data.scan_id || null;
window._lastScanFull = data; // PHASE 1: cache full result for executive PDF export
renderFindings(findings, data.risk || data.risk_level || 'NONE');
// PHASE 1: weighted security score ring
if (typeof data.security_score === 'number') {
renderSecurityScoreInline(data.security_score, data.score_risk_level);
}
setProgress(100);
setLog('Scan complete β€” ' + findings.length + ' secret(s) found', findings.length ? 'log-warn' : 'log-ok');
toast(findings.length + ' finding(s) Β· risk: ' + (data.risk || data.risk_level || 'NONE'),
findings.length ? 'warning' : 'success');
if (data.ai) showInlineAI(data.ai);
} catch(e) {
toast('Network error: ' + e.message, 'error');
setLog(e.message, 'log-err');
} finally {
setTimeout(function() { setProgress(0); }, 800);
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-play-circle-fill"></i> Run Scan'; }
}
}
/* ── PHASE 1: Security Score Ring ──────────────────────────── */
function renderSecurityScoreInline(score, riskLevel) {
var card = document.getElementById('securityScoreCard');
var ring = document.getElementById('scoreRing');
var value = document.getElementById('scoreRingValue');
var label = document.getElementById('scoreRiskLabel');
if (!card) return;
var colorMap = {
'Excellent': '#22c55e', 'Good': '#22c55e',
'Moderate': '#facc15', 'High Risk': '#fb923c', 'Critical': '#c026d3'
};
var color = colorMap[riskLevel] || '#facc15';
var circumference = 2 * Math.PI * 24; // r=24
var pct = Math.max(0, Math.min(100, score));
var offset = circumference * (1 - pct / 100);
card.style.display = 'block';
if (ring) {
ring.style.stroke = color;
ring.setAttribute('stroke-dasharray', circumference.toFixed(2));
ring.setAttribute('stroke-dashoffset', offset.toFixed(2));
}
if (value) { value.textContent = score; value.style.color = color; }
if (label) { label.textContent = riskLevel || 'β€”'; label.style.color = color; }
}
function clearCode() {
var el = document.getElementById('codeInput');
if (el) el.value = '';
renderFindings([], 'NONE');
setLog('Ready', 'log-ok');
}
/* ── AI ONLY (code panel) ─────────────────────────────────── */
async function runAIOnly() {
var code = (document.getElementById('codeInput') || {}).value || '';
if (!code.trim()) { toast('Paste some code first', 'warning'); return; }
var btn = document.getElementById('aiOnlyBtn');
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Analysing…'; }
setProgress(30); setLog('Running deep AI analysis…');
try {
var res = await fetch(API + '/api/analyze', {
method: 'POST', headers: authHeaders(),
body: JSON.stringify({ text: code, mode: 'ai_only', depth: 'full' })
});
setProgress(80);
var raw = await res.json();
var data = raw.data || raw;
if (!res.ok) { toast(data.error || 'AI analysis failed', 'error'); return; }
var ai = data.ai || data;
setMode('ai');
var codeEl = document.getElementById('aiCodeInput');
if (codeEl) codeEl.value = code;
displayAIResult(ai);
setProgress(100);
toast('AI analysis complete', 'success');
} catch(e) {
toast('AI error: ' + e.message, 'error');
} finally {
setTimeout(function() { setProgress(0); }, 800);
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-cpu"></i> AI Only'; }
}
}
/* ── FINDINGS RENDERER ────────────────────────────────────── */
function renderFindings(findings, risk) {
var list = document.getElementById('resultsList');
var rb = document.getElementById('riskBadge');
if (!list) return;
// Update risk badge β€” PHASE 1: added CRITICAL
if (rb) {
var riskClass = { 'CRITICAL':'risk-critical','HIGH':'risk-high','MEDIUM':'risk-medium','LOW':'risk-low','NONE':'risk-none' };
rb.className = 'risk-badge ' + (riskClass[risk] || 'risk-none');
rb.textContent = risk || 'NONE';
}
// Update summary β€” PHASE 1: count CRITICAL into the HIGH bucket display
// (keeps the existing 4-stat layout; CRITICAL findings still show in the list)
var counts = {CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0};
(findings||[]).forEach(function(f) { counts[f.severity] = (counts[f.severity]||0)+1; });
var sumTotal = document.getElementById('sumTotal');
var sumHigh = document.getElementById('sumHigh');
var sumMed = document.getElementById('sumMed');
var sumLow = document.getElementById('sumLow');
if (sumTotal) sumTotal.textContent = (findings||[]).length;
if (sumHigh) sumHigh.textContent = counts.CRITICAL + counts.HIGH; // CRITICAL folded into HIGH stat
if (sumMed) sumMed.textContent = counts.MEDIUM;
if (sumLow) sumLow.textContent = counts.LOW;
if (!findings || !findings.length) {
list.innerHTML = '<div class="results-empty"><i class="bi bi-shield-check" style="font-size:32px;color:var(--sg);display:block;margin-bottom:8px;"></i>No secrets found β€” code looks clean!</div>';
return;
}
var sevOrder = {CRITICAL:4,HIGH:3,MEDIUM:2,LOW:1};
var sorted = findings.slice().sort(function(a,b){ return (sevOrder[b.severity]||0) - (sevOrder[a.severity]||0); });
list.innerHTML = sorted.map(function(f, i) {
var sevClass = 'sev-' + (f.severity||'LOW');
var file = f.file ? (esc(f.file) + (f.line ? ':' + f.line : '')) : '';
var autoFixBadge = f.auto_fix ? '<span class="badge-pill" style="background:rgba(34,197,94,0.12);color:#22c55e;border:1px solid rgba(34,197,94,0.25);font-size:9px;margin-left:6px;"><i class="bi bi-magic"></i> Fix available</span>' : '';
return '<div class="finding-card" onclick="openDrawer(' + i + ')">' +
'<span class="sev-chip ' + sevClass + '">' + esc(f.severity||'?') + '</span>' +
'<div class="finding-type">' + esc(f.type || f.title || 'Secret') + autoFixBadge + '</div>' +
(file ? '<div class="finding-meta">' + file + '</div>' : '') +
(f.match ? '<div class="finding-match">' + esc(f.match) + '</div>' : '') +
'</div>';
}).join('');
// Store sorted order for drawer (matches displayed index)
window._proFindings = sorted;
}
/* ── INLINE AI PANEL (code scan) ─────────────────────────── */
function showInlineAI(ai) {
if (!ai || !ai.explanation) return;
// If we're in code panel, show a brief note in the log
setLog('AI: ' + (ai.explanation||'').substring(0,80) + '…', 'log-ok');
}
/* ── DRAWER ───────────────────────────────────────────────── */
function openDrawer(idx) {
var f = (window._proFindings || [])[idx];
if (!f) return;
document.getElementById('drawerTitle').textContent = f.type || f.title || 'Finding';
document.getElementById('drawerType').textContent = f.type || f.title || 'β€”';
document.getElementById('drawerFile').textContent = f.file ? (f.file + (f.line ? ' Β· line ' + f.line : '')) : 'β€”';
document.getElementById('drawerMatch').textContent = f.match || 'β€”';
document.getElementById('drawerDesc').textContent = f.description || 'β€”';
document.getElementById('drawerFix').textContent = f.fix || 'β€”';
var sevEl = document.getElementById('drawerSev');
var sevClass = {CRITICAL:'sev-CRITICAL',HIGH:'sev-HIGH',MEDIUM:'sev-MEDIUM',LOW:'sev-LOW'};
sevEl.innerHTML = '<span class="sev-chip ' + (sevClass[f.severity]||'sev-LOW') + '">' + esc(f.severity||'?') + '</span>';
// PHASE 1: Compliance mapping (OWASP / NIST)
var compEl = document.getElementById('drawerCompliance');
var compBadges = document.getElementById('drawerComplianceBadges');
if (compEl && compBadges) {
if (f.owasp || f.nist) {
compEl.style.display = 'block';
compBadges.innerHTML =
(f.owasp ? '<span class="badge-pill" style="background:rgba(91,123,254,0.12);color:#5b7bfe;border:1px solid rgba(91,123,254,0.25);margin-right:6px;">' + esc(f.owasp) + '</span>' : '') +
(f.nist ? '<span class="badge-pill" style="background:rgba(56,189,248,0.1);color:#38bdf8;border:1px solid rgba(56,189,248,0.2);">NIST ' + esc(f.nist) + '</span>' : '');
} else {
compEl.style.display = 'none';
compBadges.innerHTML = '';
}
}
// PHASE 1: AI Auto-Fix before/after diff
var autoFixEl = document.getElementById('drawerAutoFix');
if (autoFixEl) {
if (f.auto_fix && f.auto_fix.before) {
var af = f.auto_fix;
var conf = af.confidence || 0;
var confColor = conf >= 90 ? '#22c55e' : conf >= 70 ? '#facc15' : '#fb923c';
autoFixEl.style.display = 'block';
autoFixEl.innerHTML =
'<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.8px;color:#22c55e;margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;">' +
'<span><i class="bi bi-magic me-1"></i>AI Suggested Fix</span>' +
'<span class="badge-pill" style="color:' + confColor + ';border:1px solid ' + confColor + '33;">' + conf + '% confidence</span>' +
'</div>' +
'<div style="font-size:9px;text-transform:uppercase;color:#f43f5e;margin-bottom:3px;">Before</div>' +
'<pre style="background:rgba(244,63,94,0.06);border:1px solid rgba(244,63,94,0.18);border-radius:6px;padding:8px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-word;margin:0 0 8px;">' + esc(af.before) + '</pre>' +
'<div style="font-size:9px;text-transform:uppercase;color:#22c55e;margin-bottom:3px;">After</div>' +
'<pre style="background:rgba(34,197,94,0.06);border:1px solid rgba(34,197,94,0.18);border-radius:6px;padding:8px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-word;margin:0 0 8px;">' + esc(af.after) + '</pre>' +
'<div style="font-size:12px;color:var(--text-muted);line-height:1.5;">' + esc(af.explanation||'') + '</div>';
} else {
autoFixEl.style.display = 'none';
autoFixEl.innerHTML = '';
}
}
document.getElementById('detailDrawer').classList.add('open');
}
function closeDrawer() {
var drawer = document.getElementById('detailDrawer');
drawer.classList.remove('open');
drawer.style.transform = '';
drawer.style.transition = '';
}
/* ── MOBILE: Swipe-down-to-close for the detail drawer ─────── */
(function setupDrawerSwipe() {
var drawer = document.getElementById('detailDrawer');
if (!drawer) return;
var startY = 0, currentY = 0, dragging = false;
drawer.addEventListener('touchstart', function(e) {
// Only start swipe-to-close if touch begins near the top of the drawer
// (avoids hijacking scroll gestures inside the drawer content)
if (e.touches[0].clientY - drawer.getBoundingClientRect().top > 60) return;
startY = e.touches[0].clientY;
dragging = true;
drawer.style.transition = 'none';
}, { passive: true });
drawer.addEventListener('touchmove', function(e) {
if (!dragging) return;
currentY = e.touches[0].clientY;
var delta = Math.max(0, currentY - startY);
if (delta > 0) drawer.style.transform = 'translateY(' + delta + 'px)';
}, { passive: true });
drawer.addEventListener('touchend', function() {
if (!dragging) return;
dragging = false;
drawer.style.transition = '';
var delta = Math.max(0, currentY - startY);
drawer.style.transform = '';
if (delta > 100) closeDrawer();
startY = 0; currentY = 0;
});
})();
/* ── ZIP UPLOAD ───────────────────────────────────────────── */
function setupZipDrop() {
var zone = document.getElementById('dropZone');
var input = document.getElementById('zipInput');
if (!zone || !input) return;
zone.addEventListener('dragover', function(e) { e.preventDefault(); zone.classList.add('drag-over'); });
zone.addEventListener('dragleave', function() { zone.classList.remove('drag-over'); });
zone.addEventListener('drop', function(e) {
e.preventDefault(); zone.classList.remove('drag-over');
var file = e.dataTransfer.files[0];
if (file) handleZipFile(file);
});
input.addEventListener('change', function() {
if (this.files[0]) handleZipFile(this.files[0]);
});
}
async function handleZipFile(file) {
if (!file.name.toLowerCase().endsWith('.zip')) {
toast('Only .zip files are accepted', 'warning'); return;
}
var prog = document.getElementById('zipProgress');
var fill = document.getElementById('zipProgressFill');
var label = document.getElementById('zipStateLabel');
var results= document.getElementById('zipResults');
prog.classList.add('show');
if (fill) fill.style.width = '20%';
if (label) label.textContent = 'Uploading…';
if (results) results.innerHTML = '';
var fd = new FormData();
fd.append('file', file);
try {
var res = await fetch(API + '/scan/file', {
method: 'POST', headers: authHeadersMultipart(), body: fd
});
if (fill) fill.style.width = '80%';
var raw = await res.json();
var data = raw.data || raw;
if (!res.ok) {
toast(data.error || data.detail || 'Upload failed', 'error');
prog.classList.remove('show');
return;
}
if (fill) fill.style.width = '100%';
if (label) label.textContent = 'Scan complete!';
var findings = data.findings || [];
lastScanId = data.scan_id || data.id || null;
window._proFindings = findings;
if (results) results.innerHTML = buildResultsBlock(findings, data.risk_level || 'NONE', data.scan_id);
toast('ZIP scan: ' + findings.length + ' secret(s) found [' + (data.risk_level||'NONE') + ']',
findings.length ? 'warning' : 'success');
setTimeout(function() { prog.classList.remove('show'); }, 2000);
} catch(e) {
toast('Upload error: ' + e.message, 'error');
prog.classList.remove('show');
}
}
/* ── REPO SCAN ────────────────────────────────────────────── */
async function runRepoScan() {
var url = (document.getElementById('repoUrl') || {}).value || '';
url = url.trim();
if (!url || url === 'https://github.com/') {
toast('Enter a valid GitHub repo URL', 'warning'); return;
}
if (!url.startsWith('https://github.com/')) {
toast('Only https://github.com/ URLs are supported', 'warning'); return;
}
if (url.split('/').filter(Boolean).length < 4) {
toast('Enter a full repo URL: https://github.com/owner/repo', 'warning'); return;
}
var prog = document.getElementById('repoProgress');
var fill = document.getElementById('repoProgressFill');
var label = document.getElementById('repoStateLabel');
var msgEl = document.getElementById('repoProgressMsg');
var results = document.getElementById('repoResults');
prog.classList.add('show');
if (fill) fill.style.width = '5%';
if (label) label.textContent = 'Queuing scan…';
if (results) results.innerHTML = '';
try {
var res = await fetch(API + '/scan/repo', {
method: 'POST', headers: authHeaders(),
body: JSON.stringify({ repo_url: url })
});
var raw = await res.json();
var data = raw.data || raw;
if (!res.ok) {
toast(data.error || data.detail || 'Failed to queue scan', 'error');
prog.classList.remove('show'); return;
}
var taskId = data.task_id;
if (label) label.textContent = 'Scan queued β€” polling…';
pollRepoTask(taskId, fill, label, msgEl, prog, results);
} catch(e) {
toast('Error: ' + e.message, 'error');
prog.classList.remove('show');
}
}
function pollRepoTask(taskId, fill, label, msgEl, prog, results) {
var attempts = 0;
var maxAttempts = 120; // 6 minutes
var iv = setInterval(async function() {
attempts++;
if (attempts > maxAttempts) {
clearInterval(iv);
toast('Scan timed out', 'error');
prog.classList.remove('show');
return;
}
try {
var res = await fetch(API + '/scan/status/' + taskId, { headers: authHeaders() });
var raw = await res.json();
var task = raw.data || raw;
var state = task.state || 'QUEUED';
var progress = task.progress || 0;
var message = task.message || state;
if (fill) fill.style.width = Math.max(progress, 8) + '%';
if (label) label.textContent = message;
if (msgEl) msgEl.textContent = state;
if (state === 'DONE') {
clearInterval(iv);
var r = task.result_json || task.result || {};
var findings = r.findings || [];
lastScanId = r.scan_id || taskId;
window._proFindings = findings;
window._lastScanFull = r; // PHASE 1: cache full result for executive PDF export
if (results) results.innerHTML = buildResultsBlock(findings, r.risk_level || 'NONE', lastScanId, r);
toast('Repo scan done: ' + findings.length + ' secret(s) [' + (r.risk_level||'NONE') + ']',
findings.length ? 'warning' : 'success');
if (fill) fill.style.width = '100%';
setTimeout(function() { prog.classList.remove('show'); }, 2000);
} else if (state === 'FAILED') {
clearInterval(iv);
var errMsg = (task.result_json && task.result_json.error) || task.message || 'Scan failed';
toast(errMsg, 'error');
prog.classList.remove('show');
}
} catch(e) {
// Ignore network blips during polling
}
}, 3000);
}
/* ── RESULTS BLOCK BUILDER ────────────────────────────────── */
function buildResultsBlock(findings, risk, scanId, repoResult) {
var riskColor = {CRITICAL:'#c026d3',HIGH:'#f43f5e',MEDIUM:'#fb923c',LOW:'#fbbf24',NONE:'#34d399'};
var html = '';
// PHASE 1: Repository Health Dashboard card
if (repoResult && repoResult.repo_health) {
html += buildRepoHealthCard(repoResult.repo_health);
}
html += '<div style="background:var(--bg-2);border:1px solid var(--border);border-radius:14px;overflow:hidden;">' +
'<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;">' +
'<span style="font-size:13px;font-weight:600;color:var(--text-primary);">' + findings.length + ' secret(s) found</span>' +
'<span style="font-size:11px;font-weight:700;padding:3px 10px;border-radius:99px;background:rgba(255,255,255,.06);color:' + (riskColor[risk]||'#34d399') + ';">' + esc(risk) + '</span>';
if (scanId) {
html += '<button onclick="downloadPDF(\'' + esc(scanId) + '\')" style="margin-left:auto;background:none;border:1px solid var(--border);color:var(--text-muted);border-radius:8px;padding:5px 12px;font-size:11px;cursor:pointer;" title="Download PDF">' +
'<i class="bi bi-file-earmark-pdf me-1"></i>PDF</button>';
html += '<button onclick="exportExecutivePDF()" style="background:none;border:1px solid var(--border);color:var(--text-muted);border-radius:8px;padding:5px 12px;font-size:11px;cursor:pointer;" title="Download Executive Report">' +
'<i class="bi bi-file-earmark-richtext me-1"></i>Executive</button>';
}
html += '</div>';
if (!findings.length) {
html += '<div style="padding:24px;text-align:center;color:var(--sg);">' +
'<i class="bi bi-shield-check" style="font-size:28px;display:block;margin-bottom:6px;"></i>No secrets detected!</div>';
} else {
html += '<div style="padding:12px;">' +
findings.map(function(f, i) {
var sevColors = {CRITICAL:'#e879f9',HIGH:'#fb7185',MEDIUM:'#fdba74',LOW:'#fbbf24'};
var autoFixBadge = f.auto_fix ? ' <span class="badge-pill" style="background:rgba(34,197,94,0.12);color:#22c55e;border:1px solid rgba(34,197,94,0.25);">Fix available</span>' : '';
return '<div style="padding:10px;border-bottom:1px solid rgba(99,130,255,.06);cursor:pointer;" ' +
'onclick="openDrawer(' + i + ')">' +
'<span style="font-size:9px;font-weight:700;text-transform:uppercase;color:' + (sevColors[f.severity]||'#fbbf24') + ';margin-right:8px;">' + esc(f.severity) + '</span>' +
'<span style="font-size:12px;font-weight:600;color:var(--text-primary);">' + esc(f.type||f.title||'Secret') + '</span>' + autoFixBadge +
(f.file ? '<div style="font-family:\'JetBrains Mono\',monospace;font-size:10px;color:#38bdf8;margin-top:2px;">' + esc(f.file) + (f.line?':'+f.line:'') + '</div>' : '') +
'</div>';
}).join('') + '</div>';
}
html += '</div>';
window._proFindings = findings;
return html;
}
/* ── PHASE 1: Repository Health Dashboard card ─────────────── */
function buildRepoHealthCard(health) {
var colorMap = {
'Excellent':'#22c55e','Good':'#22c55e','Moderate':'#facc15','High Risk':'#fb923c','Critical':'#c026d3'
};
var scoreColor = colorMap[health.risk_level] || '#facc15';
var stats = [
{ label: 'Security Score', value: (health.security_score != null ? health.security_score : 0), color: scoreColor },
{ label: 'Critical', value: health.critical_count || 0, color: '#c026d3' },
{ label: 'High', value: health.high_count || 0, color: '#f43f5e' },
{ label: 'Medium', value: health.medium_count || 0, color: '#fb923c' },
{ label: 'Low', value: health.low_count || 0, color: '#facc15' },
{ label: 'Secrets Found', value: health.secret_count || 0, color: '#5b7bfe' },
{ label: 'Dependencies', value: health.dependency_count || 0, color: '#38bdf8' },
{ label: 'Outdated', value: health.outdated_packages || 0, color: '#fb923c' },
];
var cells = stats.map(function(s) {
return '<div style="text-align:center;background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:12px 8px;">' +
'<div style="font-family:\'Syne\',sans-serif;font-weight:800;font-size:20px;color:' + s.color + ';">' + s.value + '</div>' +
'<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.6px;color:var(--text-faint);margin-top:4px;">' + esc(s.label) + '</div>' +
'</div>';
}).join('');
return '<div style="background:var(--bg-2);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:12px;">' +
'<div style="font-family:\'Syne\',sans-serif;font-weight:700;font-size:14px;margin-bottom:12px;">' +
'<i class="bi bi-heart-pulse me-2" style="color:var(--accent);"></i>Repository Health</div>' +
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));gap:10px;">' + cells + '</div>' +
'</div>';
}
/* ── DEEP AI PANEL ────────────────────────────────────────── */
async function runDeepAI() {
var code = (document.getElementById('aiCodeInput') || {}).value || '';
if (!code.trim()) { toast('Paste code for AI analysis', 'warning'); return; }
var btn = document.getElementById('aiAnalyzeBtn');
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Analysing…'; }
try {
var res = await fetch(API + '/api/analyze', {
method: 'POST', headers: authHeaders(),
body: JSON.stringify({ text: code, mode: 'ai_only', depth: 'full' })
});
var raw = await res.json();
var data = raw.data || raw;
if (!res.ok) { toast(data.error || 'AI failed', 'error'); return; }
displayAIResult(data.ai || data);
toast('AI analysis complete', 'success');
} catch(e) {
toast('AI error: ' + e.message, 'error');
} finally {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-cpu"></i> Analyse with AI'; }
}
}
function displayAIResult(ai) {
if (!ai) return;
var section = document.getElementById('aiResult');
var expEl = document.getElementById('aiExplanation');
var fixEl = document.getElementById('aiFixesList');
var fixLbl = document.getElementById('aiFixesLabel');
var riskRow = document.getElementById('aiRiskRow');
var sevEl = document.getElementById('aiSeverity');
if (section) section.style.display = 'block';
if (expEl) expEl.textContent = ai.explanation || 'Analysis complete.';
if (fixEl && ai.fixes && ai.fixes.length) {
if (fixLbl) fixLbl.style.display = 'block';
fixEl.innerHTML = ai.fixes.map(function(fix, i) {
return '<div class="ai-fix-item"><div class="fix-num">' + (i+1) + '</div><div class="fix-text">' + esc(fix) + '</div></div>';
}).join('');
}
if (riskRow && ai.severity_prediction) {
riskRow.style.display = 'block';
if (sevEl) sevEl.textContent = ai.severity_prediction;
}
}
/* ── CVE LOOKUP ───────────────────────────────────────────── */
async function runCVELookup() {
var query = (document.getElementById('cveInput') || {}).value || '';
if (!query.trim()) { toast('Enter a CVE ID or keyword', 'warning'); return; }
var results = document.getElementById('cveResults');
if (results) results.innerHTML = '<div style="color:var(--text-faint);font-size:12px;padding:8px 0;">Searching…</div>';
try {
var res = await fetch(API + '/api/cve/search?query=' + encodeURIComponent(query.trim()), { headers: authHeaders() });
var raw = await res.json();
var data = raw.data || raw;
if (!res.ok) {
if (results) results.innerHTML = '<div style="color:var(--danger);font-size:12px;">CVE lookup unavailable. The CVE API may not be enabled on this plan or endpoint.</div>';
return;
}
var cves = data.cves || data.data || [];
if (!cves.length) {
if (results) results.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:8px 0;">No CVEs found for "' + esc(query) + '"</div>';
return;
}
if (results) results.innerHTML = cves.map(function(cve) {
var score = cve.cvss || cve.score;
var sev = score >= 9 ? 'HIGH' : score >= 7 ? 'HIGH' : score >= 4 ? 'MEDIUM' : 'LOW';
var sevColors = {HIGH:'#fb7185',MEDIUM:'#fdba74',LOW:'#fbbf24'};
return '<div class="cve-result-card">' +
'<div style="display:flex;align-items:center;justify-content:space-between;">' +
'<span class="cve-id">' + esc(cve.id||'') + '</span>' +
(score != null ? '<span class="cve-score-badge" style="background:rgba(255,255,255,.06);color:' + (sevColors[sev]||'#fbbf24') + '">CVSS ' + score + '</span>' : '') +
'</div>' +
'<div class="cve-desc">' + esc((cve.description||'').substring(0,220)) + '</div>' +
'</div>';
}).join('');
} catch(e) {
if (results) results.innerHTML = '<div style="color:var(--danger);font-size:12px;">Error: ' + esc(e.message) + '</div>';
}
}
/* ── PDF REPORTS ──────────────────────────────────────────── */
async function loadReports() {
var grid = document.getElementById('reportsGrid');
if (grid) grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:32px;color:var(--text-faint);"><span class="spinner-border spinner-border-sm me-2"></span>Loading…</div>';
try {
var res = await fetch(API + '/scan/history', { headers: authHeaders() });
var raw = await res.json();
var data = raw.data || raw;
var scans = Array.isArray(data) ? data : (data.scans || data.history || []);
if (!scans.length) {
if (grid) grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--text-faint);"><i class="bi bi-file-earmark-pdf" style="font-size:36px;display:block;margin-bottom:10px;opacity:.4;"></i>No scans yet. Run your first scan to see reports here.</div>';
return;
}
if (grid) grid.innerHTML = scans.map(function(s) {
var riskColors = {HIGH:'#f43f5e',MEDIUM:'#fb923c',LOW:'#fbbf24',NONE:'#34d399'};
var risk = s.risk_level || s.risk || 'NONE';
var date = s.created_at ? new Date(s.created_at).toLocaleDateString() : 'β€”';
return '<div class="report-card">' +
'<div style="display:flex;align-items:center;justify-content:space-between;">' +
'<span class="report-risk" style="color:' + (riskColors[risk]||'#34d399') + ';">' + esc(risk) + '</span>' +
'<span class="report-meta">' + esc(date) + '</span>' +
'</div>' +
'<div class="report-source" title="' + esc(s.source||'') + '">' + esc(s.source || 'scan') + '</div>' +
'<div style="font-size:12px;color:var(--text-muted);">' + (s.total_secrets||0) + ' secret(s) found</div>' +
'<button onclick="downloadPDF(\'' + esc(s.id) + '\')" style="background:linear-gradient(135deg,#5b7bfe,#4361ee);color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:12px;font-weight:600;cursor:pointer;margin-top:4px;">' +
'<i class="bi bi-file-earmark-pdf me-1"></i>Download PDF</button>' +
'</div>';
}).join('');
} catch(e) {
if (grid) grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:32px;color:var(--danger);">Failed to load reports: ' + esc(e.message) + '</div>';
}
}
async function downloadPDF(scanId) {
toast('Generating PDF…', 'info');
try {
var res = await fetch(API + '/report/' + scanId + '/pdf', { headers: authHeaders() });
if (!res.ok) {
var raw = await res.json().catch(function(){return{};});
toast(raw.detail || 'PDF generation failed', 'error'); return;
}
var blob = await res.blob();
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'secretscan-' + scanId.substring(0,8) + '.pdf';
a.click();
URL.revokeObjectURL(url);
toast('PDF downloaded!', 'success');
} catch(e) {
toast('PDF error: ' + e.message, 'error');
}
}
async function exportCurrentPDF() {
if (!lastScanId) { toast('Run a scan first to generate a PDF', 'warning'); return; }
downloadPDF(lastScanId);
}
/* ── PHASE 1: Executive PDF Report ─────────────────────────── */
async function exportExecutivePDF() {
var data = window._lastScanFull;
if (!data || !(data.findings || []).length) {
toast('Run a scan first to generate an executive report', 'warning');
return;
}
toast('Generating executive report…', 'info');
try {
var payload = {
findings: data.findings || [],
scan_id: data.id || data.scan_id || '',
risk_level: data.risk || data.risk_level || 'NONE',
total_secrets: data.total_secrets || (data.findings || []).length,
summary: data.summary || {},
source: data.source || '',
truncated: data.truncated || false,
security_score: data.security_score || 0,
score_risk_level: data.score_risk_level || 'Moderate',
repo_health: data.repo_health || null,
dependency_findings: data.dependency_findings || []
};
var res = await fetch(API + '/api/report/pdf/executive', {
method: 'POST', headers: authHeaders(),
body: JSON.stringify(payload)
});
if (res.status === 403) {
var raw = await res.json().catch(function(){return{};});
var msg = (raw.detail && raw.detail.error) || raw.error || 'Executive PDF reports require a Pro or Enterprise plan.';
toast(msg, 'warning');
return;
}
if (!res.ok) {
toast('Executive PDF generation failed', 'error');
return;
}
var blob = await res.blob();
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = 'safeaiscan-executive-report.pdf'; a.click();
URL.revokeObjectURL(url);
toast('Executive report downloaded!', 'success');
} catch(e) {
toast('Executive PDF error: ' + e.message, 'error');
}
}
/* ── Keyboard shortcuts ───────────────────────────────────── */
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
var panel = document.querySelector('.pro-panel.active');
if (panel && panel.id === 'panel-code') runCodeScan();
else if (panel && panel.id === 'panel-ai') runDeepAI();
else if (panel && panel.id === 'panel-cve') runCVELookup();
}
if (e.key === 'Escape') closeDrawer();
});
</script>
</body>
</html>