refactor(vulnerabilities): use compact rows with drawer actions

This commit is contained in:
Sycun committed 2026-11-13 17:24:37 +08:00
1 parent cab10bfcc4
commit 648002b09b
8 files changed
+181 -153

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
.vulnerability-record-list {
display: flex;
flex-direction: column;
gap: 8px;
list-style: none;
padding: 0;
margin: 0;
min-width: 0;
}
.vulnerability-record-row {
display: grid;
grid-template-columns: 18px 52px minmax(0, 1fr) auto;
align-items: center;
column-gap: 12px;
min-height: 72px;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--bg-primary);
}
.vulnerability-record-row:hover {
border-color: color-mix(in srgb, var(--accent-color) 35%, var(--border-color));
}
.vulnerability-record-row:has(.vulnerability-select-record:checked) {
background: color-mix(in srgb, var(--accent-color) 5%, var(--bg-primary));
}
.vulnerability-record-select {
display: flex;
align-items: center;
justify-content: center;
}
.vulnerability-record-select .vulnerability-select-record {
margin: 0;
}
.vulnerability-record-row > .severity-badge {
justify-self: start;
white-space: nowrap;
}
.vulnerability-record-main {
display: block;
min-width: 0;
width: 100%;
padding: 0;
border: 0;
background: transparent;
color: var(--text-primary);
font: inherit;
text-align: start;
cursor: pointer;
}
.vulnerability-record-title-line {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.vulnerability-record-title,
.vulnerability-record-summary {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.5;
}
.vulnerability-record-title {
min-width: 0;
font-size: 14px;
font-weight: 600;
}
.vulnerability-record-summary {
margin-top: 3px;
font-size: 12px;
color: var(--text-secondary);
}
.vulnerability-record-title-line .vulnerability-case-badge {
margin: 0;
flex-shrink: 0;
}
.vulnerability-record-actions {
display: flex;
align-items: center;
gap: 16px;
}
.vulnerability-record-view {
padding: 6px 0;
border: 0;
background: transparent;
color: var(--text-primary);
font: inherit;
font-size: 13px;
white-space: nowrap;
cursor: pointer;
}
.vulnerability-record-view:hover,
.vulnerability-record-main:hover .vulnerability-record-title {
color: var(--accent-color);
}
.vulnerability-record-main:focus-visible,
.vulnerability-record-view:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 4px;
}
.vulnerability-inspector-actions .btn-delete {
margin-inline-start: auto;
color: var(--error-color);
}
@media (max-width: 600px) {
.vulnerability-record-row {
grid-template-columns: 18px 46px minmax(0, 1fr);
gap: 8px;
padding: 12px;
}
.vulnerability-record-main {
padding: 0 !important;
min-height: 0 !important;
}
.vulnerability-record-actions {
grid-column: 2 / -1;
justify-content: flex-end;
}
}
+1
View File
@@ -2720,6 +2720,7 @@
"errorGeneric": "حدث خطأ ما. يُرجى المحاولة مرة أخرى."
},
"vulnerabilityPage": {
"viewFinding": "عرض التفاصيل",
"taskSearch": "البحث في المهام أو قوائم الانتظار أو الوسوم…",
"allTasks": "جميع المهام",
"clearTaskSelection": "مسح مرشح المهام",
+1
View File
@@ -2862,6 +2862,7 @@
"errorGeneric": "Something went wrong. Please try again."
},
"vulnerabilityPage": {
"viewFinding": "View details",
"taskSearch": "Search tasks, queues or tags…",
"allTasks": "All tasks",
"clearTaskSelection": "Clear task filter",
+1
View File
@@ -2875,6 +2875,7 @@
"errorGeneric": "Что-то пошло не так. Повторите попытку."
},
"vulnerabilityPage": {
"viewFinding": "Подробнее",
"taskSearch": "Поиск задач, очередей или тегов…",
"allTasks": "Все задачи",
"clearTaskSelection": "Сбросить фильтр задач",
+1
View File
@@ -2850,6 +2850,7 @@
"errorGeneric": "操作失败,请稍后重试。"
},
"vulnerabilityPage": {
"viewFinding": "查看详情",
"taskSearch": "搜索任务、队列或标签…",
"allTasks": "全部任务",
"clearTaskSelection": "清除任务筛选",
+1
View File
@@ -152,6 +152,7 @@ async function openVulnerabilityInspector(id) {
<button class="btn-primary" data-require-permission="vulnerability:write" onclick="openVulnerabilityRetest(${jsID})" ${v.conversation_id ? '' : 'disabled'}>${escapeHtml(vulnT('vulnerabilityRetest.button'))}</button>
<button class="btn-secondary" data-require-permission="vulnerability:write" onclick="editVulnerability(${jsID})">${escapeHtml(vulnT('common.edit'))}</button>
<button class="btn-secondary" onclick="openVulnerabilityDownload(${jsID},event)">${escapeHtml(vulnT('vulnerabilityPage.tableDownload'))}</button>
<button class="btn-secondary btn-delete" data-require-permission="vulnerability:delete" onclick="deleteVulnerability(${jsID})">${escapeHtml(vulnT('common.delete'))}</button>
</div>
<div class="vulnerability-inspector-tabs" role="group">
${['overview','evidence','history'].map(tab => `<button type="button" data-inspector-tab="${tab}" aria-pressed="${tab==='overview'}" onclick="switchVulnerabilityInspectorTab('${tab}')">${escapeHtml(vulnT('vulnerabilityPage.inspector_' + tab))}</button>`).join('')}
+34 -151
View File
@@ -47,33 +47,6 @@ function getVulnerabilityScrollContainer() {
return page ? page.querySelector('.page-content') : null;
}
function getExpandedVulnerabilityIds() {
const ids = [];
document.querySelectorAll('#vulnerabilities-list .vulnerability-content').forEach(function (el) {
if (el.style.display !== 'none') {
const id = (el.id || '').replace(/^content-/, '');
if (id) ids.push(id);
}
});
return ids;
}
function restoreExpandedVulnerabilityDetails(expandedIds) {
if (!expandedIds || !expandedIds.length) return;
expandedIds.forEach(function (id) {
const content = document.getElementById('content-' + id);
const icon = document.getElementById('expand-icon-' + id);
if (!content || content.style.display !== 'none') return;
content.style.display = 'block';
const row = content.closest('.vulnerability-table-detail');
if (row) row.hidden = false;
const trigger = document.getElementById('vulnerability-row-toggle-' + id);
if (trigger) trigger.setAttribute('aria-expanded', 'true');
if (icon) icon.style.transform = 'rotate(90deg)';
loadVulnerabilityRelatedFacts(id).catch(function (e) { console.warn(e); });
});
}
function buildVulnerabilityStatusPicker(vuln) {
const current = vuln.status || 'open';
const id = escapeHtml(vuln.id);
@@ -1304,13 +1277,9 @@ async function loadVulnerabilities(page = null, options = {}) {
const opts = options && typeof options === 'object' ? options : {};
const preserveScroll = !!opts.preserveScroll;
const silent = !!opts.silent;
let expandedIds = opts.expandedIds;
const scrollEl = preserveScroll ? getVulnerabilityScrollContainer() : null;
const scrollTop = scrollEl ? scrollEl.scrollTop : 0;
if (expandedIds === undefined && preserveScroll) {
expandedIds = getExpandedVulnerabilityIds();
}
const listContainer = document.getElementById('vulnerabilities-list');
const hostOpenStates = preserveScroll
@@ -1375,7 +1344,7 @@ async function loadVulnerabilities(page = null, options = {}) {
}
renderVulnerabilities(vulnerabilities, {
view, expandedIds: expandedIds || [], hostOpenStates,
view, hostOpenStates,
preserveHostSelection: preserveScroll
});
renderVulnerabilityPagination();
@@ -1419,18 +1388,15 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
const exactMatch = vulnerabilities.length === 1 && vulnerabilityFilters.id === vulnerabilities[0].id
? vulnerabilities[0].id
: null;
const expandedIds = exactMatch ? [exactMatch] : opts.expandedIds;
listContainer.innerHTML = renderVulnerabilityTable(vulnerabilities);
listContainer.innerHTML = renderVulnerabilityList(vulnerabilities);
initVulnerabilityStatusPickers(listContainer);
initVulnerabilityProjectBindSelects(listContainer);
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(listContainer);
}
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(listContainer);
if (exactMatch && vulnerabilities[0].summary_only && typeof openVulnerabilityInspector === 'function') {
if (exactMatch && typeof openVulnerabilityInspector === 'function') {
openVulnerabilityInspector(exactMatch);
} else {
restoreExpandedVulnerabilityDetails(expandedIds);
}
updateVulnerabilityListSummary();
}
@@ -1448,7 +1414,7 @@ function updateVulnerabilityListSummary() {
if (!list || !toolbar) return;
const count = ['host','task'].includes(vulnerabilityView) && list.querySelector('.vulnerability-host-browser')
? new Set(vulnerabilityBrowserRows.map(vuln => vuln.id)).size
: list.querySelectorAll('.vulnerability-card, .vulnerability-table-item').length;
: list.querySelectorAll('.vulnerability-record-row').length;
document.getElementById('vulnerability-page-summary').textContent = count
? vulnT('vulnerabilityPage.pageFindings', { count }) : '';
}
@@ -1547,7 +1513,7 @@ function renderVulnerabilityBrowser(vulnerabilities, opts) {
!Array.from(list.querySelectorAll('[data-host-key]')).some(el => el.dataset.hostKey === vulnerabilityBrowserSelection)) {
vulnerabilityBrowserSelection = '';
}
renderVulnerabilityBrowserTable(opts.expandedIds);
renderVulnerabilityBrowserTable();
if (activeSearch) {
search.focus({ preventScroll: true });
search.setSelectionRange(caret, caret);
@@ -1586,7 +1552,7 @@ function renderVulnerabilityHostRiskDot(items) {
return highest ? `<span class="vulnerability-host-risk-dot severity-${highest}" title="${escapeAttr(vulnSeverityLabel(highest))}"></span>` : '';
}
function renderVulnerabilityBrowserTable(expandedIds) {
function renderVulnerabilityBrowserTable() {
const selection = vulnerabilityBrowserSelection;
const items = getVisibleVulnerabilityBrowserItems();
document.querySelectorAll('#vulnerabilities-list [data-host-key]').forEach(option => {
@@ -1613,100 +1579,41 @@ function renderVulnerabilityBrowserTable(expandedIds) {
).join('') + `<span class="vulnerability-group-pending">${escapeHtml(vulnT('vulnerabilityPage.groupPending', { count: pending }))}</span>`;
document.getElementById('vulnerability-host-export').disabled = items.length === 0;
const container = document.getElementById('vulnerability-host-table');
container.innerHTML = renderVulnerabilityTable(items);
container.innerHTML = renderVulnerabilityList(items);
initVulnerabilityStatusPickers(container);
initVulnerabilityProjectBindSelects(container);
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
const exact = items.find(vuln => vulnerabilityFilters.id === vuln.id);
if (exact?.summary_only && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id);
else restoreExpandedVulnerabilityDetails(exact ? [exact.id] : expandedIds);
if (exact && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id);
updateVulnerabilityListSummary();
}
function renderVulnerabilityTable(items) {
return items.length ? `<div class="vulnerability-table-scroll">
<table class="vulnerability-host-table">
<colgroup>
<col class="vulnerability-col-select"><col class="vulnerability-col-severity"><col>
<col class="vulnerability-col-target"><col class="vulnerability-col-status">
<col class="vulnerability-col-task"><col class="vulnerability-col-date"><col class="vulnerability-col-actions">
</colgroup>
<thead><tr>
<th scope="col" class="vulnerability-select-column">${escapeHtml(vulnT('vulnerabilityPage.selectColumn'))}</th>
${['tableSeverity', 'tableTitle', 'detailTarget', 'tableStatus', 'tableTask', 'tableDate', 'tableActions']
.map(key => '<th scope="col">' + escapeHtml(vulnT('vulnerabilityPage.' + key)) + '</th>').join('')}
</tr></thead>
${items.map(renderVulnerabilityTableItem).join('')}
</table></div>` : `<div class="empty-state">${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}</div>`;
function renderVulnerabilityList(items) {
if (!items.length) {
return `<div class="empty-state">${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}</div>`;
}
return `<ul class="vulnerability-record-list">${items.map(renderVulnerabilityListItem).join('')}</ul>`;
}
function renderVulnerabilityTableItem(vuln) {
function renderVulnerabilityListItem(vuln) {
const id = escapeAttr(vuln.id);
const jsID = escapeJsStringAttr(vuln.id);
const task = vuln.task_tag || vuln.task_id || vulnT('vulnerabilityPage.viewNoTask');
const targetHosts = Array.from(new Map((vuln.targets || []).map(target => [target.host,target])).values());
const created = new Date(vuln.created_at);
return `<tbody class="vulnerability-table-item" id="vulnerability-card-${id}" data-vuln-id="${id}">
<tr>
<td class="vulnerability-select-column">${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''}</td>
<td><span class="severity-badge severity-${escapeAttr(vuln.severity)}">${escapeHtml(vulnSeverityLabel(vuln.severity))}</span></td>
<td class="vulnerability-table-title">
<button type="button" id="vulnerability-row-toggle-${id}" class="vulnerability-row-toggle"
aria-expanded="false" aria-controls="content-${id}" onclick="${vuln.summary_only ? 'openVulnerabilityInspector' : 'toggleVulnerabilityDetails'}(${jsID})">
<span id="expand-icon-${id}" class="vulnerability-row-caret" aria-hidden="true">›</span>
<span class="vulnerability-table-name" title="${escapeAttr(vuln.title)}">${escapeHtml(vuln.title)}</span>
</button>
<p>${escapeHtml((vuln.description || '').slice(0, 160))}</p>
const summary = (vuln.description || vuln.target || '').replace(/\s+/g, ' ').trim();
return `<li class="vulnerability-record-row" id="vulnerability-card-${id}" data-vuln-id="${id}">
<span class="vulnerability-record-select">${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''}</span>
<span class="severity-badge severity-${escapeAttr(vuln.severity)}">${escapeHtml(vulnSeverityLabel(vuln.severity))}</span>
<button type="button" class="vulnerability-record-main" aria-haspopup="dialog" onclick="openVulnerabilityInspector(${jsID})">
<span class="vulnerability-record-title-line">
<span class="vulnerability-record-title" title="${escapeAttr(vuln.title)}">${escapeHtml(vuln.title)}</span>
${vuln.case_id ? `<span class="vulnerability-case-badge">${escapeHtml(vulnT('vulnerabilityPage.mergedRecord'))}</span>` : ''}
</td>
<td>${targetHosts.length ? targetHosts.slice(0,2).map(target =>
`<span class="vulnerability-table-target" title="${escapeAttr(target.raw)}">${escapeHtml(target.host)}</span>`).join('')
: `<span class="vulnerability-table-target" title="${escapeAttr(vuln.target || '')}">${escapeHtml(vuln.target_host || vuln.target || '—')}</span>`}
${(vuln.targets || []).length > 1 ? `<small>${escapeHtml(vulnT('vulnerabilityPage.targetCount', {count:vuln.targets.length}))}</small>` : ''}
</td>
<td>${buildVulnerabilityStatusPicker(vuln)}</td>
<td><span class="vulnerability-table-task" title="${escapeAttr(vuln.task_id || task)}">${escapeHtml(task)}</span></td>
<td class="vulnerability-table-date" title="${escapeAttr(created.toLocaleString(vulnDateLocale()))}">${escapeHtml(created.toLocaleString(vulnDateLocale(), {
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
}))}</td>
<td><div class="vulnerability-table-actions">
<button type="button" class="btn-ghost" data-require-permission="vulnerability:write" onclick="openVulnerabilityRetest(${jsID})" ${vuln.conversation_id ? '' : 'disabled'}>${escapeHtml(vulnT('vulnerabilityRetest.button'))}</button>
<button type="button" class="btn-ghost" onclick="openVulnerabilityDownload(${jsID}, event)">${escapeHtml(vulnT('vulnerabilityPage.tableDownload'))}</button>
<button type="button" class="btn-ghost" data-require-permission="vulnerability:write" onclick="editVulnerability(${jsID})">${escapeHtml(vulnT('common.edit'))}</button>
<button type="button" class="btn-ghost" data-require-permission="vulnerability:delete" onclick="deleteVulnerability(${jsID})">${escapeHtml(vulnT('common.delete'))}</button>
</div></td>
</tr>
${vuln.summary_only ? '' : `<tr class="vulnerability-table-detail" hidden><td colspan="8">${renderVulnerabilityDetailsContent(vuln)}</td></tr>`}
</tbody>`;
}
function renderVulnerabilityDetailsContent(vuln) {
return `
<div class="vulnerability-content" id="content-${vuln.id}" style="display: none;">
${vuln.description ? `<div class="vulnerability-description">${escapeHtml(vuln.description)}</div>` : ''}
<div class="vulnerability-details">
${vulnDetailField(vulnT('vulnerabilityPage.detailVulnId'), vuln.id, true)}
${vulnDetailProjectField(vuln)}
${vuln.type ? vulnDetailField(vulnT('vulnerabilityPage.detailType'), vuln.type, false) : ''}
${vuln.target ? vulnDetailField(vulnT('vulnerabilityPage.detailTarget'), vuln.target, false) : ''}
${vulnDetailConversationField(vuln.conversation_id)}
${vuln.task_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskId'), vuln.task_id, true) : ''}
${vuln.task_queue_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskQueueId'), vuln.task_queue_id, true) : ''}
${vuln.conversation_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailConversationTag'), vuln.conversation_tag, false) : ''}
${vuln.task_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskTag'), vuln.task_tag, false) : ''}
</div>
<div class="vulnerability-repro">
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailPreconditions'), vuln.preconditions)}
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailReproductionSteps'), vuln.reproduction_steps)}
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailEvidence'), vuln.evidence, { code: true })}
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailImpact'), vuln.impact)}
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRecommendation'), vuln.recommendation)}
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestNotes'), vuln.retest_notes)}
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestLog'), vuln.retest_log, { code: true })}
</div>
<div class="vulnerability-related-facts" id="vuln-related-facts-${escapeAttr(vuln.id)}" data-project-id="${escapeAttr(vuln.project_id || '')}" data-vuln-id="${escapeAttr(vuln.id)}" hidden></div>
</div>
`;
</span>
${summary ? `<span class="vulnerability-record-summary" title="${escapeAttr(summary)}">${escapeHtml(summary)}</span>` : ''}
</button>
<div class="vulnerability-record-actions">
${buildVulnerabilityStatusPicker(vuln)}
<button type="button" class="vulnerability-record-view" aria-haspopup="dialog" onclick="openVulnerabilityInspector(${jsID})">${escapeHtml(vulnT('vulnerabilityPage.viewFinding'))}</button>
</div>
</li>`;
}
// 渲染分页控件
@@ -1977,7 +1884,6 @@ async function saveVulnerability() {
if (typeof refreshVulnerabilityInspector === 'function') refreshVulnerabilityInspector(currentVulnerabilityId);
const editedId = currentVulnerabilityId;
const isEdit = !!editedId;
const expandedIds = isEdit ? getExpandedVulnerabilityIds() : [];
closeVulnerabilityModal();
loadVulnerabilityStats();
@@ -1994,7 +1900,7 @@ async function saveVulnerability() {
return;
}
await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
await loadVulnerabilities(null, { preserveScroll: true, silent: true });
} catch (error) {
console.error('保存漏洞失败:', error);
alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message);
@@ -2014,6 +1920,9 @@ async function deleteVulnerability(id) {
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed'));
if (typeof vulnerabilityInspectorID !== 'undefined' && vulnerabilityInspectorID === id) {
closeAppModal('vulnerability-inspector');
}
loadVulnerabilityStats();
await removeVulnerabilityCard(id);
} catch (error) {
@@ -2151,28 +2060,6 @@ async function saveVulnerabilityAlertSubscription() {
}
}
// 切换漏洞详情展开/折叠
function toggleVulnerabilityDetails(id) {
const content = document.getElementById(`content-${id}`);
const icon = document.getElementById(`expand-icon-${id}`);
if (!content || !icon) return;
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
loadVulnerabilityRelatedFacts(id).catch((e) => console.warn(e));
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
}
const expanded = content.style.display !== 'none';
const row = content.closest('.vulnerability-table-detail');
if (row) row.hidden = !expanded;
const trigger = document.getElementById('vulnerability-row-toggle-' + id);
if (trigger) trigger.setAttribute('aria-expanded', String(expanded));
}
/** 展开漏洞详情时加载关联项目事实 */
async function loadVulnerabilityRelatedFacts(vulnId) {
const el = document.getElementById(`vuln-related-facts-${vulnId}`);
@@ -2658,11 +2545,7 @@ async function bindVulnerabilityProject(vulnId, projectId, silent) {
alert(vulnT('vulnerabilityPage.projectBindOk'));
}
loadVulnerabilityStats();
const expandedIds = getExpandedVulnerabilityIds();
if (!expandedIds.includes(vulnId)) {
expandedIds.push(vulnId);
}
await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
await loadVulnerabilities(null, { preserveScroll: true, silent: true });
} catch (error) {
console.error('绑定项目失败:', error);
alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message);
+3 -2
View File
@@ -48,6 +48,7 @@
<!-- 移动端样式层:全部规则限定在 767.98px 以下,桌面端零影响 -->
<link rel="stylesheet" href="/static/css/mobile.css?v=20261015-project-table-3">
<link rel="stylesheet" href="/static/css/vulnerability-workspace.css?v=20261113-tree-panel-height">
<link rel="stylesheet" href="/static/css/vulnerability-list.css?v=20261113-compact-rows">
<script src="/static/js/router.js?v=20261011-token-settings"></script>
</head>
<body>
@@ -7096,8 +7097,8 @@
<script src="/static/js/skills.js?v=20261027-pagination-custom-select"></script>
<script src="/static/js/fact-graph.js"></script>
<script src="/static/js/projects.js?v=20260819-1-pagination-20261027"></script>
<script src="/static/js/vulnerability.js?v=20261113-unified-list"></script>
<script src="/static/js/vulnerability-workspace.js?v=20261113-drawer-dismiss"></script>
<script src="/static/js/vulnerability.js?v=20261113-compact-rows"></script>
<script src="/static/js/vulnerability-workspace.js?v=20261113-compact-rows"></script>
<script src="/static/js/vulnerability-download.js?v=20261030-2"></script>
<script src="/static/js/webshell.js?v=20261011-token-usage"></script>
<script src="/static/js/chat-files.js?v=20261027-pagination-custom-select"></script>