remove infra.md.example, infra.md is the source of truth

This commit is contained in:
Azreen Jamal
2026-03-03 03:06:13 +08:00
parent 1ad3033cc1
commit a3c6d09350
86 changed files with 17093 additions and 39 deletions

View File

@@ -0,0 +1,910 @@
"""AYN Antivirus Dashboard — HTML Template.
Single-page application with embedded CSS and JavaScript.
All data is fetched from the ``/api/*`` endpoints.
"""
from __future__ import annotations
def get_dashboard_html() -> str:
"""Return the complete HTML dashboard as a string."""
return _HTML
_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>AYN Antivirus — Security Dashboard</title>
<style>
:root{--bg:#0a0e17;--surface:#111827;--surface2:#1a2332;--surface3:#1f2b3d;
--border:#2a3444;--text:#e2e8f0;--text-dim:#8892a4;--accent:#3b82f6;
--green:#10b981;--red:#ef4444;--orange:#f59e0b;--yellow:#eab308;
--purple:#8b5cf6;--cyan:#06b6d4;--radius:8px;--shadow:0 2px 8px rgba(0,0,0,.3)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,-apple-system,sans-serif;font-size:14px;min-height:100vh}
a{color:var(--accent);text-decoration:none}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:var(--surface)}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
/* ── Header ── */
.header{background:var(--surface);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
.header-left{display:flex;align-items:center;gap:16px}
.logo{font-size:1.3rem;font-weight:800;letter-spacing:.05em}
.logo span{color:var(--accent)}
.header-meta{display:flex;gap:20px;font-size:.82rem;color:var(--text-dim)}
.header-meta b{color:var(--text);font-weight:600}
.pulse{display:inline-block;width:8px;height:8px;background:var(--green);border-radius:50%;margin-right:6px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
/* ── Navigation ── */
.nav{background:var(--surface);border-bottom:1px solid var(--border);padding:0 24px;display:flex;gap:0;overflow-x:auto}
.nav-tab{padding:12px 20px;cursor:pointer;color:var(--text-dim);font-weight:600;font-size:.85rem;border-bottom:2px solid transparent;transition:all .2s;white-space:nowrap;user-select:none}
.nav-tab:hover{color:var(--text);background:var(--surface2)}
.nav-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
/* ── Layout ── */
.content{padding:20px 24px;max-width:1440px;margin:0 auto}
.tab-panel{display:none;animation:fadeIn .25s}
.tab-panel.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.grid{display:grid;gap:16px}
.g2{grid-template-columns:repeat(2,1fr)}
.g3{grid-template-columns:repeat(3,1fr)}
.g4{grid-template-columns:repeat(4,1fr)}
.g6{grid-template-columns:repeat(6,1fr)}
@media(max-width:900px){.g2,.g3,.g4,.g6{grid-template-columns:1fr}}
@media(min-width:901px) and (max-width:1200px){.g4{grid-template-columns:repeat(2,1fr)}.g6{grid-template-columns:repeat(3,1fr)}}
.section{margin-bottom:24px}
.section-title{font-size:1rem;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.section-title .icon{font-size:1.1rem}
/* ── Cards ── */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px;transition:border-color .2s,box-shadow .2s}
.card:hover{border-color:var(--accent);box-shadow:0 0 12px rgba(59,130,246,.1)}
.card-label{font-size:.75rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
.card-value{font-size:1.6rem;font-weight:700}
.card-sub{font-size:.78rem;color:var(--text-dim);margin-top:4px}
.card-green .card-value{color:var(--green)}
.card-red .card-value{color:var(--red)}
.card-orange .card-value{color:var(--orange)}
.card-yellow .card-value{color:var(--yellow)}
.card-accent .card-value{color:var(--accent)}
.card-purple .card-value{color:var(--purple)}
/* ── Gauge (SVG circular) ── */
.gauge-wrap{display:flex;flex-direction:column;align-items:center;padding:12px}
.gauge{position:relative;width:110px;height:110px}
.gauge svg{transform:rotate(-90deg)}
.gauge-bg{fill:none;stroke:var(--border);stroke-width:10}
.gauge-fill{fill:none;stroke-width:10;stroke-linecap:round;transition:stroke-dashoffset .8s ease}
.gauge-text{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:1.3rem;font-weight:700}
.gauge-label{margin-top:8px;font-size:.8rem;color:var(--text-dim);font-weight:600}
/* ── Badges ── */
.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
.badge-critical{background:rgba(239,68,68,.15);color:var(--red);border:1px solid var(--red)}
.badge-high{background:rgba(245,158,11,.15);color:var(--orange);border:1px solid var(--orange)}
.badge-medium{background:rgba(234,179,8,.15);color:var(--yellow);border:1px solid var(--yellow)}
.badge-low{background:rgba(16,185,129,.15);color:var(--green);border:1px solid var(--green)}
.badge-success{background:rgba(16,185,129,.15);color:var(--green);border:1px solid var(--green)}
.badge-error{background:rgba(239,68,68,.15);color:var(--red);border:1px solid var(--red)}
.badge-running{background:rgba(59,130,246,.15);color:var(--accent);border:1px solid var(--accent)}
.badge-info{background:rgba(59,130,246,.15);color:var(--accent);border:1px solid var(--accent)}
.badge-warning{background:rgba(245,158,11,.15);color:var(--orange);border:1px solid var(--orange)}
/* ── Tables ── */
.tbl-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}
table{width:100%;border-collapse:collapse}
th{background:var(--surface2);color:var(--text-dim);font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;padding:10px 12px;text-align:left;position:sticky;top:0}
td{padding:9px 12px;border-top:1px solid var(--border);font-size:.84rem;vertical-align:middle}
tr:hover td{background:rgba(59,130,246,.04)}
.mono{font-family:'Cascadia Code','Fira Code',monospace;font-size:.78rem}
.trunc{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.empty-row td{text-align:center;padding:32px;color:var(--text-dim);font-size:.95rem}
/* ── Bar Chart (CSS) ── */
.bar-chart{display:flex;align-items:flex-end;gap:4px;height:120px;padding:8px 0}
.bar-col{display:flex;flex-direction:column;align-items:center;flex:1;min-width:0}
.bar{width:100%;min-height:2px;border-radius:3px 3px 0 0;transition:height .4s;position:relative;cursor:default}
.bar:hover::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--surface3);border:1px solid var(--border);padding:3px 8px;border-radius:4px;font-size:.7rem;white-space:nowrap;z-index:5}
.bar-label{font-size:.6rem;color:var(--text-dim);margin-top:4px;writing-mode:vertical-lr;transform:rotate(180deg);max-height:40px;overflow:hidden}
.bar-threats{background:var(--red)}
.bar-scans{background:var(--accent)}
/* ── Disk bars ── */
.disk-row{display:flex;align-items:center;gap:12px;padding:6px 0}
.disk-mount{width:100px;font-size:.8rem;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.disk-bar-outer{flex:1;height:14px;background:var(--surface2);border-radius:7px;overflow:hidden}
.disk-bar-inner{height:100%;border-radius:7px;transition:width .6s}
.disk-pct{width:50px;text-align:right;font-size:.8rem;font-weight:600}
/* ── Buttons ── */
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);font-size:.82rem;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap}
.btn:hover{border-color:var(--accent);background:var(--surface3)}
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-primary:hover{background:#2563eb}
.btn-sm{padding:5px 10px;font-size:.76rem}
.btn:disabled{opacity:.5;cursor:not-allowed}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
/* ── Filter bar ── */
.filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;align-items:center}
.filters select,.filters input{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:7px 10px;border-radius:6px;font-size:.82rem}
.filters select:focus,.filters input:focus{outline:none;border-color:var(--accent)}
.filters input{min-width:200px}
/* ── Sub-tabs ── */
.sub-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
.sub-tab{padding:8px 16px;cursor:pointer;color:var(--text-dim);font-size:.82rem;font-weight:600;border-bottom:2px solid transparent;transition:all .2s}
.sub-tab:hover{color:var(--text)}
.sub-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
/* ── Pagination ── */
.pager{display:flex;align-items:center;justify-content:center;gap:12px;padding:12px;font-size:.84rem;color:var(--text-dim)}
/* ── Logs ── */
.log-view{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:8px;max-height:500px;overflow-y:auto;font-family:'Cascadia Code','Fira Code',monospace;font-size:.78rem;line-height:1.7}
.log-line{padding:2px 4px;display:flex;gap:10px;border-bottom:1px solid rgba(42,52,68,.4)}
.log-ts{color:var(--text-dim);min-width:140px}
.log-src{color:var(--purple);min-width:80px}
.log-msg{flex:1;word-break:break-all}
/* ── Toast ── */
.toast-area{position:fixed;top:70px;right:20px;z-index:200;display:flex;flex-direction:column;gap:8px}
.toast{padding:10px 18px;border-radius:6px;font-size:.84rem;font-weight:600;animation:slideIn .3s;box-shadow:var(--shadow)}
.toast-success{background:#065f46;color:#a7f3d0;border:1px solid var(--green)}
.toast-error{background:#7f1d1d;color:#fca5a5;border:1px solid var(--red)}
.toast-info{background:#1e3a5f;color:#93c5fd;border:1px solid var(--accent)}
@keyframes slideIn{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:none}}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div class="header-left">
<div class="logo">⚔️ <span>AYN</span> ANTIVIRUS</div>
<div style="font-size:.75rem;color:var(--text-dim)">Security Dashboard</div>
</div>
<div class="header-meta">
<div><span class="pulse"></span><b id="hd-host">—</b></div>
<div>Up <b id="hd-uptime">—</b></div>
<div id="hd-time">—</div>
</div>
</div>
<!-- NAV -->
<div class="nav" id="nav">
<div class="nav-tab active" data-tab="overview">📊 Overview</div>
<div class="nav-tab" data-tab="threats">🛡️ Threats</div>
<div class="nav-tab" data-tab="scans">🔍 Scans</div>
<div class="nav-tab" data-tab="definitions">📚 Definitions</div>
<div class="nav-tab" data-tab="containers">🐳 Containers</div>
<div class="nav-tab" data-tab="quarantine">🔒 Quarantine</div>
<div class="nav-tab" data-tab="logs">📋 Logs</div>
</div>
<!-- CONTENT -->
<div class="content">
<!-- ═══════ TAB: OVERVIEW ═══════ -->
<div class="tab-panel active" id="panel-overview">
<!-- Status cards -->
<div class="section">
<div class="grid g4" id="status-cards">
<div class="card card-green"><div class="card-label">Protection</div><div class="card-value" id="ov-prot">Active</div><div class="card-sub" id="ov-prot-sub">AI-powered analysis</div></div>
<div class="card card-accent"><div class="card-label">Last Scan</div><div class="card-value" id="ov-scan">—</div><div class="card-sub" id="ov-scan-sub">—</div></div>
<div class="card card-purple"><div class="card-label">Signatures</div><div class="card-value" id="ov-sigs">—</div><div class="card-sub" id="ov-sigs-sub">—</div></div>
<div class="card"><div class="card-label">Quarantine</div><div class="card-value" id="ov-quar">0</div><div class="card-sub">Isolated items</div></div>
</div>
</div>
<!-- CPU Per-Core -->
<div class="section">
<div class="section-title"><span class="icon">🧮</span> CPU Per Core <span id="cpu-summary" style="font-size:.8rem;color:var(--text-dim);margin-left:8px"></span></div>
<div class="card" style="padding:12px">
<canvas id="cpu-canvas" height="140" style="width:100%;display:block"></canvas>
</div>
</div>
<!-- Memory Breakdown -->
<div class="section">
<div class="section-title"><span class="icon">🧠</span> Memory Usage <span id="mem-summary" style="font-size:.8rem;color:var(--text-dim);margin-left:8px"></span></div>
<div class="grid g2">
<div class="card" style="padding:12px">
<canvas id="mem-canvas" height="160" style="width:100%;display:block"></canvas>
</div>
<div class="card" style="padding:12px">
<div class="card-label">Memory Breakdown</div>
<div id="mem-bars" style="margin-top:8px"></div>
<div style="margin-top:12px;border-top:1px solid var(--border);padding-top:8px">
<div class="card-label">Swap</div>
<div id="swap-bar"></div>
</div>
</div>
</div>
</div>
<!-- Load + Network + Processes -->
<div class="section">
<div class="grid g3">
<div class="card"><div class="card-label">Load Average</div><div class="card-value" id="ov-load" style="font-size:1.2rem">—</div><div class="card-sub">1 / 5 / 15 min</div></div>
<div class="card"><div class="card-label">Network Connections</div><div class="card-value" id="ov-netconn">—</div><div class="card-sub">Active inet sockets</div></div>
<div class="card"><div class="card-label">CPU Frequency</div><div class="card-value" id="ov-freq" style="font-size:1.2rem">—</div><div class="card-sub">Current MHz</div></div>
</div>
</div>
<!-- Top Processes -->
<div class="section">
<div class="section-title"><span class="icon">⚙️</span> Top Processes</div>
<div class="tbl-wrap">
<table><thead><tr><th>PID</th><th>Process</th><th>CPU %</th><th>RAM %</th></tr></thead><tbody id="proc-tbody"><tr class="empty-row"><td colspan="4">Loading…</td></tr></tbody></table>
</div>
</div>
<!-- Disk -->
<div class="section">
<div class="section-title"><span class="icon">💾</span> Disk Usage</div>
<div class="card" id="disk-area"><div style="color:var(--text-dim);padding:8px">Loading…</div></div>
</div>
<!-- Threat summary -->
<div class="section">
<div class="section-title"><span class="icon">⚠️</span> Threat Summary</div>
<div class="grid g4">
<div class="card card-red"><div class="card-label">Critical</div><div class="card-value" id="ov-tc">0</div></div>
<div class="card card-orange"><div class="card-label">High</div><div class="card-value" id="ov-th">0</div></div>
<div class="card card-yellow"><div class="card-label">Medium</div><div class="card-value" id="ov-tm">0</div></div>
<div class="card card-green"><div class="card-label">Low</div><div class="card-value" id="ov-tl">0</div></div>
</div>
</div>
<!-- Scan Activity Chart -->
<div class="section">
<div class="section-title"><span class="icon">📈</span> Scan Activity (14 days)</div>
<div class="card" style="padding:12px">
<canvas id="scan-canvas" height="160" style="width:100%;display:block"></canvas>
<div style="display:flex;gap:16px;justify-content:center;margin-top:8px;font-size:.75rem;color:var(--text-dim)">
<span>🔵 Scans</span><span>🔴 Threats Found</span>
</div>
</div>
</div>
</div>
<!-- ═══════ TAB: THREATS ═══════ -->
<div class="tab-panel" id="panel-threats">
<div class="filters">
<select id="f-severity"><option value="">All Severities</option><option value="CRITICAL">Critical</option><option value="HIGH">High</option><option value="MEDIUM">Medium</option><option value="LOW">Low</option></select>
<select id="f-type"><option value="">All Types</option><option value="MALWARE">Malware</option><option value="MINER">Miner</option><option value="SPYWARE">Spyware</option><option value="VIRUS">Virus</option><option value="ROOTKIT">Rootkit</option></select>
<input type="text" id="f-search" placeholder="Search threats…">
</div>
<div class="tbl-wrap">
<table><thead><tr><th>Time</th><th>File Path</th><th>Threat</th><th>Type</th><th>Severity</th><th>Detector</th><th>AI Verdict</th><th>Status</th><th>Actions</th></tr></thead><tbody id="threat-tbody"><tr class="empty-row"><td colspan="9">Loading…</td></tr></tbody></table>
</div>
<div class="pager"><button class="btn btn-sm" id="threat-prev">← Prev</button><span id="threat-page">Page 1</span><button class="btn btn-sm" id="threat-next">Next →</button></div>
</div>
<!-- ═══════ TAB: SCANS ═══════ -->
<div class="tab-panel" id="panel-scans">
<div class="btn-row">
<button class="btn btn-primary" id="btn-quick-scan" onclick="doAction('quick-scan',this)">⚡ Run Quick Scan</button>
<button class="btn" id="btn-full-scan" onclick="doAction('full-scan',this)">🔍 Run Full Scan</button>
</div>
<div class="section">
<div class="section-title"><span class="icon">📈</span> Scan History (30 days)</div>
<div class="card" style="padding:12px"><canvas id="scan-chart-canvas" height="160" style="width:100%;display:block"></canvas><div style="display:flex;gap:16px;justify-content:center;margin-top:8px;font-size:.75rem;color:var(--text-dim)"><span>🔵 Scans</span><span>🔴 Threats</span></div></div>
</div>
<div class="section">
<div class="section-title"><span class="icon">📋</span> Recent Scans</div>
<div class="tbl-wrap">
<table><thead><tr><th>Time</th><th>Type</th><th>Path</th><th>Files</th><th>Threats</th><th>Duration</th><th>Status</th></tr></thead><tbody id="scan-tbody"><tr class="empty-row"><td colspan="7">Loading…</td></tr></tbody></table>
</div>
</div>
</div>
<!-- ═══════ TAB: DEFINITIONS ═══════ -->
<div class="tab-panel" id="panel-definitions">
<div class="grid g4 section">
<div class="card card-purple"><div class="card-label">Hashes</div><div class="card-value" id="def-hashes">0</div></div>
<div class="card card-accent"><div class="card-label">Malicious IPs</div><div class="card-value" id="def-ips">0</div></div>
<div class="card card-orange"><div class="card-label">Domains</div><div class="card-value" id="def-domains">0</div></div>
<div class="card card-red"><div class="card-label">URLs</div><div class="card-value" id="def-urls">0</div></div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="doAction('update-sigs',this)">🔄 Update All Feeds</button>
<button class="btn btn-sm" onclick="doFeedUpdate('malwarebazaar',this)">MalwareBazaar</button>
<button class="btn btn-sm" onclick="doFeedUpdate('threatfox',this)">ThreatFox</button>
<button class="btn btn-sm" onclick="doFeedUpdate('urlhaus',this)">URLhaus</button>
<button class="btn btn-sm" onclick="doFeedUpdate('feodotracker',this)">FeodoTracker</button>
<button class="btn btn-sm" onclick="doFeedUpdate('emergingthreats',this)">EmergingThreats</button>
</div>
<div class="sub-tabs" id="def-subtabs">
<div class="sub-tab active" data-def="all">All</div>
<div class="sub-tab" data-def="hash">Hashes</div>
<div class="sub-tab" data-def="ip">IPs</div>
<div class="sub-tab" data-def="domain">Domains</div>
<div class="sub-tab" data-def="url">URLs</div>
</div>
<div class="filters"><input type="text" id="def-search" placeholder="Search definitions…" style="flex:1;max-width:400px"></div>
<div class="tbl-wrap"><table><thead id="def-thead"></thead><tbody id="def-tbody"><tr class="empty-row"><td colspan="6">Loading…</td></tr></tbody></table></div>
<div class="pager"><button class="btn btn-sm" id="def-prev" onclick="defPage(-1)">← Prev</button><span id="def-page-info">Page 1</span><button class="btn btn-sm" id="def-next" onclick="defPage(1)">Next →</button></div>
<div class="section" style="margin-top:20px">
<div class="section-title"><span class="icon">🔄</span> Recent Updates</div>
<div class="tbl-wrap"><table><thead><tr><th>Time</th><th>Feed</th><th>Hashes</th><th>IPs</th><th>Domains</th><th>URLs</th><th>Status</th></tr></thead><tbody id="sigup-tbody"></tbody></table></div>
</div>
</div>
<!-- ═══════ TAB: CONTAINERS ═══════ -->
<div class="tab-panel" id="panel-containers">
<div class="btn-row">
<button class="btn btn-primary" id="btn-scan-containers" onclick="doAction('scan-containers',this)">🐳 Scan All Containers</button>
</div>
<div class="grid g3 section">
<div class="card card-accent"><div class="card-label">Containers Found</div><div class="card-value" id="ct-count">0</div></div>
<div class="card card-green"><div class="card-label">Available Runtimes</div><div class="card-value" id="ct-runtimes" style="font-size:1rem">—</div></div>
<div class="card card-red"><div class="card-label">Container Threats</div><div class="card-value" id="ct-threats">0</div></div>
</div>
<div class="section">
<div class="section-title"><span class="icon">📦</span> Discovered Containers</div>
<div class="tbl-wrap">
<table><thead><tr><th>ID</th><th>Name</th><th>Image</th><th>Runtime</th><th>Status</th><th>IP</th><th>Ports</th><th>Action</th></tr></thead>
<tbody id="ct-tbody"><tr class="empty-row"><td colspan="8">Loading…</td></tr></tbody></table>
</div>
</div>
<div class="section">
<div class="section-title"><span class="icon">⚠️</span> Container Threats</div>
<div class="tbl-wrap">
<table><thead><tr><th>Time</th><th>Container</th><th>Threat</th><th>Type</th><th>Severity</th><th>Details</th></tr></thead>
<tbody id="ct-threat-tbody"><tr class="empty-row"><td colspan="6">No container threats ✅</td></tr></tbody></table>
</div>
</div>
</div>
<!-- ═══════ TAB: QUARANTINE ═══════ -->
<div class="tab-panel" id="panel-quarantine">
<div class="grid g2 section">
<div class="card"><div class="card-label">Total Quarantined</div><div class="card-value" id="q-count">0</div></div>
<div class="card"><div class="card-label">Vault Size</div><div class="card-value" id="q-size">0 B</div></div>
</div>
<div class="tbl-wrap"><table><thead><tr><th>ID</th><th>Original Path</th><th>Threat</th><th>Date</th><th>Size</th></tr></thead><tbody id="quar-tbody"><tr class="empty-row"><td colspan="5">Vault is empty ✅</td></tr></tbody></table></div>
</div>
<!-- ═══════ TAB: LOGS ═══════ -->
<div class="tab-panel" id="panel-logs">
<div class="btn-row"><button class="btn btn-sm" onclick="loadLogs()">🔄 Refresh</button></div>
<div class="log-view" id="log-view"><div style="color:var(--text-dim)">Loading…</div></div>
</div>
</div><!-- /content -->
<!-- Toast area -->
<div class="toast-area" id="toast-area"></div>
<script>
/* ── State ── */
let S={threats:[],threatPage:1,threatPerPage:25,defType:'all',defPage:1,defPerPage:50,defSearch:''};
const $=id=>document.getElementById(id);
const Q=(s,el)=>(el||document).querySelectorAll(s);
/* ── Helpers ── */
function fmt(n){return n==null?'':Number(n).toLocaleString()}
function fmtBytes(b){if(!b)return '0 B';const u=['B','KB','MB','GB','TB'];let i=0;let v=b;while(v>=1024&&i<u.length-1){v/=1024;i++;}return v.toFixed(i?1:0)+' '+u[i];}
function fmtDur(s){if(!s||s<0)return '0s';s=Math.round(s);if(s<60)return s+'s';if(s<3600)return Math.floor(s/60)+'m '+s%60+'s';return Math.floor(s/3600)+'h '+Math.floor(s%3600/60)+'m';}
function ago(ts){if(!ts)return '';const d=new Date(ts+'Z');const s=Math.floor((Date.now()-d)/1000);if(s<60)return s+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago';}
function sevBadge(s){const c=esc((s||'').toUpperCase());const cl=c.toLowerCase().replace(/[^a-z]/g,'');return `<span class="badge badge-${cl}">${c}</span>`;}
function statusBadge(s){const m={completed:'success',success:'success',running:'running',failed:'error',error:'error'};const safe=esc(s||'');const cl=(m[s]||'info').replace(/[^a-z]/g,'');return `<span class="badge badge-${cl}">${safe}</span>`;}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML;}
function trunc(s,n){s=s||'';return s.length>n?s.slice(0,n)+'':s;}
function setGauge(id,pct,color){const g=$(id);if(!g)return;const c=g.querySelector('.gauge-fill');const t=g.querySelector('.gauge-text');const off=314-(314*Math.min(pct,100)/100);c.style.strokeDashoffset=off;if(color)c.style.stroke=color;t.textContent=Math.round(pct)+'%';}
function gaugeColor(p){return p>90?'var(--red)':p>70?'var(--orange)':p>50?'var(--yellow)':'var(--green)';}
/* ── Toast ── */
function toast(msg,type='info'){const t=document.createElement('div');t.className='toast toast-'+type;t.textContent=msg;$('toast-area').appendChild(t);setTimeout(()=>t.remove(),4000);}
/* ── API ── */
async function api(path){try{const r=await fetch(path);if(!r.ok)throw new Error(r.statusText);return await r.json();}catch(e){console.error('API error:',path,e);return null;}}
/* ── Tab switching ── */
Q('.nav-tab').forEach(t=>t.addEventListener('click',()=>{
Q('.nav-tab').forEach(x=>x.classList.remove('active'));
Q('.tab-panel').forEach(x=>x.classList.remove('active'));
t.classList.add('active');
$('panel-'+t.dataset.tab).classList.add('active');
if(t.dataset.tab==='threats')loadThreats();
if(t.dataset.tab==='scans')loadScans();
if(t.dataset.tab==='definitions')loadDefs();
if(t.dataset.tab==='containers')loadContainers();
if(t.dataset.tab==='quarantine')loadQuarantine();
if(t.dataset.tab==='logs')loadLogs();
}));
/* ═══════ OVERVIEW ═══════ */
/* ── Canvas Chart Helpers ── */
let _cpuHistory=[];const _CPU_HIST_MAX=60;
let _memHistory=[];const _MEM_HIST_MAX=60;
function drawLineChart(canvasId,datasets,opts={}){
const cv=$(canvasId);if(!cv)return;
const dpr=window.devicePixelRatio||1;
const rect=cv.getBoundingClientRect();
cv.width=rect.width*dpr;cv.height=(opts.height||rect.height)*dpr;
const ctx=cv.getContext('2d');ctx.scale(dpr,dpr);
const W=rect.width,H=opts.height||rect.height;
const pad={t:10,r:10,b:24,l:42};
const cw=W-pad.l-pad.r,ch=H-pad.t-pad.b;
// Background
ctx.fillStyle='#0d1117';ctx.fillRect(0,0,W,H);
// Grid
const gridLines=opts.gridLines||5;
const maxVal=opts.maxVal||Math.max(...datasets.flatMap(d=>d.data),1);
ctx.strokeStyle='#1e293b';ctx.lineWidth=1;ctx.font='10px system-ui';ctx.fillStyle='#6b7280';
for(let i=0;i<=gridLines;i++){
const y=pad.t+ch-(ch*i/gridLines);
ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(pad.l+cw,y);ctx.stroke();
const v=((maxVal*i/gridLines)).toFixed(opts.decimals||0);
ctx.fillText(v+(opts.unit||''),2,y+3);
}
// Data lines
datasets.forEach(ds=>{
if(!ds.data.length)return;
const n=ds.data.length;
ctx.beginPath();ctx.strokeStyle=ds.color;ctx.lineWidth=ds.lineWidth||2;
ds.data.forEach((v,i)=>{
const x=pad.l+(cw*i/(n-1||1));
const y=pad.t+ch-ch*(Math.min(v,maxVal)/maxVal);
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
});
ctx.stroke();
// Fill
if(ds.fill){
const n2=ds.data.length;
ctx.lineTo(pad.l+cw,pad.t+ch);ctx.lineTo(pad.l,pad.t+ch);ctx.closePath();
ctx.fillStyle=ds.fill;ctx.fill();
}
});
// Labels
if(opts.labels&&opts.labels.length){
ctx.fillStyle='#6b7280';ctx.font='9px system-ui';ctx.textAlign='center';
const n=opts.labels.length;
opts.labels.forEach((l,i)=>{
if(i%Math.ceil(n/8)!==0&&i!==n-1)return;
const x=pad.l+(cw*i/(n-1||1));
ctx.fillText(l,x,H-2);
});
}
}
function drawBarChart(canvasId,data,opts={}){
const cv=$(canvasId);if(!cv||!data.length)return;
const dpr=window.devicePixelRatio||1;
const rect=cv.getBoundingClientRect();
cv.width=rect.width*dpr;cv.height=(opts.height||rect.height)*dpr;
const ctx=cv.getContext('2d');ctx.scale(dpr,dpr);
const W=rect.width,H=opts.height||rect.height;
const pad={t:10,r:10,b:28,l:42};
const cw=W-pad.l-pad.r,ch=H-pad.t-pad.b;
ctx.fillStyle='#0d1117';ctx.fillRect(0,0,W,H);
const maxVal=opts.maxVal||Math.max(...data.flatMap(d=>[(d.scans||0),(d.threats||0)]),1);
// Grid
ctx.strokeStyle='#1e293b';ctx.lineWidth=1;ctx.font='10px system-ui';ctx.fillStyle='#6b7280';
for(let i=0;i<=4;i++){
const y=pad.t+ch-(ch*i/4);
ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(pad.l+cw,y);ctx.stroke();
ctx.fillText(Math.round(maxVal*i/4),2,y+3);
}
const n=data.length;const bw=Math.max((cw/n)*0.35,2);const gap=cw/n;
data.forEach((d,i)=>{
const x=pad.l+gap*i+gap*0.15;
const sh=ch*(d.scans||0)/maxVal;
const th=ch*(d.threats||0)/maxVal;
// Scans bar
ctx.fillStyle='#3b82f6';ctx.fillRect(x,pad.t+ch-sh,bw,sh);
// Threats bar
ctx.fillStyle='#ef4444';ctx.fillRect(x+bw+1,pad.t+ch-th,bw,th);
// Label
ctx.fillStyle='#6b7280';ctx.font='9px system-ui';ctx.textAlign='center';
const day=(d.day||'').slice(5);
ctx.fillText(day,x+bw,H-4);
});
}
function drawCoreChart(canvasId,cores){
const cv=$(canvasId);if(!cv||!cores.length)return;
const dpr=window.devicePixelRatio||1;
const rect=cv.getBoundingClientRect();
cv.width=rect.width*dpr;cv.height=140*dpr;
const ctx=cv.getContext('2d');ctx.scale(dpr,dpr);
const W=rect.width,H=140;
const pad={t:8,r:8,b:20,l:8};
const n=cores.length;const gap=4;
const bw=Math.min((W-pad.l-pad.r-(n-1)*gap)/n,60);
ctx.fillStyle='#0d1117';ctx.fillRect(0,0,W,H);
cores.forEach((pct,i)=>{
const x=pad.l+i*(bw+gap);
const barH=(H-pad.t-pad.b)*(pct/100);
const c=pct>90?'#ef4444':pct>70?'#f59e0b':pct>50?'#eab308':'#3b82f6';
// Background
ctx.fillStyle='#1e293b';ctx.fillRect(x,pad.t,bw,H-pad.t-pad.b);
// Bar
ctx.fillStyle=c;ctx.fillRect(x,H-pad.b-barH,bw,barH);
// Label
ctx.fillStyle='#e2e8f0';ctx.font='bold 10px system-ui';ctx.textAlign='center';
ctx.fillText(Math.round(pct)+'%',x+bw/2,H-pad.b-barH-4>pad.t?H-pad.b-barH-4:pad.t+12);
ctx.fillStyle='#6b7280';ctx.font='9px system-ui';
ctx.fillText('C'+i,x+bw/2,H-4);
});
}
function renderMemBars(h){
const total=h.mem_total||1;
const used=h.mem_used||0;
const cached=h.mem_cached||0;
const buffers=h.mem_buffers||0;
const avail=h.mem_available||0;
const app=used-cached-buffers;
const items=[
{label:'App/Used',val:Math.max(app,0),color:'var(--purple)'},
{label:'Cached',val:cached,color:'var(--cyan)'},
{label:'Buffers',val:buffers,color:'var(--accent)'},
{label:'Available',val:avail,color:'var(--green)'},
];
$('mem-bars').innerHTML=items.map(it=>{
const pct=(it.val/total*100).toFixed(1);
return `<div class="disk-row"><div class="disk-mount" style="width:70px"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${it.color};margin-right:4px"></span>${it.label}</div><div class="disk-bar-outer"><div class="disk-bar-inner" style="width:${pct}%;background:${it.color}"></div></div><div class="disk-pct">${fmtBytes(it.val)}</div></div>`;
}).join('');
// Swap
const spct=h.swap_total?(h.swap_used/h.swap_total*100).toFixed(1):0;
$('swap-bar').innerHTML=h.swap_total?`<div class="disk-row"><div class="disk-mount" style="width:70px">${fmtBytes(h.swap_used)}/${fmtBytes(h.swap_total)}</div><div class="disk-bar-outer"><div class="disk-bar-inner" style="width:${spct}%;background:var(--orange)"></div></div><div class="disk-pct">${spct}%</div></div>`:'<div style="color:var(--text-dim);font-size:.8rem">No swap</div>';
}
async function loadOverview(){
const [st,h,ts,ch]=await Promise.all([api('/api/status'),api('/api/health'),api('/api/threat-stats'),api('/api/scan-chart?days=14')]);
if(st){
$('hd-host').textContent=st.hostname||'';
$('hd-uptime').textContent=fmtDur(st.uptime_seconds);
$('hd-time').textContent=st.server_time||'';
const ls=st.last_scan;
$('ov-scan').textContent=ls?ago(ls.timestamp):'Never';
$('ov-scan-sub').textContent=ls?`${fmt(ls.files_scanned)} files, ${ls.threats_found} threats`:'No scans yet';
const sig=st.signatures||{};
$('ov-sigs').textContent=fmt((sig.total_hashes||0)+(sig.total_ips||0)+(sig.total_domains||0)+(sig.total_urls||0));
$('ov-sigs-sub').textContent=sig.last_update?'Updated '+ago(sig.last_update):'Not updated';
$('ov-quar').textContent=fmt(st.quarantine_count);
}
if(h){
// CPU per-core chart
const cores=h.cpu_per_core||[];
if(cores.length){
drawCoreChart('cpu-canvas',cores);
$('cpu-summary').textContent=`${cores.length} cores @ ${h.cpu_freq_mhz||'?'} MHz — avg ${Math.round(h.cpu_percent)}%`;
}
// CPU history
_cpuHistory.push(h.cpu_percent||0);if(_cpuHistory.length>_CPU_HIST_MAX)_cpuHistory.shift();
// Memory
_memHistory.push(h.mem_percent||0);if(_memHistory.length>_MEM_HIST_MAX)_memHistory.shift();
drawLineChart('mem-canvas',[
{data:_memHistory,color:'#8b5cf6',fill:'rgba(139,92,246,0.1)',lineWidth:2},
],{maxVal:100,unit:'%',height:160,gridLines:4});
$('mem-summary').textContent=`${fmtBytes(h.mem_used)} / ${fmtBytes(h.mem_total)} (${h.mem_percent?.toFixed(1)}%)`;
renderMemBars(h);
// Load / Net / Freq
const la=h.load_avg||[0,0,0];
$('ov-load').textContent=la.map(v=>v.toFixed(2)).join(' / ');
$('ov-netconn').textContent=fmt(h.net_connections||0);
$('ov-freq').textContent=h.cpu_freq_mhz?h.cpu_freq_mhz+' MHz':'';
// Top processes
const procs=h.top_processes||[];
const ptb=$('proc-tbody');
if(procs.length){
ptb.innerHTML=procs.map(p=>{
const cpuC=p.cpu>50?'var(--red)':p.cpu>20?'var(--orange)':'var(--text)';
return `<tr><td class="mono">${p.pid}</td><td>${esc(p.name)}</td><td style="color:${cpuC};font-weight:600">${p.cpu}%</td><td>${p.mem}%</td></tr>`;
}).join('');
}else{ptb.innerHTML='<tr class="empty-row"><td colspan="4">No active processes</td></tr>';}
// Disks
const da=$('disk-area');
const disks=h.disk_usage||[];
if(disks.length){
da.innerHTML=disks.map(d=>{
const p=d.percent||0;const c=p>90?'var(--red)':p>70?'var(--orange)':'var(--accent)';
return `<div class="disk-row"><div class="disk-mount" title="${esc(d.mount)}">${esc(d.mount)}</div><div class="disk-bar-outer"><div class="disk-bar-inner" style="width:${p}%;background:${c}"></div></div><div class="disk-pct">${p.toFixed(1)}%</div><div style="font-size:.75rem;color:var(--text-dim);min-width:100px">${fmtBytes(d.used)} / ${fmtBytes(d.total)}</div></div>`;
}).join('');
}else{da.innerHTML='<div style="color:var(--text-dim);padding:8px">No disk info</div>';}
}
if(ts){
const bs=ts.by_severity||{};
$('ov-tc').textContent=fmt(bs.CRITICAL||0);
$('ov-th').textContent=fmt(bs.HIGH||0);
$('ov-tm').textContent=fmt(bs.MEDIUM||0);
$('ov-tl').textContent=fmt(bs.LOW||0);
}
if(ch&&ch.chart&&ch.chart.length){
drawBarChart('scan-canvas',ch.chart.slice(-14),{height:160});
}
}
/* ═══════ THREATS ═══════ */
async function loadThreats(){
const d=await api(`/api/threats?limit=200`);
if(!d)return;
S.threats=d.threats||[];
renderThreats();
}
function renderThreats(){
const sev=$('f-severity').value.toUpperCase();
const typ=$('f-type').value.toUpperCase();
const q=$('f-search').value.toLowerCase();
let f=S.threats;
if(sev)f=f.filter(t=>(t.severity||'').toUpperCase()===sev);
if(typ)f=f.filter(t=>(t.threat_type||'').toUpperCase()===typ);
if(q)f=f.filter(t=>(t.threat_name||'').toLowerCase().includes(q)||(t.file_path||'').toLowerCase().includes(q));
const total=f.length;const pages=Math.max(Math.ceil(total/S.threatPerPage),1);
S.threatPage=Math.min(S.threatPage,pages);
const start=(S.threatPage-1)*S.threatPerPage;
const slice=f.slice(start,start+S.threatPerPage);
const tb=$('threat-tbody');
if(!slice.length){tb.innerHTML='<tr class="empty-row"><td colspan="9">No threats detected ✅</td></tr>';
}else{tb.innerHTML=slice.map(t=>{
const act=t.action_taken||'detected';
const st=act==='detected'?'<span class="badge badge-warning">detected</span>':act==='quarantined'?'<span class="badge badge-info">quarantined</span>':statusBadge(act);
let btns='';
if(act==='detected'||act==='monitoring'){
btns=`<div style="display:flex;gap:4px;flex-wrap:wrap"><button class="btn btn-sm" style="background:var(--purple);color:#fff;border-color:var(--purple);font-size:.7rem;padding:3px 8px" onclick="aiAnalyze(${t.id},this)">🧠 AI Analyze</button><button class="btn btn-sm" style="background:var(--red);color:#fff;border-color:var(--red);font-size:.7rem;padding:3px 8px" onclick="threatAction('quarantine',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','${esc(t.threat_name).replace(/'/g,"\\'")}',this)">🔒 Quarantine</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('delete-threat',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','',this)">🗑️ Delete</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('whitelist',${t.id},'','',this)">✅ Ignore</button></div>`;
} else if(act==='quarantined'){
btns=`<div style="display:flex;gap:4px;flex-wrap:wrap"><button class="btn btn-sm" style="background:var(--purple);color:#fff;border-color:var(--purple);font-size:.7rem;padding:3px 8px" onclick="aiAnalyze(${t.id},this)">🧠 AI Analyze</button><button class="btn btn-sm" style="background:var(--green);color:#fff;border-color:var(--green);font-size:.7rem;padding:3px 8px" onclick="threatAction('restore',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','',this)">♻️ Restore</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('delete-threat',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','',this)">🗑️ Delete</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('whitelist',${t.id},'','',this)">✅ Ignore</button></div>`;
} else {
btns=`<span style="color:var(--text-dim);font-size:.75rem">${esc(act)}</span>`;
}
const det=t.details||'';
let aiCol='<span style="color:var(--text-dim);font-size:.75rem">—</span>';
const aiMatch=det.match(/\[AI:\s*(\w+)\s+(\d+)%\]\s*(.*)/);
if(aiMatch){const v=aiMatch[1],c=aiMatch[2],rsn=aiMatch[3];const vc=v==='safe'?'var(--green)':v==='threat'?'var(--red)':'var(--orange)';aiCol=`<div style="font-size:.75rem"><span style="color:${vc};font-weight:700">${v.toUpperCase()}</span> <span style="color:var(--text-dim)">${c}%</span><div style="color:var(--text-dim);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(rsn)}">${esc(rsn)}</div></div>`;}
return `<tr><td>${ago(t.timestamp)}</td><td class="mono trunc" title="${esc(t.file_path)}">${esc(trunc(t.file_path,50))}</td><td>${esc(t.threat_name)}</td><td>${esc(t.threat_type)}</td><td>${sevBadge(t.severity)}</td><td>${esc(t.detector)}</td><td>${aiCol}</td><td>${st}</td><td>${btns}</td></tr>`;
}).join('');}
$('threat-page').textContent=`Page ${S.threatPage} of ${pages} (${total})`;
}
$('threat-prev').onclick=()=>{S.threatPage=Math.max(1,S.threatPage-1);renderThreats();};
$('threat-next').onclick=()=>{S.threatPage++;renderThreats();};
$('f-severity').onchange=$('f-type').onchange=()=>{S.threatPage=1;renderThreats();};
let _tTimer;$('f-search').oninput=()=>{clearTimeout(_tTimer);_tTimer=setTimeout(()=>{S.threatPage=1;renderThreats();},300);};
/* ═══════ SCANS ═══════ */
async function loadScans(){
const [sc,ch]=await Promise.all([api('/api/scans?limit=30'),api('/api/scan-chart?days=30')]);
if(sc){
const tb=$('scan-tbody');
const scans=sc.scans||[];
if(!scans.length){tb.innerHTML='<tr class="empty-row"><td colspan="7">No scans yet</td></tr>';
}else{tb.innerHTML=scans.map(s=>`<tr><td>${ago(s.timestamp)}</td><td>${esc(s.scan_type)}</td><td class="mono trunc" title="${esc(s.scan_path)}">${esc(trunc(s.scan_path,40))}</td><td>${fmt(s.files_scanned)}</td><td>${s.threats_found?'<span style="color:var(--red)">'+s.threats_found+'</span>':'0'}</td><td>${fmtDur(s.duration_seconds)}</td><td>${statusBadge(s.status)}</td></tr>`).join('');}
}
if(ch&&ch.chart&&ch.chart.length)drawBarChart('scan-chart-canvas',ch.chart,{height:160});
}
/* ═══════ DEFINITIONS ═══════ */
Q('#def-subtabs .sub-tab').forEach(t=>t.addEventListener('click',()=>{
Q('#def-subtabs .sub-tab').forEach(x=>x.classList.remove('active'));
t.classList.add('active');S.defType=t.dataset.def;S.defPage=1;loadDefs();
}));
let _dTimer;$('def-search').oninput=()=>{clearTimeout(_dTimer);_dTimer=setTimeout(()=>{S.defPage=1;S.defSearch=$('def-search').value;loadDefs();},400);};
function defPage(d){S.defPage=Math.max(1,S.defPage+d);loadDefs();}
async function loadDefs(){
const typ=S.defType==='all'?'':S.defType;
const q=encodeURIComponent(S.defSearch||'');
const [dd,su]=await Promise.all([
api(`/api/definitions?type=${typ}&page=${S.defPage}&per_page=${S.defPerPage}&search=${q}`),
api('/api/sig-updates?limit=10')
]);
if(dd){
$('def-hashes').textContent=fmt(dd.total_hashes);
$('def-ips').textContent=fmt(dd.total_ips);
$('def-domains').textContent=fmt(dd.total_domains);
$('def-urls').textContent=fmt(dd.total_urls);
renderDefTable(dd);
const total=dd.total_hashes+dd.total_ips+dd.total_domains+dd.total_urls;
const pages=Math.max(Math.ceil(total/S.defPerPage),1);
$('def-page-info').textContent=`Page ${S.defPage} of ${pages}`;
}
if(su){
const tb=$('sigup-tbody');
const ups=su.updates||[];
if(!ups.length){tb.innerHTML='<tr class="empty-row"><td colspan="7">No updates yet</td></tr>';
}else{tb.innerHTML=ups.map(u=>`<tr><td>${ago(u.timestamp)}</td><td>${esc(u.feed_name)}</td><td>${fmt(u.hashes_added)}</td><td>${fmt(u.ips_added)}</td><td>${fmt(u.domains_added)}</td><td>${fmt(u.urls_added)}</td><td>${statusBadge(u.status)}</td></tr>`).join('');}
}
}
function renderDefTable(dd){
const th=$('def-thead');const tb=$('def-tbody');
let rows=[];const t=S.defType;
if(t==='all'||t==='hash'){
th.innerHTML='<tr><th>Hash</th><th>Threat Name</th><th>Type</th><th>Severity</th><th>Source</th><th>Date</th></tr>';
rows=rows.concat((dd.hashes||[]).map(r=>`<tr><td class="mono">${esc(trunc(r.hash,16))}</td><td>${esc(r.threat_name)}</td><td>${esc(r.threat_type)}</td><td>${sevBadge(r.severity)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
}
if(t==='all'||t==='ip'){
if(t==='ip')th.innerHTML='<tr><th>IP Address</th><th>Threat Name</th><th>Type</th><th>Source</th><th>Date</th></tr>';
rows=rows.concat((dd.ips||[]).map(r=>`<tr><td class="mono">${esc(r.ip)}</td><td>${esc(r.threat_name)}</td><td>${esc(r.type)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
}
if(t==='all'||t==='domain'){
if(t==='domain')th.innerHTML='<tr><th>Domain</th><th>Threat Name</th><th>Type</th><th>Source</th><th>Date</th></tr>';
rows=rows.concat((dd.domains||[]).map(r=>`<tr><td class="mono">${esc(r.domain)}</td><td>${esc(r.threat_name)}</td><td>${esc(r.type)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
}
if(t==='all'||t==='url'){
if(t==='url')th.innerHTML='<tr><th>URL</th><th>Threat Name</th><th>Type</th><th>Source</th><th>Date</th></tr>';
rows=rows.concat((dd.urls||[]).map(r=>`<tr><td class="mono trunc" title="${esc(r.url)}">${esc(trunc(r.url,60))}</td><td>${esc(r.threat_name)}</td><td>${esc(r.type)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
}
if(t==='all'&&!rows.length&&!dd.hashes?.length)th.innerHTML='<tr><th>Hash</th><th>Threat Name</th><th>Type</th><th>Severity</th><th>Source</th><th>Date</th></tr>';
tb.innerHTML=rows.length?rows.join(''):'<tr class="empty-row"><td colspan="6">No definitions found. Run an update to fetch threat feeds.</td></tr>';
}
/* ═══════ CONTAINERS ═══════ */
async function loadContainers(){
const [cl,cs]=await Promise.all([api('/api/containers'),api('/api/container-scan')]);
if(cl){
$('ct-count').textContent=fmt(cl.count);
$('ct-runtimes').textContent=cl.runtimes.length?cl.runtimes.join(', '):'None detected';
const tb=$('ct-tbody');
const cc=cl.containers||[];
if(!cc.length){tb.innerHTML='<tr class="empty-row"><td colspan="8">No containers found. Install Docker, Podman, or LXC.</td></tr>';
}else{
tb.innerHTML=cc.map(c=>{
const st=c.status==='running'?'<span class="badge badge-success">running</span>':c.status==='stopped'?'<span class="badge badge-error">stopped</span>':'<span class="badge badge-warning">'+esc(c.status)+'</span>';
const ports=(c.ports||[]).slice(0,3).join(', ')||(c.status==='running'?'':'');
return `<tr><td class="mono">${esc(trunc(c.container_id,12))}</td><td>${esc(c.name)}</td><td class="mono trunc" title="${esc(c.image)}">${esc(trunc(c.image,30))}</td><td>${esc(c.runtime)}</td><td>${st}</td><td class="mono">${esc(c.ip_address||'')}</td><td class="mono" style="font-size:.72rem">${esc(ports)}</td><td><button class="btn btn-sm" onclick="scanSingleContainer('${esc(c.container_id)}',this)">Scan</button></td></tr>`;
}).join('');
}
}
if(cs){
const threats=cs.threats||[];
$('ct-threats').textContent=fmt(threats.length);
const tb=$('ct-threat-tbody');
if(!threats.length){tb.innerHTML='<tr class="empty-row"><td colspan="6">No container threats ✅</td></tr>';
}else{tb.innerHTML=threats.map(t=>`<tr><td>${ago(t.timestamp)}</td><td>${esc(trunc(t.file_path,30))}</td><td>${esc(t.threat_name)}</td><td>${esc(t.threat_type)}</td><td>${sevBadge(t.severity)}</td><td class="trunc" title="${esc(t.details)}">${esc(trunc(t.details,60))}</td></tr>`).join('');}
}
}
async function scanSingleContainer(id,btn){
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span>';btn.disabled=true;
toast(`Scanning container ${id.slice(0,12)}…`,'info');
try{
const r=await fetch('/api/actions/scan-container',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':window.AYN_API_KEY||''},body:JSON.stringify({container_id:id})});
const ct=r.headers.get('content-type')||'';
if(!ct.includes('application/json')){const t=await r.text();toast('Server error: '+t.slice(0,100),'error');btn.innerHTML=orig;btn.disabled=false;return;}
const d=await r.json();
if(d.status==='error'){toast(d.error||'Failed','error');}
else{toast(`Container scan: ${d.threats_found||0} threats found`,'success');}
loadContainers();
}catch(e){toast('Failed: '+e.message,'error');}
btn.innerHTML=orig;btn.disabled=false;
}
/* ═══════ QUARANTINE ═══════ */
async function loadQuarantine(){
const d=await api('/api/quarantine');
if(!d)return;
$('q-count').textContent=fmt(d.count);
$('q-size').textContent=fmtBytes(d.total_size);
const tb=$('quar-tbody');
const items=d.items||[];
if(!items.length){tb.innerHTML='<tr class="empty-row"><td colspan="5">Vault is empty ✅</td></tr>';
}else{tb.innerHTML=items.map(i=>`<tr><td class="mono">${esc(trunc(i.id,12))}</td><td class="mono trunc" title="${esc(i.original_path)}">${esc(trunc(i.original_path,50))}</td><td>${esc(i.threat_name)}</td><td>${ago(i.quarantine_date)}</td><td>${fmtBytes(i.size||i.file_size||0)}</td></tr>`).join('');}
}
/* ═══════ LOGS ═══════ */
async function loadLogs(){
const d=await api('/api/logs?limit=50');
if(!d)return;
const lv=$('log-view');
const logs=d.logs||[];
if(!logs.length){lv.innerHTML='<div style="color:var(--text-dim);padding:12px">No activity yet.</div>';return;}
lv.innerHTML=logs.map(l=>{
const lc=l.level==='ERROR'?'var(--red)':l.level==='WARNING'?'var(--orange)':'var(--accent)';
return `<div class="log-line"><span class="log-ts">${l.timestamp||''}</span><span style="color:${lc};font-weight:700;min-width:56px">${l.level}</span><span class="log-src">${esc(l.source)}</span><span class="log-msg">${esc(l.message)}</span></div>`;
}).join('');
lv.scrollTop=0;
}
/* ═══════ AI ANALYSIS ═══════ */
async function aiAnalyze(threatId,btn){
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span> Analyzing…';btn.disabled=true;
try{
const r=await fetch('/api/actions/ai-analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({threat_id:threatId})});
const ct=r.headers.get('content-type')||'';
if(!ct.includes('application/json')){toast('Server error','error');btn.innerHTML=orig;btn.disabled=false;return;}
const d=await r.json();
if(d.status==='ok'){
const emoji=d.verdict==='safe'?'':d.verdict==='threat'?'🚨':'⚠️';
const color=d.verdict==='safe'?'success':d.verdict==='threat'?'error':'info';
toast(`${emoji} AI: ${d.verdict.toUpperCase()} (${d.confidence}%) — ${d.reason}`,color);
if(d.verdict==='safe'){toast(`Recommended: ${d.recommended_action}`,'info');}
loadThreats();
} else {toast(d.error||'AI analysis failed','error');}
}catch(e){toast('Failed: '+e.message,'error');}
btn.innerHTML=orig;btn.disabled=false;
}
/* ═══════ THREAT ACTIONS ═══════ */
async function threatAction(action,threatId,filePath,threatName,btn){
const labels={'quarantine':'Quarantine','delete-threat':'Delete','whitelist':'Whitelist'};
if(action==='delete-threat'&&!confirm('Permanently delete '+filePath+'?'))return;
const orig=btn.parentElement.innerHTML;btn.parentElement.innerHTML='<span class="spinner"></span>';
try{
const body={threat_id:threatId};
if(filePath)body.file_path=filePath;
if(threatName)body.threat_name=threatName;
const r=await fetch('/api/actions/'+action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const ct=r.headers.get('content-type')||'';
if(!ct.includes('application/json')){toast('Server error','error');return;}
const d=await r.json();
if(d.status==='ok'){toast((labels[action]||action)+' done','success');loadThreats();}
else{toast(d.error||'Failed','error');}
}catch(e){toast('Failed: '+e.message,'error');}
}
/* ═══════ ACTIONS ═══════ */
async function doAction(action,btn){
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span> Running…';btn.disabled=true;
toast(`${action} started…`,'info');
try{
const r=await fetch(`/api/actions/${action}`,{method:'POST',headers:{'X-API-Key':window.AYN_API_KEY||''}});
const ct=r.headers.get('content-type')||'';
if(!ct.includes('application/json')){const t=await r.text();toast('Server error: '+(r.status>=400?r.status+' ':'')+t.slice(0,100),'error');btn.innerHTML=orig;btn.disabled=false;return;}
const d=await r.json();
if(d.status==='error'){toast(d.error||'Failed','error');}
else{toast(`${action} completed`,'success');}
refreshAll();
}catch(e){toast('Request failed: '+e.message,'error');}
btn.innerHTML=orig;btn.disabled=false;
}
async function doFeedUpdate(feed,btn){
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span>';btn.disabled=true;
toast(`Updating ${feed}…`,'info');
try{
const r=await fetch('/api/actions/update-feed',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':window.AYN_API_KEY||''},body:JSON.stringify({feed})});
const ct=r.headers.get('content-type')||'';
if(!ct.includes('application/json')){const t=await r.text();toast(`${feed}: Server error `+t.slice(0,100),'error');btn.innerHTML=orig;btn.disabled=false;return;}
const d=await r.json();
if(d.status==='error'){toast(`${feed}: ${d.error}`,'error');}
else{toast(`${feed} updated`,'success');}
loadDefs();
}catch(e){toast('Failed: '+e.message,'error');}
btn.innerHTML=orig;btn.disabled=false;
}
/* ═══════ REFRESH ═══════ */
async function refreshAll(){
await loadOverview();
const active=document.querySelector('.nav-tab.active');
if(active){
const tab=active.dataset.tab;
if(tab==='threats')loadThreats();
if(tab==='scans')loadScans();
if(tab==='definitions')loadDefs();
if(tab==='containers')loadContainers();
if(tab==='quarantine')loadQuarantine();
if(tab==='logs')loadLogs();
}
}
/* ── Boot ── */
refreshAll();
setInterval(refreshAll,30000);
</script>
</body>
</html>"""