更新: 77 个文件 - 2026-03-17 00:30:01
这个提交包含在:
@@ -1,95 +1,80 @@
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>websafe dashboard</title>
|
||||
<style>
|
||||
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 2rem; background: #f8fafc; color: #0f172a; }
|
||||
h1, h2 { margin-bottom: .5rem; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin: 1rem 0 2rem; }
|
||||
.card { background: white; border: 1px solid #cbd5e1; border-radius: 14px; padding: 1rem; box-shadow: 0 4px 18px rgba(15,23,42,.06); }
|
||||
.filters { display:flex; flex-wrap:wrap; gap:.75rem; margin: 1rem 0; }
|
||||
input, select { padding: .6rem .75rem; border: 1px solid #cbd5e1; border-radius: 10px; background: white; }
|
||||
table { width: 100%%; border-collapse: collapse; background: white; border-radius: 12px; overflow: hidden; margin-bottom: 2rem; }
|
||||
th, td { padding: .75rem; border-bottom: 1px solid #e2e8f0; text-align: left; font-size: .92rem; }
|
||||
code { background: #e2e8f0; padding: .1rem .35rem; border-radius: 6px; }
|
||||
.muted { color: #475569; }
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>websafe authorized lab dashboard</title>
|
||||
<link rel="stylesheet" href="./assets/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>websafe Local Lab Dashboard</h1>
|
||||
<p>LAB ONLY | AUTHORIZED TARGETS ONLY | 本地静态看板</p>
|
||||
<div id="summary" class="cards"></div>
|
||||
<h2>System Coverage</h2>
|
||||
<table>
|
||||
<thead><tr><th>System</th><th>Total</th><th>Verified Real</th><th>Verified Synthetic</th><th>Blocked</th><th>Manual</th><th>Browser</th><th>Latest</th></tr></thead>
|
||||
<tbody id="systemRows"></tbody>
|
||||
</table>
|
||||
<h2>Recent Runs</h2>
|
||||
<div class="filters">
|
||||
<input id="search" placeholder="Search advisory or run id">
|
||||
<select id="systemFilter"><option value="">All systems</option></select>
|
||||
<select id="statusFilter"><option value="">All statuses</option></select>
|
||||
<select id="familyFilter"><option value="">All profiles</option></select>
|
||||
<div class="dashboard-shell">
|
||||
<header class="hero">
|
||||
<div class="hero-grid">
|
||||
<div>
|
||||
<div class="eyebrow">Authorized Lab Dashboard</div>
|
||||
<h1>本地攻防实证工作台</h1>
|
||||
<p>面向授权实验场景的本地静态前端。聚合 advisory、run bundle、日志、浏览器证据、失败原因、利用思路与源头信息,并支持可折叠细节与自动刷新。</p>
|
||||
<div class="hero-actions">
|
||||
<button id="refreshDashboard" class="chip" type="button">Refresh Dashboard</button>
|
||||
<label class="ghost-chip"><input id="autoRefresh" type="checkbox" checked> Auto Refresh</label>
|
||||
<a class="ghost-chip" href="./summary.json" target="_blank" rel="noreferrer">Open Summary JSON</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="panel-header">
|
||||
<h2>Sync State</h2>
|
||||
<div id="syncState" class="sync-indicator"><span class="dot"></span><strong>Booting</strong><span>Loading generated JSON</span></div>
|
||||
</div>
|
||||
<div id="metrics" class="hero-meta"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace">
|
||||
<aside class="sidebar">
|
||||
<section class="glass-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Filters</h2>
|
||||
<span id="runCount" class="tag">0 shown</span>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<label>Search
|
||||
<input id="searchInput" placeholder="Search run id, advisory, title">
|
||||
</label>
|
||||
<label>System
|
||||
<select id="systemFilter"><option value="">All systems</option></select>
|
||||
</label>
|
||||
<label>Status
|
||||
<select id="statusFilter"><option value="">All statuses</option></select>
|
||||
</label>
|
||||
<label>Profile
|
||||
<select id="familyFilter"><option value="">All profiles</option></select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="glass-panel">
|
||||
<div class="panel-header"><h2>Systems</h2></div>
|
||||
<div id="systemCoverage" class="system-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="glass-panel">
|
||||
<div class="panel-header"><h2>Recent Failures</h2></div>
|
||||
<div id="failureFeed" class="failure-feed"></div>
|
||||
</section>
|
||||
|
||||
<section class="glass-panel">
|
||||
<div class="panel-header"><h2>Run Queue View</h2></div>
|
||||
<div id="runList" class="run-list"></div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main id="detailRoot" class="detail-view">
|
||||
<div class="glass-panel empty-state">Select a run to inspect full details.</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Run</th><th>System</th><th>Advisory</th><th>Status</th><th>Mode</th><th>Profile</th><th>Finished</th><th>Artifacts</th></tr></thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
<script>
|
||||
async function main() {
|
||||
const [summary, runs, systems] = await Promise.all([
|
||||
fetch('./summary.json').then(r => r.json()),
|
||||
fetch('./runs.json').then(r => r.json()),
|
||||
fetch('./systems.json').then(r => r.json())
|
||||
]);
|
||||
const summaryRoot = document.getElementById('summary');
|
||||
const cards = [{label: 'Advisories', value: summary.advisory_count}, {label: 'Run Count', value: summary.run_count}];
|
||||
for (const [key, value] of Object.entries(summary.statuses)) {
|
||||
cards.push({label: key, value});
|
||||
}
|
||||
summaryRoot.innerHTML = cards.map(item => `<div class="card"><strong>${item.label}</strong><div style="font-size:2rem;margin-top:.5rem;">${item.value}</div></div>`).join('');
|
||||
|
||||
const systemRows = document.getElementById('systemRows');
|
||||
systemRows.innerHTML = systems.map(item => `<tr><td><code>${item.system_id}</code></td><td>${item.total}</td><td>${item.verified_real}</td><td>${item.verified_synthetic}</td><td>${item.blocked}</td><td>${item.manual}</td><td>${item.browser_present}/${item.browser_required}</td><td>${item.latest_update || ''}</td></tr>`).join('');
|
||||
|
||||
const systemFilter = document.getElementById('systemFilter');
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const familyFilter = document.getElementById('familyFilter');
|
||||
const search = document.getElementById('search');
|
||||
const distinct = (values) => Array.from(new Set(values.filter(Boolean))).sort();
|
||||
systemFilter.innerHTML += distinct(runs.map(item => item.system_id)).map(value => `<option value="${value}">${value}</option>`).join('');
|
||||
statusFilter.innerHTML += distinct(runs.map(item => item.verification_status)).map(value => `<option value="${value}">${value}</option>`).join('');
|
||||
familyFilter.innerHTML += distinct(runs.map(item => item.repro_profile_id)).map(value => `<option value="${value}">${value}</option>`).join('');
|
||||
|
||||
const rows = document.getElementById('rows');
|
||||
function renderRows() {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
const filtered = runs.filter(item => {
|
||||
if (systemFilter.value && item.system_id !== systemFilter.value) return false;
|
||||
if (statusFilter.value && item.verification_status !== statusFilter.value) return false;
|
||||
if (familyFilter.value && item.repro_profile_id !== familyFilter.value) return false;
|
||||
if (query) {
|
||||
const haystack = `${item.run_id} ${item.advisory_id} ${item.system_id} ${item.repro_profile_id}`.toLowerCase();
|
||||
if (!haystack.includes(query)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
rows.innerHTML = filtered.map(item => {
|
||||
const links = [];
|
||||
if (item.dashboard_refs && item.dashboard_refs.report_html) links.push(`<a href="${item.dashboard_refs.report_html}">report</a>`);
|
||||
if (item.dashboard_refs && item.dashboard_refs.timeline) links.push(`<a href="${item.dashboard_refs.timeline}">timeline</a>`);
|
||||
if (item.dashboard_refs && item.dashboard_refs.bundle) links.push(`<a href="${item.dashboard_refs.bundle}">bundle</a>`);
|
||||
if (item.browser_links && item.browser_links.length) links.push(`<a href="${item.browser_links[0]}">browser</a>`);
|
||||
if (item.container_links && item.container_links.length) links.push(`<a href="${item.container_links[0]}">logs</a>`);
|
||||
const reason = item.blocked_reason ? `<div class="muted">${item.blocked_reason}</div>` : '';
|
||||
return `<tr><td><code>${item.run_id}</code>${reason}</td><td><code>${item.system_id}</code></td><td><code>${item.advisory_id}</code></td><td>${item.verification_status}</td><td>${item.verification_mode}</td><td><code>${item.repro_profile_id}</code></td><td>${item.finished_at || ''}</td><td>${links.join(' | ') || '-'}</td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
[systemFilter, statusFilter, familyFilter, search].forEach(node => node.addEventListener('input', renderRows));
|
||||
renderRows();
|
||||
}
|
||||
main();
|
||||
</script>
|
||||
<script src="./assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
在新工单中引用
屏蔽一个用户