<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SkillAudit — Threat Dashboard</title>
<meta name="description" content="Live security intelligence for the AI agent skill ecosystem. Real-time threat monitoring, risk trends, and flagged domains.">
<meta property="og:title" content="SkillAudit Threat Dashboard">
<meta property="og:description" content="Live security intelligence for the AI agent skill ecosystem.">
<meta property="og:url" content="https://skillaudit.vercel.app/dashboard">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0a0f;--surface:#12121a;--surface2:#1a1a26;--border:#2a2a3a;--text:#e0e0e8;--text2:#8888a0;--green:#22c55e;--yellow:#eab308;--orange:#f97316;--red:#ef4444;--purple:#a855f7;--blue:#3b82f6;--cyan:#06b6d4}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
a{color:var(--cyan);text-decoration:none}
a:hover{text-decoration:underline}
.header{padding:24px 32px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
.header h1{font-size:20px;font-weight:700;display:flex;align-items:center;gap:8px}
.header h1 span{color:var(--cyan)}
.header .meta{font-size:13px;color:var(--text2);display:flex;align-items:center;gap:16px}
.live-dot{width:8px;height:8px;border-radius:50%;background:var(--green);display:inline-block;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;padding:24px 32px}
.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;transition:border-color .2s}
.card:hover{border-color:var(--cyan)}
.card h3{font-size:12px;text-transform:uppercase;letter-spacing:1px;color:var(--text2);margin-bottom:12px}
.card .big{font-size:36px;font-weight:700;line-height:1.1}
.card .sub{font-size:13px;color:var(--text2);margin-top:4px}
.wide{grid-column:1/-1}
.half{grid-column:span 2}
@media(max-width:768px){.half{grid-column:1/-1}}
/* Risk distribution bar */
.risk-bar{display:flex;height:32px;border-radius:8px;overflow:hidden;margin-top:12px}
.risk-bar div{transition:width .5s ease}
.risk-bar .clean{background:var(--green)}
.risk-bar .low{background:var(--yellow)}
.risk-bar .moderate{background:var(--orange)}
.risk-bar .high{background:var(--red)}
.risk-bar .critical{background:var(--purple)}
.risk-legend{display:flex;gap:16px;margin-top:8px;flex-wrap:wrap}
.risk-legend span{font-size:12px;color:var(--text2);display:flex;align-items:center;gap:4px}
.risk-legend .dot{width:10px;height:10px;border-radius:3px;display:inline-block}
/* Threat table */
.threat-list{list-style:none}
.threat-list li{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)}
.threat-list li:last-child{border:none}
.sev{font-size:11px;font-weight:600;padding:2px 8px;border-radius:4px;text-transform:uppercase;min-width:60px;text-align:center}
.sev.critical{background:rgba(168,85,247,.2);color:var(--purple)}
.sev.high{background:rgba(239,68,68,.2);color:var(--red)}
.sev.medium{background:rgba(249,115,22,.2);color:var(--orange)}
.sev.low{background:rgba(234,179,8,.2);color:var(--yellow)}
.threat-info{flex:1;min-width:0}
.threat-info .name{font-size:14px;font-weight:500}
.threat-info .domain{font-size:12px;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.threat-info .time{font-size:11px;color:var(--text2)}
/* Rules table */
.rule-list{list-style:none}
.rule-list li{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border)}
.rule-list li:last-child{border:none}
.rule-name{font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rule-count{font-size:14px;font-weight:600;color:var(--cyan);min-width:40px;text-align:right}
.rule-bar-bg{width:80px;height:6px;background:var(--surface2);border-radius:3px;margin-left:12px}
.rule-bar-fill{height:100%;background:var(--cyan);border-radius:3px;transition:width .5s ease}
/* Domain list */
.domain-list{list-style:none}
.domain-list li{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border);font-size:13px}
.domain-list li:last-child{border:none}
.domain-name{font-family:monospace;color:var(--text)}
.domain-risk{font-size:11px;font-weight:600;padding:2px 8px;border-radius:4px}
.empty{color:var(--text2);font-size:13px;font-style:italic;padding:20px 0;text-align:center}
.footer{padding:24px 32px;text-align:center;color:var(--text2);font-size:12px;border-top:1px solid var(--border);margin-top:16px}
.footer a{color:var(--cyan)}
.loading{display:flex;align-items:center;justify-content:center;padding:40px;color:var(--text2)}
.spinner{width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--cyan);border-radius:50%;animation:spin .8s linear infinite;margin-right:10px}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="header">
<h1>🛡️ <span>SkillAudit</span> Threat Dashboard</h1>
<div class="meta">
<span><span class="live-dot"></span> Live</span>
<span id="lastUpdate">Loading...</span>
<a href="/">← Back to Scanner</a>
</div>
</div>
<div class="grid" id="dashboard">
<div class="loading"><div class="spinner"></div>Loading dashboard...</div>
</div>
<div class="footer">
Powered by <a href="https://skillaudit.vercel.app">SkillAudit</a> — Security infrastructure for the AI agent ecosystem.
<br>Data refreshes every 60 seconds. API: <a href="/feed">/feed</a> · <a href="/stats">/stats</a> · <a href="/feed/rules">/feed/rules</a>
</div>
<script>
const BASE = '';
let refreshTimer;
function fmt(n) { return n >= 1000 ? (n/1000).toFixed(1) + 'k' : String(n); }
function ago(ts) {
const d = Date.now() - new Date(ts).getTime();
if (d < 60000) return 'just now';
if (d < 3600000) return Math.floor(d/60000) + 'm ago';
if (d < 86400000) return Math.floor(d/3600000) + 'h ago';
return Math.floor(d/86400000) + 'd ago';
}
async function fetchJSON(path) {
const r = await fetch(BASE + path, { headers: { Accept: 'application/json' } });
return r.json();
}
function riskColor(level) {
const m = { clean:'var(--green)', low:'var(--yellow)', moderate:'var(--orange)', high:'var(--red)', critical:'var(--purple)' };
return m[level] || 'var(--text2)';
}
async function loadDashboard() {
try {
const [stats, feed, domains, rules] = await Promise.all([
fetchJSON('/stats'),
fetchJSON('/feed'),
fetchJSON('/feed/domains'),
fetchJSON('/feed/rules'),
]);
const rd = stats.riskDistribution || {};
const total = Object.values(rd).reduce((a,b) => a+b, 0) || 1;
const threats = Array.isArray(feed.recentThreats) ? feed.recentThreats : feed.recentThreats?.items || [];
const flagged = (domains.items || domains.domains || []).slice(0, 10);
const topRules = (Array.isArray(rules.allTime) ? rules.allTime : rules.allTime?.items || []).slice(0, 10);
const todayRules = (Array.isArray(rules.today) ? rules.today : rules.today?.items || []).slice(0, 5);
const maxHits = topRules.length > 0 ? (topRules[0].hits || topRules[0].hitCount || 1) : 1;
// Calculate threat rate
const recentCount = threats.length;
const cleanPct = ((rd.clean||0)/total*100).toFixed(0);
const html = `
<!-- KPI Cards -->
<div class="card">
<h3>Total Scans</h3>
<div class="big">${fmt(stats.totalScans || 0)}</div>
<div class="sub">Skills analyzed to date</div>
</div>
<div class="card">
<h3>Clean Rate</h3>
<div class="big" style="color:var(--green)">${cleanPct}%</div>
<div class="sub">${fmt(rd.clean||0)} of ${fmt(total)} scans passed clean</div>
</div>
<div class="card">
<h3>Threats Detected</h3>
<div class="big" style="color:var(--red)">${fmt(recentCount)}</div>
<div class="sub">Recent actionable findings</div>
</div>
<div class="card">
<h3>Flagged Domains</h3>
<div class="big" style="color:var(--orange)">${flagged.length}</div>
<div class="sub">Domains with moderate+ risk</div>
</div>
<!-- Risk Distribution -->
<div class="card wide">
<h3>Risk Distribution</h3>
<div class="risk-bar">
<div class="clean" style="width:${(rd.clean||0)/total*100}%"></div>
<div class="low" style="width:${(rd.low||0)/total*100}%"></div>
<div class="moderate" style="width:${(rd.moderate||0)/total*100}%"></div>
<div class="high" style="width:${(rd.high||0)/total*100}%"></div>
<div class="critical" style="width:${(rd.critical||0)/total*100}%"></div>
</div>
<div class="risk-legend">
<span><span class="dot" style="background:var(--green)"></span> Clean ${fmt(rd.clean||0)}</span>
<span><span class="dot" style="background:var(--yellow)"></span> Low ${fmt(rd.low||0)}</span>
<span><span class="dot" style="background:var(--orange)"></span> Moderate ${fmt(rd.moderate||0)}</span>
<span><span class="dot" style="background:var(--red)"></span> High ${fmt(rd.high||0)}</span>
<span><span class="dot" style="background:var(--purple)"></span> Critical ${fmt(rd.critical||0)}</span>
</div>
</div>
<!-- Recent Threats -->
<div class="card half">
<h3>Recent Threats</h3>
${threats.length === 0 ? '<div class="empty">No recent threats detected</div>' : `
<ul class="threat-list">
${threats.slice(0, 8).map(t => `
<li>
<span class="sev ${t.severity}">${t.severity}</span>
<div class="threat-info">
<div class="name">${esc(t.name || t.ruleId)}</div>
<div class="domain">${esc(t.domain || t.source || '')}</div>
<div class="time">${t.detectedAt ? ago(t.detectedAt) : ''}</div>
</div>
</li>
`).join('')}
</ul>`}
</div>
<!-- Top Detection Rules -->
<div class="card half">
<h3>Top Detection Rules (All Time)</h3>
${topRules.length === 0 ? '<div class="empty">No rule data yet</div>' : `
<ul class="rule-list">
${topRules.map(r => `
<li>
<span class="sev ${r.severity || 'medium'}">${(r.severity||'?')[0].toUpperCase()}</span>
<span class="rule-name" title="${esc(r.ruleId)}">${esc(r.ruleId)}</span>
<span class="rule-count">${fmt(r.hits||r.hitCount||0)}</span>
<div class="rule-bar-bg"><div class="rule-bar-fill" style="width:${(r.hits||r.hitCount||0)/maxHits*100}%"></div></div>
</li>
`).join('')}
</ul>`}
</div>
<!-- Flagged Domains -->
<div class="card half">
<h3>Flagged Domains</h3>
${flagged.length === 0 ? '<div class="empty">No flagged domains</div>' : `
<ul class="domain-list">
${flagged.map(d => `
<li>
<span class="domain-name">${esc(d.domain)}</span>
<span class="domain-risk" style="background:${riskColor(d.riskLevel)}22;color:${riskColor(d.riskLevel)}">${d.riskLevel}</span>
</li>
`).join('')}
</ul>`}
</div>
<!-- Trending Today -->
<div class="card half">
<h3>Trending Rules (Today)</h3>
${todayRules.length === 0 ? '<div class="empty">No detections today yet</div>' : `
<ul class="rule-list">
${todayRules.map(r => `
<li>
<span class="rule-name">${esc(r.ruleId)}</span>
<span class="rule-count">${fmt(r.hits||r.hitCount||0)}</span>
</li>
`).join('')}
</ul>`}
</div>
`;
document.getElementById('dashboard').innerHTML = html;
document.getElementById('lastUpdate').textContent = 'Updated ' + new Date().toLocaleTimeString();
} catch (e) {
document.getElementById('dashboard').innerHTML = '<div class="empty">Failed to load dashboard data. <a href="javascript:loadDashboard()">Retry</a></div>';
}
}
function esc(s) {
if (!s) return '';
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
loadDashboard();
refreshTimer = setInterval(loadDashboard, 60000);
</script>
</body>
</html>