690 lines
19 KiB
HTML
690 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>THE ANALYZER — Autonomous Security Engine</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;500;600;700&display=swap');
|
||
|
||
:root {
|
||
--bg: #0a0e17;
|
||
--surface: #111827;
|
||
--surface2: #1e293b;
|
||
--border: #1e293b;
|
||
--text: #e2e8f0;
|
||
--text-dim: #64748b;
|
||
--accent: #f97316;
|
||
--accent2: #22d3ee;
|
||
--green: #22c55e;
|
||
--red: #ef4444;
|
||
--yellow: #eab308;
|
||
--blue: #3b82f6;
|
||
--purple: #a855f7;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Inter', sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ─── Sidebar ─── */
|
||
.sidebar {
|
||
width: 260px;
|
||
background: var(--surface);
|
||
border-right: 1px solid var(--border);
|
||
padding: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.logo {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
color: var(--accent);
|
||
padding: 12px;
|
||
background: var(--bg);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
text-align: center;
|
||
}
|
||
|
||
.logo span { color: var(--text-dim); font-weight: 400; }
|
||
|
||
.nav {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.nav-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
padding: 10px 12px;
|
||
text-align: left;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-family: 'Inter', sans-serif;
|
||
transition: all 0.15s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.nav-btn:hover { background: var(--surface2); color: var(--text); }
|
||
.nav-btn.active { background: var(--surface2); color: var(--accent); }
|
||
.nav-btn .icon { font-size: 16px; width: 20px; text-align: center; }
|
||
|
||
/* ─── Main ─── */
|
||
.main {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.topbar {
|
||
padding: 16px 24px;
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.topbar h1 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
.status-dot {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.status-dot.idle { background: var(--text-dim); }
|
||
.status-dot.running { background: var(--green); animation: pulse 1.5s infinite; }
|
||
.status-dot.error { background: var(--red); }
|
||
.status-dot.done { background: var(--blue); }
|
||
|
||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||
|
||
.content {
|
||
flex: 1;
|
||
padding: 24px;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
/* ─── Scan Panel ─── */
|
||
.panel {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--text-dim);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.input-group input {
|
||
flex: 1;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
outline: none;
|
||
transition: border 0.15s;
|
||
}
|
||
|
||
.input-group input:focus { border-color: var(--accent); }
|
||
.input-group input::placeholder { color: var(--text-dim); }
|
||
|
||
.mode-selector {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.mode-btn {
|
||
flex: 1;
|
||
padding: 8px 16px;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-family: 'Inter', sans-serif;
|
||
transition: all 0.15s;
|
||
text-align: center;
|
||
}
|
||
|
||
.mode-btn:hover { border-color: var(--accent2); color: var(--text); }
|
||
.mode-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(249,115,22,0.1); }
|
||
|
||
.btn-primary {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: linear-gradient(135deg, var(--accent), #ea580c);
|
||
border: none;
|
||
border-radius: 8px;
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
font-family: 'Inter', sans-serif;
|
||
}
|
||
|
||
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(249,115,22,0.3); }
|
||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
|
||
.btn-danger { background: var(--red); }
|
||
.btn-danger:hover { box-shadow: 0 4px 20px rgba(239,68,68,0.3); }
|
||
|
||
/* ─── Output Terminal ─── */
|
||
.terminal {
|
||
background: #000;
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.terminal-header {
|
||
padding: 10px 16px;
|
||
background: #0a0a0a;
|
||
border-bottom: 1px solid #1a1a1a;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.terminal-header span { color: var(--text-dim); font-size: 12px; }
|
||
.terminal-dots { display: flex; gap: 6px; }
|
||
.terminal-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||
.terminal-dot.r { background: #ff5f56; }
|
||
.terminal-dot.y { background: #ffbd2e; }
|
||
.terminal-dot.g { background: #27c93f; }
|
||
|
||
.terminal-body {
|
||
padding: 16px;
|
||
height: 350px;
|
||
overflow-y: auto;
|
||
color: #a8b2c1;
|
||
}
|
||
|
||
.terminal-body::-webkit-scrollbar { width: 6px; }
|
||
.terminal-body::-webkit-scrollbar-track { background: #000; }
|
||
.terminal-body::-webkit-scrollbar-thumb { background: #1a1a1a; border-radius: 3px; }
|
||
|
||
.terminal-line { white-space: pre-wrap; word-break: break-all; margin-bottom: 2px; }
|
||
.terminal-line .find { color: var(--red); font-weight: 600; }
|
||
.terminal-line .ok { color: var(--green); }
|
||
.terminal-line .info { color: var(--blue); }
|
||
.terminal-line .warn { color: var(--yellow); }
|
||
.terminal-line .brain { color: var(--purple); }
|
||
.terminal-line .dim { color: var(--text-dim); }
|
||
.terminal-line .accent { color: var(--accent); }
|
||
|
||
/* ─── Vector Grid ─── */
|
||
.vector-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.vector-card {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
user-select: none;
|
||
}
|
||
|
||
.vector-card:hover { border-color: var(--accent2); }
|
||
.vector-card.selected { border-color: var(--accent); background: rgba(249,115,22,0.08); }
|
||
|
||
.vector-card .num {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.vector-card .name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.vector-card .sev {
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sev-critical { color: var(--red); }
|
||
.sev-high { color: var(--accent); }
|
||
.sev-medium { color: var(--yellow); }
|
||
.sev-low { color: var(--blue); }
|
||
|
||
/* ─── Report List ─── */
|
||
.report-list { display: flex; flex-direction: column; gap: 8px; }
|
||
|
||
.report-item {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 14px 16px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.report-item:hover { border-color: var(--accent2); }
|
||
.report-item .target { font-weight: 500; font-size: 14px; }
|
||
.report-item .meta { font-size: 12px; color: var(--text-dim); }
|
||
.report-item .vulns { font-family: 'JetBrains Mono', monospace; font-weight: 600; }
|
||
.report-item .vulns.found { color: var(--red); }
|
||
.report-item .vulns.none { color: var(--green); }
|
||
|
||
.hidden { display: none !important; }
|
||
|
||
/* ─── Responsive ─── */
|
||
@media (max-width: 768px) {
|
||
.sidebar { width: 60px; padding: 12px 8px; }
|
||
.sidebar .logo { font-size: 10px; padding: 8px; }
|
||
.sidebar .nav-btn span { display: none; }
|
||
.main { padding: 0; }
|
||
.content { padding: 16px; }
|
||
.input-group { flex-direction: column; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ─── Sidebar ─── -->
|
||
<div class="sidebar">
|
||
<div class="logo">
|
||
ANALYZER<br>
|
||
<span>v2.0</span>
|
||
</div>
|
||
<div class="nav">
|
||
<button class="nav-btn active" onclick="showPanel('scan')">
|
||
<span class="icon">⚡</span> <span>Scan</span>
|
||
</button>
|
||
<button class="nav-btn" onclick="showPanel('vectors')">
|
||
<span class="icon">🎯</span> <span>Vectors</span>
|
||
</button>
|
||
<button class="nav-btn" onclick="showPanel('reports')">
|
||
<span class="icon">📄</span> <span>Reports</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── Main ─── -->
|
||
<div class="main">
|
||
<div class="topbar">
|
||
<h1><span class="status-dot idle" id="statusDot"></span><span id="statusText">Ready</span></h1>
|
||
<div style="display:flex;gap:8px;align-items:center;">
|
||
<span style="font-size:12px;color:var(--text-dim)" id="timer">00:00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<!-- ═══ SCAN PANEL ═══ -->
|
||
<div id="panel-scan" class="">
|
||
<div class="panel">
|
||
<div class="panel-title">New Scan</div>
|
||
<div class="input-group">
|
||
<input type="text" id="targetInput" placeholder="https://target.com" spellcheck="false">
|
||
</div>
|
||
<div class="mode-selector">
|
||
<button class="mode-btn active" data-mode="quick" onclick="setMode(this)">⚡ Quick</button>
|
||
<button class="mode-btn" data-mode="deep" onclick="setMode(this)">🔍 Deep</button>
|
||
<button class="mode-btn" data-mode="custom" onclick="setMode(this)">🎯 Custom</button>
|
||
</div>
|
||
<button class="btn-primary" id="scanBtn" onclick="startScan()">▶ Launch Scan</button>
|
||
<button class="btn-primary btn-danger hidden" id="stopBtn" onclick="stopScan()">■ Stop</button>
|
||
</div>
|
||
|
||
<!-- Custom vector selector (hidden by default) -->
|
||
<div class="panel hidden" id="vectorSelector">
|
||
<div class="panel-title">Select Vectors</div>
|
||
<div class="vector-grid" id="vectorGrid"></div>
|
||
</div>
|
||
|
||
<!-- Terminal Output -->
|
||
<div class="terminal hidden" id="terminal">
|
||
<div class="terminal-header">
|
||
<div class="terminal-dots">
|
||
<div class="terminal-dot r"></div>
|
||
<div class="terminal-dot y"></div>
|
||
<div class="terminal-dot g"></div>
|
||
</div>
|
||
<span>analyzer@kali:~$</span>
|
||
</div>
|
||
<div class="terminal-body" id="terminalBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ VECTORS PANEL ═══ -->
|
||
<div id="panel-vectors" class="hidden">
|
||
<div class="panel">
|
||
<div class="panel-title">Attack Vectors — 21 Total</div>
|
||
<div class="vector-grid" id="vectorGridFull"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ REPORTS PANEL ═══ -->
|
||
<div id="panel-reports" class="hidden">
|
||
<div class="panel">
|
||
<div class="panel-title">Scan Reports</div>
|
||
<div class="report-list" id="reportList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ─── State ───
|
||
let currentMode = 'quick';
|
||
let selectedVectors = [];
|
||
let currentScanId = null;
|
||
|
||
// ─── Panel Navigation ───
|
||
function showPanel(name) {
|
||
document.querySelectorAll('.content > div').forEach(d => d.classList.add('hidden'));
|
||
document.getElementById('panel-' + name).classList.remove('hidden');
|
||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||
if (event && event.target) {
|
||
const btn = event.target.closest('.nav-btn');
|
||
if (btn) btn.classList.add('active');
|
||
}
|
||
|
||
// Load panel content
|
||
if (name === 'vectors') loadVectorsFull();
|
||
if (name === 'reports') loadReports();
|
||
}
|
||
|
||
// ─── Mode Selection ───
|
||
function setMode(btn) {
|
||
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
currentMode = btn.dataset.mode;
|
||
|
||
const vecSel = document.getElementById('vectorSelector');
|
||
if (currentMode === 'custom') {
|
||
vecSel.classList.remove('hidden');
|
||
loadVectorSelector();
|
||
} else {
|
||
vecSel.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// ─── Vector Selector ───
|
||
async function loadVectorSelector() {
|
||
const res = await fetch('/api/vectors');
|
||
const vectors = await res.json();
|
||
const grid = document.getElementById('vectorGrid');
|
||
grid.innerHTML = '';
|
||
|
||
vectors.forEach(v => {
|
||
const card = document.createElement('div');
|
||
card.className = 'vector-card' + (selectedVectors.includes(v.num) ? ' selected' : '');
|
||
card.innerHTML = `
|
||
<div class="num">#${String(v.num).padStart(2,'0')}</div>
|
||
<div class="name">${v.name}</div>
|
||
<div class="sev sev-${v.severity.toLowerCase()}">${v.severity}</div>
|
||
`;
|
||
card.onclick = () => {
|
||
const idx = selectedVectors.indexOf(v.num);
|
||
if (idx >= 0) selectedVectors.splice(idx, 1);
|
||
else selectedVectors.push(v.num);
|
||
card.classList.toggle('selected');
|
||
};
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
// ─── Vectors Full List ───
|
||
async function loadVectorsFull() {
|
||
const res = await fetch('/api/vectors');
|
||
const vectors = await res.json();
|
||
const grid = document.getElementById('vectorGridFull');
|
||
if (grid.children.length > 0) return; // already loaded
|
||
grid.innerHTML = '';
|
||
|
||
vectors.forEach(v => {
|
||
const card = document.createElement('div');
|
||
card.className = 'vector-card';
|
||
card.innerHTML = `
|
||
<div class="num">#${String(v.num).padStart(2,'0')}</div>
|
||
<div class="name">${v.name}</div>
|
||
<div class="sev sev-${v.severity.toLowerCase()}">${v.severity}</div>
|
||
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;">${v.desc}</div>
|
||
`;
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
// ─── Reports List ───
|
||
async function loadReports() {
|
||
const res = await fetch('/api/reports');
|
||
const reports = await res.json();
|
||
const list = document.getElementById('reportList');
|
||
list.innerHTML = '';
|
||
|
||
if (reports.length === 0) {
|
||
list.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-dim)">No reports yet. Run a scan!</div>';
|
||
return;
|
||
}
|
||
|
||
reports.forEach(r => {
|
||
const item = document.createElement('div');
|
||
item.className = 'report-item';
|
||
const vulnClass = r.vulns > 0 ? 'found' : 'none';
|
||
item.innerHTML = `
|
||
<div>
|
||
<div class="target">${r.target}</div>
|
||
<div class="meta">${r.date} · ${(r.size/1024).toFixed(1)} KB</div>
|
||
</div>
|
||
<div class="vulns ${vulnClass}">${r.vulns > 0 ? '⚠ ' + r.vulns : '✅ 0'} vulns</div>
|
||
`;
|
||
item.onclick = () => window.open('/report/' + r.file, '_blank');
|
||
list.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// ─── Terminal output ───
|
||
function escapeHtml(str) {
|
||
const div = document.createElement('div');
|
||
div.appendChild(document.createTextNode(str));
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function appendTerminal(text) {
|
||
const body = document.getElementById('terminalBody');
|
||
const line = document.createElement('div');
|
||
line.className = 'terminal-line';
|
||
|
||
// Sanitize then colorize
|
||
const safe = escapeHtml(text);
|
||
const html = safe
|
||
.replace(/💥.*/g, m => `<span class="find">${m}</span>`)
|
||
.replace(/✅.*/g, m => `<span class="ok">${m}</span>`)
|
||
.replace(/ℹ️.*/g, m => `<span class="info">${m}</span>`)
|
||
.replace(/⚠️.*/g, m => `<span class="warn">${m}</span>`)
|
||
.replace(/🧠.*/g, m => `<span class="brain">${m}</span>`)
|
||
.replace(/➜.*/g, m => `<span class="dim">${m}</span>`)
|
||
.replace(/🎯.*/g, m => `<span class="accent">${m}</span>`)
|
||
.replace(/❌.*/g, m => `<span class="find">${m}</span>`);
|
||
|
||
line.innerHTML = html;
|
||
body.appendChild(line);
|
||
body.scrollTop = body.scrollHeight;
|
||
}
|
||
|
||
// ─── Timer ───
|
||
let timerInterval = null;
|
||
function startTimer() {
|
||
const start = Date.now();
|
||
timerInterval = setInterval(() => {
|
||
const elapsed = Math.floor((Date.now() - start) / 1000);
|
||
const m = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
||
const s = String(elapsed % 60).padStart(2, '0');
|
||
document.getElementById('timer').textContent = `${m}:${s}`;
|
||
}, 1000);
|
||
}
|
||
|
||
function stopTimer() {
|
||
if (timerInterval) clearInterval(timerInterval);
|
||
}
|
||
|
||
// ─── Scan ───
|
||
async function startScan() {
|
||
const target = document.getElementById('targetInput').value.trim();
|
||
if (!target) {
|
||
alert('Enter a target URL');
|
||
return;
|
||
}
|
||
|
||
// Show terminal
|
||
document.getElementById('terminal').classList.remove('hidden');
|
||
document.getElementById('terminalBody').innerHTML = '';
|
||
document.getElementById('scanBtn').classList.add('hidden');
|
||
document.getElementById('stopBtn').classList.remove('hidden');
|
||
|
||
// Status
|
||
document.getElementById('statusDot').className = 'status-dot running';
|
||
document.getElementById('statusText').textContent = 'Scanning...';
|
||
startTimer();
|
||
|
||
const payload = {
|
||
target: target,
|
||
mode: currentMode,
|
||
vectors: selectedVectors.join(',')
|
||
};
|
||
|
||
appendTerminal('🎯 Target: ' + target);
|
||
appendTerminal('⚡ Mode: ' + currentMode);
|
||
appendTerminal('───────────────');
|
||
|
||
try {
|
||
const res = await fetch('/api/scan', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const data = await res.json();
|
||
currentScanId = data.scan_id;
|
||
|
||
// Connect to SSE stream
|
||
const evtSource = new EventSource(`/api/scan/${data.scan_id}/stream`);
|
||
|
||
evtSource.onmessage = (event) => {
|
||
const msg = JSON.parse(event.data);
|
||
|
||
if (msg.type === 'output') {
|
||
appendTerminal(msg.data);
|
||
} else if (msg.type === 'complete') {
|
||
evtSource.close();
|
||
finishScan(msg.data);
|
||
} else if (msg.type === 'error') {
|
||
appendTerminal('❌ ' + msg.data);
|
||
evtSource.close();
|
||
finishScan({exit_code: -1});
|
||
}
|
||
};
|
||
|
||
evtSource.onerror = () => {
|
||
evtSource.close();
|
||
appendTerminal('⚠️ Connection lost. Scan may still be running.');
|
||
};
|
||
|
||
} catch (e) {
|
||
appendTerminal('❌ Failed to start scan: ' + e.message);
|
||
finishScan({});
|
||
}
|
||
}
|
||
|
||
function finishScan(data) {
|
||
stopTimer();
|
||
document.getElementById('statusDot').className = data.exit_code === 0 ? 'status-dot done' : 'status-dot error';
|
||
document.getElementById('statusText').textContent = data.exit_code === 0 ? 'Complete' : 'Failed';
|
||
document.getElementById('scanBtn').classList.remove('hidden');
|
||
document.getElementById('stopBtn').classList.add('hidden');
|
||
|
||
if (data.report) {
|
||
appendTerminal('');
|
||
appendTerminal('📄 Report: /report/' + data.report);
|
||
appendTerminal('✨ Scan complete in ' + (data.elapsed || '?'));
|
||
} else if (data.exit_code === 0) {
|
||
appendTerminal('✨ Scan complete');
|
||
}
|
||
|
||
loadReports();
|
||
}
|
||
|
||
async function stopScan() {
|
||
if (!currentScanId) return;
|
||
await fetch(`/api/scan/${currentScanId}/stop`, {method: 'POST'});
|
||
appendTerminal('⏹️ Scan stopped by user');
|
||
finishScan({exit_code: -1});
|
||
}
|
||
|
||
// ─── Init ───
|
||
loadVectorSelector();
|
||
loadReports(); // preload reports too
|
||
|
||
// Enter key starts scan
|
||
document.getElementById('targetInput').addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') startScan();
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|