<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>SkillAudit — Scan History & Risk Trends</title>
<meta name="description" content="Track how an AI agent skill's risk changes over time. Detect supply chain attacks through risk trend analysis.">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0f0f23;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,monospace;line-height:1.6}
a{color:#00ff88;text-decoration:none}a:hover{text-decoration:underline}
.container{max-width:900px;margin:0 auto;padding:1.5rem}
.header{text-align:center;padding:1.5rem 0 1rem}
.header h1{font-size:1.5rem;color:#888}.header h1 span{color:#00ff88}
.header p{color:#555;font-size:0.9rem;margin-top:0.3rem}
.form-card{background:#111133;border:1px solid #2a2a5a;border-radius:12px;padding:1.5rem;margin-bottom:1.5rem}
.form-row{display:flex;gap:0.8rem}
.form-row input{flex:1;background:#0a0a1e;border:1px solid #3a3a6a;border-radius:8px;padding:0.7rem 1rem;color:#e0e0e0;font-family:monospace;font-size:0.9rem;outline:none}
.form-row input:focus{border-color:#00ff88}
.btn{background:linear-gradient(135deg,#00cc66,#00ff88);color:#000;border:none;border-radius:8px;padding:0.7rem 1.5rem;font-size:0.95rem;font-weight:700;font-family:monospace;cursor:pointer}
.btn:disabled{opacity:0.5;cursor:not-allowed}
#result{display:none}
/* Overview cards */
.overview{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:0.8rem;margin-bottom:1.5rem}
.ov-card{background:#111133;border:1px solid #2a2a5a;border-radius:10px;padding:1rem;text-align:center}
.ov-card .val{font-size:1.6rem;font-weight:900}
.ov-card .lbl{font-size:0.7rem;color:#888;text-transform:uppercase;letter-spacing:0.05em;margin-top:0.2rem}
/* Chart */
.chart-wrap{background:#111133;border:1px solid #2a2a5a;border-radius:12px;padding:1.2rem;margin-bottom:1.5rem}
.chart-wrap h2{font-size:0.95rem;color:#888;margin-bottom:0.8rem}
canvas{width:100%!important;height:200px!important}
/* Timeline */
.timeline{margin-bottom:1.5rem}
.timeline h2{font-size:1rem;color:#00ff88;border-bottom:1px solid #2a2a5a;padding-bottom:0.5rem;margin-bottom:0.8rem}
.tl-item{display:flex;align-items:center;gap:0.8rem;padding:0.6rem 0.8rem;border-radius:8px;margin-bottom:0.3rem;background:#111133;cursor:pointer;transition:background 0.15s}
.tl-item:hover{background:#1a1a4e}
.tl-dot{width:12px;height:12px;border-radius:50%;flex-shrink:0}
.tl-date{font-size:0.8rem;color:#888;width:140px;flex-shrink:0}
.tl-risk{font-weight:700;font-size:0.85rem;width:80px;text-transform:uppercase;flex-shrink:0}
.tl-score{font-size:0.85rem;color:#aaa;width:70px;flex-shrink:0}
.tl-findings{font-size:0.8rem;color:#666;flex:1;text-align:right}
.tl-delta{font-size:0.75rem;font-weight:700;width:60px;text-align:right;flex-shrink:0}
.trend-badge{display:inline-block;padding:0.2rem 0.8rem;border-radius:6px;font-weight:700;font-size:0.85rem}
.trend-worsening{background:#3d0a0a;color:#ff4444}
.trend-improving{background:#0a3d1a;color:#00ff88}
.trend-stable{background:#1a1a3e;color:#888}
.empty{text-align:center;padding:2rem;color:#555}
.error{background:#3d0a0a;border:1px solid #ff4444;color:#ff4444;padding:1rem;border-radius:8px;text-align:center}
.footer{text-align:center;padding:2rem 0;color:#555;font-size:0.8rem;border-top:1px solid #1a1a3e;margin-top:2rem}
</style>
</head><body>
<div class="container">
<div class="header">
<h1>🛡️ Skill<span>Audit</span> — History</h1>
<p>Track how a skill's risk changes over time. Spot supply chain attacks early.</p>
</div>
<div class="form-card">
<form id="history-form" onsubmit="loadHistory(event)">
<div class="form-row">
<input type="url" id="url-input" placeholder="https://example.com/SKILL.md" required>
<button type="submit" class="btn" id="load-btn">View History</button>
</div>
</form>
</div>
<div id="error" style="display:none" class="error"></div>
<div id="result"></div>
<div class="footer">
<a href="/">← Back to SkillAudit</a> · <a href="/compare">Compare Versions</a> · <a href="/docs">API Docs</a><br>
Built by <a href="https://moltbook.com/u/Megamind_0x">Megamind_0x</a> 🧠
</div>
</div>
<script>
const riskColors = {clean:'#00ff88',low:'#88ff00',moderate:'#ffaa00',high:'#ff4444',critical:'#ff0044'};
const riskBgs = {clean:'#0a3d1a',low:'#2a3d0a',moderate:'#3d2a0a',high:'#3d1a0a',critical:'#3d0a0a'};
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function fmtDate(iso){
const d=new Date(iso);
return d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})+' '+d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'});
}
async function loadHistory(e){
e.preventDefault();
const url=document.getElementById('url-input').value.trim();
const btn=document.getElementById('load-btn');
const errEl=document.getElementById('error');
const resEl=document.getElementById('result');
btn.disabled=true;btn.textContent='Loading…';
errEl.style.display='none';resEl.style.display='none';
try{
const res=await fetch('/scan/history/url?url='+encodeURIComponent(url)+'&limit=50');
const d=await res.json();
if(!res.ok)throw new Error(d.error||'Failed');
if(d.scans===0){
resEl.innerHTML='<div class="empty"><p>No scan history found for this URL.</p><p style="margin-top:0.5rem"><a href="/?url='+encodeURIComponent(url)+'">Scan it now →</a></p></div>';
resEl.style.display='block';return;
}
renderHistory(d);
}catch(err){
errEl.textContent=err.message;errEl.style.display='block';
}finally{
btn.disabled=false;btn.textContent='View History';
}
}
function renderHistory(d){
const el=document.getElementById('result');
const trendClass='trend-'+(d.trend||'stable');
const curColor=riskColors[d.currentRisk]||'#888';
const peakColor=riskColors[d.peakRisk]||'#888';
let html=`
<div class="overview">
<div class="ov-card"><div class="val" style="color:${curColor}">${esc((d.currentRisk||'?').toUpperCase())}</div><div class="lbl">Current Risk</div></div>
<div class="ov-card"><div class="val" style="color:${curColor}">${d.currentScore}</div><div class="lbl">Current Score</div></div>
<div class="ov-card"><div class="val">${d.scans}</div><div class="lbl">Total Scans</div></div>
<div class="ov-card"><div class="val" style="color:${peakColor}">${esc((d.peakRisk||'?').toUpperCase())}</div><div class="lbl">Peak Risk</div></div>
<div class="ov-card">
<div class="val"><span class="trend-badge ${trendClass}">${esc((d.trend||'stable').toUpperCase())}</span></div>
<div class="lbl">Trend</div>
</div>
</div>
`;
// Chart
const hist=d.history||[];
if(hist.length>=2){
html+=`<div class="chart-wrap"><h2>Risk Score Over Time</h2><canvas id="chart"></canvas></div>`;
}
// Timeline
html+=`<div class="timeline"><h2>Scan Timeline (${hist.length} scans)</h2>`;
for(let i=0;i<hist.length;i++){
const h=hist[i];
const color=riskColors[h.riskLevel]||'#888';
const prev=hist[i+1];
let deltaHtml='';
if(prev){
const sd=h.riskScore-prev.riskScore;
if(sd>0)deltaHtml=`<span style="color:#ff4444">+${sd}</span>`;
else if(sd<0)deltaHtml=`<span style="color:#00ff88">${sd}</span>`;
else deltaHtml=`<span style="color:#555">—</span>`;
}
const reportUrl=h.scanId?'/report/'+h.scanId:'#';
html+=`<a href="${reportUrl}" class="tl-item" style="text-decoration:none;color:inherit">
<div class="tl-dot" style="background:${color}"></div>
<div class="tl-date">${fmtDate(h.scannedAt)}</div>
<div class="tl-risk" style="color:${color}">${esc(h.riskLevel)}</div>
<div class="tl-score">Score: ${h.riskScore}</div>
<div class="tl-findings">${h.findingsCount||0} findings</div>
<div class="tl-delta">${deltaHtml}</div>
</a>`;
}
html+=`</div>`;
el.innerHTML=html;
el.style.display='block';
// Draw chart
if(hist.length>=2){
requestAnimationFrame(()=>drawChart(hist));
}
}
function drawChart(hist){
const canvas=document.getElementById('chart');
if(!canvas)return;
const ctx=canvas.getContext('2d');
const dpr=window.devicePixelRatio||1;
const rect=canvas.parentElement.getBoundingClientRect();
const w=rect.width-2*parseFloat(getComputedStyle(canvas.parentElement).paddingLeft);
const h=200;
canvas.width=w*dpr;canvas.height=h*dpr;
canvas.style.width=w+'px';canvas.style.height=h+'px';
ctx.scale(dpr,dpr);
// Reverse so oldest is left
const data=[...hist].reverse();
const maxScore=Math.max(...data.map(d=>d.riskScore),10);
const pad={top:20,right:20,bottom:30,left:45};
const cw=w-pad.left-pad.right;
const ch=h-pad.top-pad.bottom;
// Grid
ctx.strokeStyle='#1a1a3e';ctx.lineWidth=1;
for(let i=0;i<=4;i++){
const y=pad.top+ch*(1-i/4);
ctx.beginPath();ctx.moveTo(pad.left,y);ctx.lineTo(w-pad.right,y);ctx.stroke();
ctx.fillStyle='#555';ctx.font='10px monospace';ctx.textAlign='right';
ctx.fillText(Math.round(maxScore*i/4),pad.left-6,y+3);
}
// X-axis labels (first, middle, last dates)
ctx.fillStyle='#555';ctx.font='10px monospace';ctx.textAlign='center';
const xLabels=[0,Math.floor(data.length/2),data.length-1];
for(const idx of xLabels){
if(idx>=data.length)continue;
const x=pad.left+(idx/(data.length-1))*cw;
const d=new Date(data[idx].scannedAt);
ctx.fillText(d.toLocaleDateString('en-US',{month:'short',day:'numeric'}),x,h-5);
}
if(data.length<2)return;
// Area fill
ctx.beginPath();
for(let i=0;i<data.length;i++){
const x=pad.left+(i/(data.length-1))*cw;
const y=pad.top+ch*(1-data[i].riskScore/maxScore);
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
}
ctx.lineTo(pad.left+cw,pad.top+ch);
ctx.lineTo(pad.left,pad.top+ch);
ctx.closePath();
const grad=ctx.createLinearGradient(0,pad.top,0,pad.top+ch);
grad.addColorStop(0,'rgba(0,255,136,0.15)');grad.addColorStop(1,'rgba(0,255,136,0)');
ctx.fillStyle=grad;ctx.fill();
// Line
ctx.beginPath();
for(let i=0;i<data.length;i++){
const x=pad.left+(i/(data.length-1))*cw;
const y=pad.top+ch*(1-data[i].riskScore/maxScore);
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
}
ctx.strokeStyle='#00ff88';ctx.lineWidth=2;ctx.stroke();
// Dots with risk colors
for(let i=0;i<data.length;i++){
const x=pad.left+(i/(data.length-1))*cw;
const y=pad.top+ch*(1-data[i].riskScore/maxScore);
ctx.beginPath();ctx.arc(x,y,3,0,Math.PI*2);
ctx.fillStyle=riskColors[data[i].riskLevel]||'#888';ctx.fill();
}
}
// URL param support
(function(){
const p=new URLSearchParams(location.search);
if(p.get('url')){
document.getElementById('url-input').value=p.get('url');
loadHistory(new Event('submit'));
}
})();
</script>
</body></html>