Threat_Hunter / ui /static /checkpoint.js
EricChen2005's picture
Deploy ThreatHunter - AMD MI300X + Qwen2.5-32B
c8d30bc
/**
* checkpoint.js โ€” Checkpoint Dashboard ๅ‰็ซฏ้‚่ผฏ
* =============================================
* ๅŠŸ่ƒฝ๏ผš
* 1. ่ผ‰ๅ…ฅๆŽƒๆๆธ…ๅ–ฎ๏ผˆ/api/checkpoints๏ผ‰โ€” ้กฏ็คบๆ่ฟฐๆ€งๆจ™็ฑค
* 2. ่ผ‰ๅ…ฅ้ธๅฎšๆŽƒๆ็š„ JSONL ไบ‹ไปถ๏ผˆ/api/checkpoints/{filename}๏ผ‰
* 3. ไบ‹ไปถ้Žๆฟพ๏ผˆไพ event type / agent / ๆœๅฐ‹ๆ–‡ๅญ—๏ผ‰
* 4. ็ตฑ่จˆๆ‘˜่ฆๅก็‰‡่จˆ็ฎ—
* 5. ไบ‹ไปถๆ™‚้–“่ปธๆธฒๆŸ“ + ่กจๆ ผๅผ่ฉณ็ดฐ้ขๆฟ
*
* ้ตๅฎˆ๏ผšAGENTS.md โ€” ๆ‰€ๆœ‰ .md ๅ ฑๅ‘Šไฝฟ็”จ็น้ซ”ไธญๆ–‡๏ผ›็จ‹ๅผ็ขผ่จป่งฃไฝฟ็”จ็น้ซ”ไธญๆ–‡
*/
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๅ…จๅŸŸ็‹€ๆ…‹
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
let allEvents = []; // ็•ถๅ‰ๆŽƒๆ็š„ๅ…จ้ƒจไบ‹ไปถ
let filteredEvents = []; // ้ŽๆฟพๅพŒ็š„ไบ‹ไปถ
// DOM ๅ…ƒ็ด ๅฟซๅ–
const $ = (id) => document.getElementById(id);
const scanSelector = $('scanSelector');
const refreshBtn = $('refreshBtn');
const eventFilter = $('eventFilter');
const agentFilter = $('agentFilter');
const searchInput = $('searchInput');
const timeline = $('timeline');
const filteredCount = $('filteredCount');
const detailPanel = $('detailPanel');
const detailTitle = $('detailTitle');
const detailContent = $('detailContent');
const closeDetail = $('closeDetail');
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// API ๅ‘ผๅซ
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
/**
* ๅ–ๅพ—ๆŽƒๆๆช”ๆกˆๆธ…ๅ–ฎ
*/
async function fetchScanList() {
try {
const res = await fetch('/api/checkpoints');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data.files || [];
} catch (e) {
console.error('[CHECKPOINT] fetchScanList failed:', e);
return [];
}
}
/**
* ๅ–ๅพ—ๆŒ‡ๅฎšๆŽƒๆ็š„ไบ‹ไปถๆธ…ๅ–ฎ
* @param {string} filename โ€” JSONL ๆช”ๅ
*/
async function fetchScanEvents(filename) {
try {
const res = await fetch(`/api/checkpoints/${encodeURIComponent(filename)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data.events || [];
} catch (e) {
console.error('[CHECKPOINT] fetchScanEvents failed:', e);
return [];
}
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๆŽƒๆ้ธๆ“‡ๅ™จ๏ผˆๆ”นๅ–„๏ผšไฝฟ็”จๆ่ฟฐๆ€งๆจ™็ฑค๏ผ‰
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async function loadScanList() {
const files = await fetchScanList();
scanSelector.innerHTML = '';
if (files.length === 0) {
scanSelector.innerHTML = '<option value="">No scans found</option>';
return;
}
// ๆŒ‰ๆ›ดๆ–ฐๆ™‚้–“ๅ€’ๅบ๏ผˆๆœ€ๆ–ฐๅœจๅ‰๏ผ‰
files.sort((a, b) => (b.modified || '').localeCompare(a.modified || ''));
files.forEach((f, i) => {
const opt = document.createElement('option');
opt.value = f.name;
// ไฝฟ็”จๆ่ฟฐๆ€งๆจ™็ฑค๏ผˆAPI ๅ›žๅ‚ณ็š„ label๏ผ‰
const timeStr = f.modified ? f.modified.substring(5, 16).replace('T', ' ') : '';
const label = f.label || f.name;
opt.textContent = `${timeStr} โ€” ${label}`;
scanSelector.appendChild(opt);
});
// ่‡ชๅ‹•่ผ‰ๅ…ฅๆœ€ๆ–ฐ็š„ๆŽƒๆ
if (files.length > 0) {
scanSelector.value = files[0].name;
await loadScanEvents(files[0].name);
}
}
async function loadScanEvents(filename) {
// ๆธ…็ฉบ็‹€ๆ…‹
allEvents = [];
filteredEvents = [];
renderTimeline([]);
updateStats([]);
if (!filename) return;
// ่ผ‰ๅ…ฅไบ‹ไปถ
allEvents = await fetchScanEvents(filename);
// ๅปบ็ซ‹ Agent ้Žๆฟพ้ธ้ …
buildAgentFilter();
// ๅŸท่กŒ้Žๆฟพ
applyFilters();
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ้Žๆฟพ้‚่ผฏ
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ็œŸๆญฃ็š„ LLM Agent ็™ฝๅๅ–ฎ๏ผˆIssue #3๏ผ‰
// pipeline / orchestrator / input_sanitizer ๆ˜ฏๅŸบ็คŽ่จญๆ–ฝ่€Œ้ž Agent
// ไธๆ‡‰ๅ‡บ็พๅœจ Agent ้Žๆฟพๅ™จไธญ
const REAL_AGENTS = new Set([
'scout', 'analyst', 'critic', 'advisor',
'security_guard', 'intel_fusion'
]);
function buildAgentFilter() {
// ๅชๅˆ—ๅ‡บ็œŸๆญฃ็š„ LLM Agent๏ผˆ็™ฝๅๅ–ฎ้Žๆฟพ๏ผ‰
const agents = [...new Set(allEvents.map(e => e.agent).filter(Boolean))]
.filter(a => REAL_AGENTS.has(a))
.sort();
agentFilter.innerHTML = '<option value="">All Agents</option>';
agents.forEach(a => {
const opt = document.createElement('option');
opt.value = a;
// ้กฏ็คบๅ‹ๅ–„ๅ็จฑ
const displayNames = {
scout: 'Scout (CVE ๅตๅฏŸ)', analyst: 'Analyst (้ขจ้šชๅˆ†ๆž)',
critic: 'Critic (่พจ่ซ–)', advisor: 'Advisor (่ฃ่บซๅ ฑๅ‘Š)',
security_guard: 'Security Guard', intel_fusion: 'Intel Fusion'
};
opt.textContent = displayNames[a] || a;
agentFilter.appendChild(opt);
});
}
function applyFilters() {
const eventType = eventFilter.value;
const agent = agentFilter.value;
const search = (searchInput.value || '').toLowerCase().trim();
filteredEvents = allEvents.filter(e => {
if (eventType && e.event !== eventType) return false;
if (agent && e.agent !== agent) return false;
if (search) {
const haystack = JSON.stringify(e).toLowerCase();
if (!haystack.includes(search)) return false;
}
return true;
});
renderTimeline(filteredEvents);
updateStats(filteredEvents);
filteredCount.textContent = `${filteredEvents.length} / ${allEvents.length} events`;
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ็ตฑ่จˆ่จˆ็ฎ—
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
function updateStats(events) {
// ็ตฑ่จˆไฝฟ็”จๅ…จ้ƒจไบ‹ไปถ๏ผˆ้ž้ŽๆฟพๅพŒ็š„๏ผ‰๏ผŒๆ‰่ƒฝๆบ–็ขบๅๆ˜ ๆ•ดๆฌกๆŽƒๆ
const all = allEvents;
// ็ธฝไบ‹ไปถๆ•ธ๏ผˆ้กฏ็คบ้ŽๆฟพๅพŒ / ๅ…จ้ƒจ๏ผ‰
$('statTotal').querySelector('.cp-stat-value').textContent =
events.length === all.length ? (all.length || 'โ€”') : `${events.length}`;
// LLM ๅ‘ผๅซๆ•ธ๏ผˆๅ…จๅฑ€๏ผ‰
const llmCalls = all.filter(e => e.event === 'LLM_CALL').length;
$('statLLM').querySelector('.cp-stat-value').textContent = llmCalls || '0';
// ้Œฏ่ชค / ้‡่ฉฆ๏ผˆๅ…จๅฑ€๏ผ‰
const errors = all.filter(e => e.event === 'LLM_ERROR').length;
const retries = all.filter(e => e.event === 'LLM_RETRY').length;
$('statErrors').querySelector('.cp-stat-value').textContent = `${errors} / ${retries}`;
// ๆŒ็บŒๆ™‚้–“
const scanEnd = all.find(e => e.event === 'SCAN_END');
if (scanEnd && scanEnd.data && scanEnd.data.total_duration_seconds != null) {
const dur = scanEnd.data.total_duration_seconds;
$('statDuration').querySelector('.cp-stat-value').textContent =
dur >= 60 ? `${(dur / 60).toFixed(1)}m` : `${dur.toFixed(1)}s`;
} else {
$('statDuration').querySelector('.cp-stat-value').textContent = 'โ€”';
}
// Agent ๆ•ธ
const uniqueAgents = new Set(all.map(e => e.agent).filter(a => a && a !== 'pipeline'));
$('statAgents').querySelector('.cp-stat-value').textContent = uniqueAgents.size || 'โ€”';
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๆ™‚้–“่ปธๆธฒๆŸ“
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
function renderTimeline(events) {
if (events.length === 0) {
timeline.innerHTML = `
<div class="cp-empty-state">
<div class="cp-empty-icon">๐Ÿ“ก</div>
<div class="cp-empty-text">No events to display</div>
</div>`;
return;
}
const html = events.map(e => {
const ts = formatTimestamp(e.ts);
const dataPreview = buildDataPreview(e);
return `<div class="cp-event" data-seq="${e.seq}" onclick="showDetail(${e.seq})">
<span class="cp-event-seq">#${e.seq}</span>
<span class="cp-event-ts">${ts}</span>
<span class="cp-event-type" data-type="${esc(e.event)}">${esc(e.event)}</span>
<span class="cp-event-agent">${esc(e.agent)}</span>
<span class="cp-event-data">${esc(dataPreview)}</span>
</div>`;
}).join('');
timeline.innerHTML = html;
}
/**
* ๆ ผๅผๅŒ– ISO ๆ™‚้–“ๆˆณ็‚บ HH:MM:SS.mmm
*/
function formatTimestamp(iso) {
if (!iso) return 'โ€”';
try {
const d = new Date(iso);
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
const ms = String(d.getMilliseconds()).padStart(3, '0');
return `${h}:${m}:${s}.${ms}`;
} catch { return iso.substring(11, 23); }
}
/**
* ๅพžไบ‹ไปถ่ณ‡ๆ–™ๅปบๆง‹้ ่ฆฝๆ–‡ๅญ—
*/
function buildDataPreview(event) {
const d = event.data || {};
switch (event.event) {
case 'SCAN_START': return `scan_id=${d.scan_id || ''}`;
case 'SCAN_END': return `status=${d.final_status || ''} | ${d.total_duration_seconds || 0}s | ${d.total_checkpoints || 0} events`;
case 'STAGE_ENTER': return d.tech_stack_preview ? d.tech_stack_preview.split('\\n')[0].substring(0, 80) : (d.input_preview || `keys=${(d.input_keys || []).join(',')}`);
case 'STAGE_EXIT': return `${d.status || 'OK'} | ${d.duration_ms || 0}ms${d.vuln_count != null ? ' | vulns=' + d.vuln_count : ''}${d.risk_score != null ? ' | risk=' + d.risk_score : ''}`;
case 'LLM_CALL': return `model=${shortModel(d.model)} | ${d.task_preview || ''}`;
case 'LLM_RESULT': return `${d.status || 'OK'} | ${d.duration_ms || 0}ms | ${d.output_length || 0} chars`;
case 'LLM_RETRY': return `${shortModel(d.failed_model)} โ†’ retry #${d.retry_count || 0}`;
case 'LLM_ERROR': return `${shortModel(d.model)} | ${(d.error || '').substring(0, 60)}`;
case 'TOOL_CALL': return `${d.tool_name || '?'} | ${d.status || ''} | input=${(d.input || '').substring(0, 50)}`;
case 'HARNESS_CHECK': return `${d.layer || ''} ${d.check_name || ''} โ†’ ${d.result || ''}`;
case 'DEGRADATION': return d.reason || '';
default: return JSON.stringify(d).substring(0, 100);
}
}
/** ๅฐ‡้•ทๆจกๅž‹ๅ็ธฎ็Ÿญ */
function shortModel(m) {
if (!m) return '?';
// meta-llama/llama-3.3-70b-instruct:free โ†’ llama-3.3-70b:free
return m.replace(/^[^\/]+\//, '').replace('-instruct', '');
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ่ฉณ็ดฐ้ขๆฟ๏ผˆๆ”นๅ–„๏ผš่กจๆ ผๅผ๏ผŒ้ž JSON๏ผ‰
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
function showDetail(seq) {
const event = allEvents.find(e => e.seq === seq);
if (!event) return;
detailTitle.textContent = `#${event.seq} โ€” ${event.event}`;
detailContent.innerHTML = buildDetailTable(event);
detailPanel.classList.remove('cp-hidden');
}
function hideDetail() {
detailPanel.classList.add('cp-hidden');
}
/**
* ๅฐ‡ไบ‹ไปถ่ฝ‰็‚บ็ตๆง‹ๅŒ–่กจๆ ผ๏ผˆๅ–ไปฃ JSON ้กฏ็คบ๏ผ‰
*/
function buildDetailTable(event) {
let html = '';
// โ”€โ”€ ๅŸบๆœฌ่ณ‡่จŠๅ€ โ”€โ”€
html += `<div class="cp-detail-section">
<div class="cp-detail-section-title">ๅŸบๆœฌ่ณ‡่จŠ</div>
<table class="cp-detail-table">
<tr><td class="cp-dt-key">ๅบ่™Ÿ</td><td class="cp-dt-val">#${event.seq}</td></tr>
<tr><td class="cp-dt-key">ไบ‹ไปถ้กžๅž‹</td><td class="cp-dt-val"><span class="cp-event-type" data-type="${esc(event.event)}">${esc(event.event)}</span></td></tr>
<tr><td class="cp-dt-key">Agent</td><td class="cp-dt-val">${esc(event.agent)}</td></tr>
<tr><td class="cp-dt-key">ๆ™‚้–“</td><td class="cp-dt-val">${esc(event.ts)}</td></tr>
<tr><td class="cp-dt-key">Scan ID</td><td class="cp-dt-val cp-mono">${esc(event.scan_id)}</td></tr>
</table>
</div>`;
// โ”€โ”€ ๆ นๆ“šไบ‹ไปถ้กžๅž‹๏ผŒ็”จๅฐๆ‡‰็š„่กจๆ ผๅ‘ˆ็พ โ”€โ”€
const d = event.data || {};
switch (event.event) {
case 'SCAN_START':
html += renderSection('ๆŽƒๆๅ•Ÿๅ‹•', [
['Scan ID', d.scan_id],
]);
break;
case 'SCAN_END':
html += renderSection('ๆŽƒๆ็ตๆŸ', [
['ๆœ€็ต‚็‹€ๆ…‹', d.final_status, d.final_status === 'COMPLETE' ? 'green' : 'red'],
['็ธฝๆŒ็บŒๆ™‚้–“', d.total_duration_seconds != null ? `${d.total_duration_seconds}s (${(d.total_duration_seconds/60).toFixed(1)} min)` : 'โ€”'],
['็ธฝ Checkpoint ๆ•ธ', d.total_checkpoints],
]);
if (d.event_summary) {
html += renderSection('ไบ‹ไปถ็ตฑ่จˆ', Object.entries(d.event_summary).map(([k, v]) => [k, v]));
}
break;
case 'STAGE_ENTER':
html += renderSection('Stage ้€ฒๅ…ฅ', [
['่ผธๅ…ฅ Keys', d.input_keys],
['Input Hash', d.input_hash],
]);
if (d.tech_stack_preview) {
html += renderSection('ๆŽƒๆ็›ฎๆจ™้ ่ฆฝ', [], d.tech_stack_preview);
}
if (d.packages && d.packages.length > 0) {
// v3.4: ้กฏ็คบ PackageExtractor ่ƒๅ–็š„ๅฅ—ไปถ
html += renderSection('ๅทฒ่ƒๅ–็š„็ฌฌไธ‰ๆ–นๅฅ—ไปถ๏ผˆๅ”ๅŠฉ Intel Fusion ๅ’Œ Scout ๆŸฅ่ฉข๏ผ‰', [
['ๅฅ—ไปถๆ•ธ้‡', d.packages.length],
['ๅฅ—ไปถๆธ…ๅ–ฎ', d.packages.join(', ')],
]);
}
if (d.vuln_count != null) {
html += renderSection('ๆผๆดž่ณ‡่จŠ', [['ๆผๆดžๆ•ธ้‡', d.vuln_count]]);
}
break;
case 'STAGE_EXIT':
html += renderSection('Stage ๅฎŒๆˆ', [
['็‹€ๆ…‹', d.status, d.status === 'SUCCESS' ? 'green' : (d.status === 'DEGRADED' ? 'orange' : 'red')],
['่€—ๆ™‚', d.duration_ms != null ? `${d.duration_ms}ms` : 'โ€”'],
['่ผธๅ‡บ Keys', d.output_keys],
]);
if (d.vuln_count != null) html += renderKV('ๆผๆดžๆ•ธ้‡', d.vuln_count);
if (d.risk_score != null) html += renderKV('้ขจ้šชๅˆ†ๆ•ธ', d.risk_score);
if (d.scan_path) html += renderKV('ๆŽƒๆ่ทฏๅพ‘', d.scan_path);
if (d.degraded != null) html += renderKV('ๆ˜ฏๅฆ้™็ดš', d.degraded ? 'โš ๏ธ ๆ˜ฏ' : 'โœ… ๅฆ');
if (d.verdict) html += renderKV('่ฃๆฑบ', d.verdict);
// v3.4: Intel Fusion ๅฅ—ไปถ่ซ‹ๆฑ‚่ณ‡่จŠ
if (d.packages_used && d.packages_used.length > 0) {
html += renderSection('ๅทฒๆไบคๆŽƒๆ็š„ๅฅ—ไปถ๏ผˆScout๏ผ‰', [
['ๅฅ—ไปถๆ•ธ้‡', d.packages_used.length],
['ๅฅ—ไปถๅ็จฑ', d.packages_used.join(', ')],
]);
}
if (d.cves_scored != null) html += renderKV('CVEs Scored', d.cves_scored);
break;
case 'LLM_CALL':
html += renderSection('LLM ๅ‘ผๅซ', [
['ๆจกๅž‹', d.model],
['Provider', d.provider],
['ไปปๅ‹™ๆ่ฟฐ', d.task_preview],
]);
break;
case 'LLM_RESULT':
html += renderSection('LLM ๅ›žๆ‡‰', [
['ๆจกๅž‹', d.model],
['็‹€ๆ…‹', d.status, d.status === 'SUCCESS' ? 'green' : 'red'],
['ๅ›žๆ‡‰้•ทๅบฆ', d.output_length != null ? `${d.output_length} ๅญ—ๅ…ƒ` : 'โ€”'],
['่€—ๆ™‚', d.duration_ms != null ? `${d.duration_ms}ms (${(d.duration_ms/1000).toFixed(1)}s)` : 'โ€”'],
]);
if (d.thinking) {
html += renderSection('LLM ๆ€่€ƒ้Ž็จ‹๏ผˆๅ‰ 1000 ๅญ—ๅ…ƒ๏ผ‰', [], d.thinking);
}
break;
case 'LLM_RETRY':
html += renderSection('LLM ้‡่ฉฆ', [
['ๅคฑๆ•—ๆจกๅž‹', d.failed_model],
['้‡่ฉฆๆฌกๆ•ธ', d.retry_count],
['ไธ‹ไธ€ๅ€‹ๆจกๅž‹', d.next_model],
['้Œฏ่ชคๅŽŸๅ› ', d.error],
]);
break;
case 'LLM_ERROR':
html += renderSection('LLM ้Œฏ่ชค', [
['ๆจกๅž‹', d.model],
['้Œฏ่ชค่จŠๆฏ', d.error],
]);
break;
case 'TOOL_CALL':
html += renderSection('ๅทฅๅ…ทๅ‘ผๅซ', [
['ๅทฅๅ…ทๅ็จฑ', d.tool_name],
['็‹€ๆ…‹', d.status, d.status === 'SUCCESS' ? 'green' : 'red'],
['่ผธๅ…ฅ', d.input],
['่ผธๅ‡บ', d.output],
]);
break;
case 'HARNESS_CHECK':
html += renderSection('Harness ๆชขๆŸฅ', [
['ไฟ้šœๅฑค', d.layer],
['ๆชขๆŸฅๅ็จฑ', d.check_name],
['็ตๆžœ', d.result, d.result === 'PASS' ? 'green' : 'orange'],
]);
if (d.details) {
html += renderSection('่ฉณ็ดฐ่ณ‡ๆ–™', Object.entries(d.details).map(([k, v]) => [k, v]));
}
break;
case 'DEGRADATION':
html += renderSection('้™็ดšไบ‹ไปถ', [
['ๅŽŸๅ› ', d.reason],
['ๅฑค็ดš', d.level],
]);
break;
default:
// ๆœช็Ÿฅไบ‹ไปถ้กžๅž‹๏ผš็›ดๆŽฅๅˆ—ๅ‡บ data ็š„ key-value
if (Object.keys(d).length > 0) {
html += renderSection('ไบ‹ไปถ่ณ‡ๆ–™', Object.entries(d).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : v]));
}
}
return html;
}
/**
* ๆธฒๆŸ“ไธ€ๅ€‹่กจๆ ผ section
* @param {string} title โ€” section ๆจ™้กŒ
* @param {Array} rows โ€” [[key, value, color?], ...]
* @param {string} codeBlock โ€” ๅฆ‚ๆœ‰๏ผŒๆธฒๆŸ“็‚บ็จ‹ๅผ็ขผๅ€ๅกŠ
*/
function renderSection(title, rows, codeBlock) {
let html = `<div class="cp-detail-section">
<div class="cp-detail-section-title">${esc(title)}</div>`;
if (rows.length > 0) {
html += '<table class="cp-detail-table">';
for (const [key, val, color] of rows) {
const displayVal = val == null ? 'โ€”' : String(val);
const colorClass = color ? ` cp-dt-${color}` : '';
html += `<tr>
<td class="cp-dt-key">${esc(key)}</td>
<td class="cp-dt-val${colorClass}">${esc(displayVal)}</td>
</tr>`;
}
html += '</table>';
}
if (codeBlock) {
html += `<pre class="cp-detail-code">${esc(String(codeBlock))}</pre>`;
}
html += '</div>';
return html;
}
/** ๆธฒๆŸ“ๅ–ฎไธ€ key-value๏ผˆ่ฟฝๅŠ ๅˆฐๅ‰ไธ€ๅ€‹ section ไธ‹ๆ–น๏ผ‰ */
function renderKV(key, val) {
return `<div class="cp-detail-section">
<table class="cp-detail-table">
<tr><td class="cp-dt-key">${esc(key)}</td><td class="cp-dt-val">${esc(String(val))}</td></tr>
</table>
</div>`;
}
/**
* HTML ่ทณ่„ซ๏ผˆ้˜ฒๆญข XSS๏ผ‰
*/
function esc(str) {
if (str == null) return '';
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ไบ‹ไปถ็นซ็ต
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
scanSelector.addEventListener('change', () => loadScanEvents(scanSelector.value));
refreshBtn.addEventListener('click', () => loadScanList());
eventFilter.addEventListener('change', applyFilters);
agentFilter.addEventListener('change', applyFilters);
searchInput.addEventListener('input', applyFilters);
closeDetail.addEventListener('click', hideDetail);
// ESC ้—œ้–‰่ฉณ็ดฐ้ขๆฟ
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') hideDetail();
});
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๅˆๅง‹ๅŒ–
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
/** ้กฏ็คบ Toast ้€š็Ÿฅ */
function showToast(msg, type = 'info') {
const id = 'cp-toast-' + Date.now();
const colors = {
info: 'rgba(0,255,255,0.15)',
success: 'rgba(0,230,118,0.15)',
warning: 'rgba(255,214,0,0.15)',
};
const border = {
info: 'rgba(0,255,255,0.3)',
success: 'rgba(0,230,118,0.3)',
warning: 'rgba(255,214,0,0.3)',
};
const toast = document.createElement('div');
toast.id = id;
toast.style.cssText = [
'position:fixed', 'bottom:24px', 'right:24px', 'z-index:9999',
'padding:10px 18px', 'border-radius:6px',
`background:${colors[type] || colors.info}`,
`border:1px solid ${border[type] || border.info}`,
'color:var(--text-main)', 'font-family:var(--mono)', 'font-size:0.82rem',
'backdrop-filter:blur(8px)', 'box-shadow:0 4px 20px rgba(0,0,0,0.3)',
'animation:slideIn 0.2s ease-out',
'max-width:360px',
].join(';');
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}
/**
* ่‡ชๅ‹•่ผช่ฉข๏ผšๆฏ 15 ็ง’ๆชขๆŸฅๆ˜ฏๅฆๆœ‰ๆ–ฐๆƒๆๆช”ๆกˆ๏ผˆIssue #2๏ผ‰
* ่งฃๆฑบใ€Œๅ…ˆ้–‹ Checkpoint ้  โ†’ ๅ†่ท‘ๆƒๆใ€ๅทฅไฝœๆตไธ‹๏ผŒๆ–ฐๆƒๆไธๅ‡บ็พ็š„ๅ•้กŒ
*/
let _lastKnownFilenames = new Set();
let _autoRefreshInterval = null;
async function startAutoRefresh() {
if (_autoRefreshInterval) return; // ้˜ฒๆญข้‡่ค‡ๅ•Ÿๅ‹•
_autoRefreshInterval = setInterval(async () => {
try {
const files = await fetchScanList();
const currentNames = new Set(files.map(f => f.name));
// ๆชขๆŸฅๆ˜ฏๅฆๆœ‰ๆ–ฐๆช”ๆกˆ
const newFiles = files.filter(f => !_lastKnownFilenames.has(f.name));
if (newFiles.length > 0 && _lastKnownFilenames.size > 0) {
// ๆœ‰ๆ–ฐๆƒๆๅ‡บ็พ๏ผๆ›ดๆ–ฐ้ธๆ“‡ๅ™จไธฆ้€š็Ÿฅไฝฟ็”จ่€…
await refreshScanListSilently(files);
const newest = newFiles[0];
const timeStr = newest.modified ? newest.modified.substring(5, 16).replace('T', ' ') : '';
showToast(`๐ŸŸข ๆ–ฐๆƒๆๅฎŒๆˆ๏ผš${timeStr} โ€” ${newest.label || newest.name}`, 'success');
}
_lastKnownFilenames = currentNames;
} catch (e) {
// ่บซ้ป˜ๅคฑๆ•—๏ผŒไธๅฝฑ้Ÿฟไฝฟ็”จ่€…
console.warn('[CHECKPOINT] auto-refresh failed:', e);
}
}, 15000); // 15 ็ง’่ผช่ฉขไธ€ๆฌก
console.info('[CHECKPOINT] Auto-refresh started (15s interval)');
}
/**
* ้™้ป˜ๆ›ดๆ–ฐๆƒๆใ€€้ธๆ“‡ๅ™จ๏ผˆไธ่ฎŠๅ‹•็›ฎๅ‰้ธๅฎš้ …๏ผ‰
*/
async function refreshScanListSilently(files) {
const currentSelection = scanSelector.value;
// ้‡ๅปบ้ธ้ …
scanSelector.innerHTML = '';
files.sort((a, b) => (b.modified || '').localeCompare(a.modified || ''));
files.forEach(f => {
const opt = document.createElement('option');
opt.value = f.name;
const timeStr = f.modified ? f.modified.substring(5, 16).replace('T', ' ') : '';
const label = f.label || f.name;
opt.textContent = `${timeStr} โ€” ${label}`;
scanSelector.appendChild(opt);
});
// ๅฐ่ฉฆไฟ็•™ๅŽŸ้ธๆ“‡
if (currentSelection && files.some(f => f.name === currentSelection)) {
scanSelector.value = currentSelection;
} else if (files.length > 0) {
scanSelector.value = files[0].name;
}
}
loadScanList().then(() => {
// ่จ˜้Œ„ๅˆๅง‹ๅทฒ็Ÿฅๆช”ๆกˆ้›†
fetchScanList().then(files => {
_lastKnownFilenames = new Set(files.map(f => f.name));
});
// ๅ•Ÿๅ‹•่‡ชๅ‹•ๅˆทๆ–ฐ
startAutoRefresh();
});