diff --git a/web/static/css/style.css b/web/static/css/style.css index 234fa31f..1c82929a 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -7201,12 +7201,16 @@ html[data-theme="dark"] .openapi-doc-btn:hover { .terminal-panes { position: relative; + height: 400px; min-height: 400px; + overflow: hidden; } .terminal-pane { display: none; - min-height: 400px; + height: 100%; + min-height: 0; + overflow: hidden; } .terminal-pane.active { @@ -7214,38 +7218,46 @@ html[data-theme="dark"] .openapi-doc-btn:hover { } .terminal-container { - min-height: 400px; - padding: 8px; + height: 100%; + min-height: 0; + padding: 0; box-sizing: border-box; + overflow: hidden; } .terminal-container .xterm { - padding: 0; + padding: 8px 12px 8px 8px; + width: 100%; + height: 100%; + box-sizing: border-box; + overflow: hidden; + background: #0d1117; } .terminal-container .xterm-viewport { border-radius: 0; - /* 与 WebShell 终端一致:细窄、深色,避免系统默认浅色粗滚动条 */ + background: #0d1117; scrollbar-width: thin; - scrollbar-color: rgba(110, 118, 129, 0.5) transparent; + scrollbar-color: rgba(139, 148, 158, 0.7) rgba(22, 27, 34, 0.95); } .terminal-container .xterm-viewport::-webkit-scrollbar { - width: 6px; + width: 8px; } .terminal-container .xterm-viewport::-webkit-scrollbar-track { - background: transparent; - margin: 4px 0; - border-radius: 3px; + background: rgba(22, 27, 34, 0.95); + border-left: 1px solid rgba(48, 54, 61, 0.9); } .terminal-container .xterm-viewport::-webkit-scrollbar-thumb { - background: rgba(110, 118, 129, 0.4); - border-radius: 3px; + min-height: 44px; + background: rgba(139, 148, 158, 0.72); + border: 1px solid rgba(22, 27, 34, 0.95); + border-radius: 8px; } .terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover { - background: rgba(110, 118, 129, 0.65); + background: rgba(177, 186, 196, 0.82); } .terminal-container .xterm-viewport::-webkit-scrollbar-thumb:active { - background: rgba(139, 148, 158, 0.7); + background: rgba(201, 209, 217, 0.9); } .terminal-error { @@ -20031,6 +20043,160 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { transform: translateX(2px); } +.dashboard-section-actions { + display: inline-flex; + align-items: center; + gap: 10px; + margin-left: auto; + flex-shrink: 0; +} + +.dashboard-severity-status-filter { + display: inline-flex; + align-items: center; + position: relative; +} + +.dashboard-severity-status-filter-btn { + height: 30px; + min-width: 124px; + padding: 0 10px; + border-radius: 7px; + border: 1px solid rgba(148, 163, 184, 0.22); + background: rgba(248, 250, 252, 0.62); + color: var(--text-primary); + font-size: 0.78rem; + font-weight: 600; + cursor: pointer; + outline: none; + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 7px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.46); + transition: border-color 0.2s, box-shadow 0.2s, background 0.2s, transform 0.15s; +} + +.dashboard-severity-status-filter-btn:hover, +.dashboard-severity-status-filter.is-open .dashboard-severity-status-filter-btn { + border-color: rgba(0, 102, 255, 0.24); + background: rgba(255, 255, 255, 0.78); + box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.65); +} + +.dashboard-severity-status-filter-btn:focus-visible { + border-color: rgba(0, 102, 255, 0.42); + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.10); +} + +.dashboard-severity-status-filter-dot { + width: 6px; + height: 6px; + border-radius: 999px; + background: #64748b; + box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.12); + flex-shrink: 0; +} + +.dashboard-severity-status-filter[data-value="open"] .dashboard-severity-status-filter-dot { background: #ef4444; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.14); } +.dashboard-severity-status-filter[data-value="confirmed"] .dashboard-severity-status-filter-dot { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.16); } +.dashboard-severity-status-filter[data-value="fixed"] .dashboard-severity-status-filter-dot { background: #22c55e; box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16); } +.dashboard-severity-status-filter[data-value="false_positive"] .dashboard-severity-status-filter-dot { background: #94a3b8; box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.16); } +.dashboard-severity-status-filter[data-value="ignored"] .dashboard-severity-status-filter-dot { background: #64748b; box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.16); } + +.dashboard-severity-status-filter-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-severity-status-filter-caret { + width: 7px; + height: 7px; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + transform: translateY(-2px) rotate(45deg); + opacity: 0.62; + flex-shrink: 0; + transition: transform 0.18s ease, opacity 0.18s ease; +} + +.dashboard-severity-status-filter.is-open .dashboard-severity-status-filter-caret { + transform: translateY(2px) rotate(225deg); + opacity: 0.9; +} + +.dashboard-severity-status-filter-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + z-index: 30; + width: 156px; + padding: 5px; + border-radius: 9px; + border: 1px solid rgba(148, 163, 184, 0.18); + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.16), 0 1px 0 rgba(255, 255, 255, 0.65) inset; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.dashboard-severity-status-filter-option { + width: 100%; + height: 30px; + padding: 0 10px 0 28px; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--text-primary); + font-size: 0.78rem; + font-weight: 600; + text-align: left; + cursor: pointer; + position: relative; + transition: background 0.15s, color 0.15s; +} + +.dashboard-severity-status-filter-option::before { + content: ''; + position: absolute; + left: 10px; + top: 50%; + width: 9px; + height: 5px; + border-left: 2px solid currentColor; + border-bottom: 2px solid currentColor; + transform: translateY(-65%) rotate(-45deg); + opacity: 0; +} + +.dashboard-severity-status-filter-option:hover, +.dashboard-severity-status-filter-option.is-active { + background: rgba(0, 102, 255, 0.075); + color: #1d4ed8; +} + +.dashboard-severity-status-filter-option.is-active::before { + opacity: 1; +} + +@media (max-width: 640px) { + .dashboard-section-actions { + width: 100%; + justify-content: space-between; + } + .dashboard-section-header--severity { + flex-wrap: wrap; + } + .dashboard-severity-status-filter-btn { + min-width: 124px; + } + .dashboard-severity-status-filter-menu { + left: auto; + right: 0; + } +} + /* 最近漏洞 / 近期事实 Tab */ .dashboard-section-header--tabs { align-items: center; @@ -21540,10 +21706,16 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .dashboard-severity-status-grid { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; } +@media (max-width: 1180px) { + .dashboard-severity-status-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + @media (max-width: 480px) { .dashboard-severity-status-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -21591,6 +21763,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .dashboard-severity-status-cell.s-confirmed .dashboard-severity-status-icon { background: rgba(245, 158, 11, 0.12); color: #d97706; } .dashboard-severity-status-cell.s-fixed .dashboard-severity-status-icon { background: rgba(34, 197, 94, 0.12); color: #16a34a; } .dashboard-severity-status-cell.s-fp .dashboard-severity-status-icon { background: rgba(148, 163, 184, 0.18); color: #64748b; } +.dashboard-severity-status-cell.s-ignored .dashboard-severity-status-icon { background: rgba(100, 116, 139, 0.16); color: #64748b; } .dashboard-severity-status-text { display: flex; @@ -30833,6 +31006,37 @@ html[data-theme="dark"] .dashboard-feed-tabs { border-color: #263244 !important; } +html[data-theme="dark"] .dashboard-severity-status-filter-btn { + background: rgba(15, 23, 42, 0.46); + border-color: rgba(71, 85, 105, 0.58); + color: var(--text-primary); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.035); +} + +html[data-theme="dark"] .dashboard-severity-status-filter-btn:hover, +html[data-theme="dark"] .dashboard-severity-status-filter.is-open .dashboard-severity-status-filter-btn, +html[data-theme="dark"] .dashboard-severity-status-filter-btn:focus-visible { + background: rgba(23, 32, 51, 0.72); + border-color: rgba(96, 165, 250, 0.36); + box-shadow: 0 8px 18px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.05); +} + +html[data-theme="dark"] .dashboard-severity-status-filter-menu { + background: rgba(15, 23, 42, 0.94); + border-color: rgba(71, 85, 105, 0.62); + box-shadow: 0 16px 34px rgba(0,0,0,0.32), inset 0 1px 0 rgba(255,255,255,0.04); +} + +html[data-theme="dark"] .dashboard-severity-status-filter-option { + color: var(--text-primary); +} + +html[data-theme="dark"] .dashboard-severity-status-filter-option:hover, +html[data-theme="dark"] .dashboard-severity-status-filter-option.is-active { + background: rgba(96, 165, 250, 0.10); + color: #bfdbfe; +} + html[data-theme="dark"] .dashboard-feed-tab { color: var(--text-secondary); } @@ -31490,6 +31694,73 @@ html[data-theme="dark"] .projects-stat-chip--warn { border-color: rgba(245, 158, 11, 0.34) !important; } +html[data-theme="dark"] .projects-confidence--confirmed, +html[data-theme="dark"] .projects-fact-badge--ok { + background: rgba(34, 197, 94, 0.12) !important; + color: #6ee7a8 !important; + border-color: rgba(34, 197, 94, 0.22) !important; +} + +html[data-theme="dark"] .projects-confidence--tentative, +html[data-theme="dark"] .projects-fact-badge--warn { + background: rgba(245, 158, 11, 0.12) !important; + color: #fbbf24 !important; + border-color: rgba(245, 158, 11, 0.22) !important; +} + +html[data-theme="dark"] .projects-confidence--deprecated, +html[data-theme="dark"] .projects-fact-badge--na { + background: rgba(148, 163, 184, 0.11) !important; + color: #aebbd0 !important; + border-color: rgba(148, 163, 184, 0.20) !important; +} + +html[data-theme="dark"] .projects-category--target, +html[data-theme="dark"] .project-fact-graph-node-category--target { + background: rgba(59, 130, 246, 0.18) !important; + color: #93c5fd !important; + border-color: rgba(59, 130, 246, 0.36) !important; +} + +html[data-theme="dark"] .projects-category--auth, +html[data-theme="dark"] .projects-category--chain, +html[data-theme="dark"] .projects-category--custom, +html[data-theme="dark"] .project-fact-graph-node-category--chain, +html[data-theme="dark"] .project-fact-graph-node-category--vulnerability { + background: rgba(168, 85, 247, 0.16) !important; + color: #d8b4fe !important; + border-color: rgba(168, 85, 247, 0.34) !important; +} + +html[data-theme="dark"] .projects-category--infra, +html[data-theme="dark"] .projects-category--note, +html[data-theme="dark"] .project-fact-graph-node-category--infra, +html[data-theme="dark"] .project-fact-graph-node-category--note, +html[data-theme="dark"] .project-fact-graph-node-category--missing { + background: rgba(148, 163, 184, 0.14) !important; + color: #cbd5e1 !important; + border-color: rgba(148, 163, 184, 0.30) !important; +} + +html[data-theme="dark"] .projects-category--business, +html[data-theme="dark"] .project-fact-graph-node-category--auth, +html[data-theme="dark"] .project-fact-graph-node-category--business { + background: rgba(20, 184, 166, 0.16) !important; + color: #5eead4 !important; + border-color: rgba(20, 184, 166, 0.34) !important; +} + +html[data-theme="dark"] .projects-category--finding, +html[data-theme="dark"] .projects-category--exploit, +html[data-theme="dark"] .projects-category--poc, +html[data-theme="dark"] .project-fact-graph-node-category--finding, +html[data-theme="dark"] .project-fact-graph-node-category--exploit, +html[data-theme="dark"] .project-fact-graph-node-category--poc { + background: rgba(249, 115, 22, 0.16) !important; + color: #fdba74 !important; + border-color: rgba(249, 115, 22, 0.36) !important; +} + html[data-theme="dark"] .hitl-tool-badge { background: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%) !important; color: #ffffff !important; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 8509e310..ddb7b8b2 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -219,6 +219,8 @@ "statusFixed": "Fixed", "statusFalsePositive": "False positive", "statusIgnored": "Ignored", + "statusFilter": "Status filter", + "allStatuses": "All statuses", "fixRate": "Fix rate", "dataStale": "Data may be stale — please refresh", "recommendedActions": "Recommended Actions", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 2d98ae4a..91abe59e 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -212,6 +212,8 @@ "statusFixed": "已修复", "statusFalsePositive": "误报", "statusIgnored": "已忽略", + "statusFilter": "状态筛选", + "allStatuses": "全部状态", "fixRate": "修复率", "dataStale": "数据可能已过期,请手动刷新", "recommendedActions": "推荐操作", diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 6ad2ee14..9d09ef22 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -13,6 +13,8 @@ var DASHBOARD_POLL_INTERVAL_MS = 60 * 1000; var DASHBOARD_STALE_THRESHOLD_MS = 5 * 60 * 1000; var DASHBOARD_STALE_CHECK_INTERVAL_MS = 30 * 1000; +var DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY = 'cyberstrike.dashboard.severityStatusFilter'; +var DASHBOARD_SEVERITY_STATUS_FILTER_VALUES = ['', 'open', 'confirmed', 'fixed', 'ignored', 'false_positive']; var dashboardState = { currentController: null, // 当前正在进行的 fetch 的 AbortController @@ -23,6 +25,7 @@ var dashboardState = { lastResources: null, // 上一轮关键资源快照,用于判断是否首次有数据 / 智能 CTA recentFeedTab: 'vulns', // 最近漏洞 / 近期事实 Tab accessTab: 'c2', // 接入概览 Tab:c2 | webshell + severityStatusFilter: null, // 严重程度分布当前状态筛选:'' | open | confirmed | fixed | false_positive | ignored lastProjectSummary: null, // 最近一次项目仪表盘摘要(供 Tab 切换时重绘) }; @@ -99,6 +102,7 @@ async function refreshDashboard() { }; try { + var selectedSeverityStatus = getDashboardSeverityStatusFilter(); // /api/vulnerabilities/stats 只给出 by_severity 与 by_status 两个独立维度, // 无法得到「严重 × 待处理」的交叉计数。这里按四档各拉一次(limit=1,仅取 total), // 用真实的「待处理 × 各严重度」数量驱动告警条 / KPI 副标 / 风险概览卡的加权分, @@ -113,7 +117,7 @@ async function refreshDashboard() { hitlPendingRes, notificationsRes, externalMcpStatsRes, webshellRes, c2ListenersRes, c2SessionsRes, c2TasksRes, - projectSummaryRes + projectSummaryRes, severityFilteredStatsRes ] = await Promise.all([ fetchJson('/api/agent-loop/tasks'), fetchJson('/api/vulnerabilities/stats'), @@ -144,7 +148,8 @@ async function refreshDashboard() { fetchJson('/api/c2/listeners'), fetchJson('/api/c2/sessions?limit=500'), fetchJson('/api/c2/tasks?page=1&page_size=1'), - fetchJson('/api/projects/dashboard-summary?fact_limit=10') + fetchJson('/api/projects/dashboard-summary?fact_limit=10'), + selectedSeverityStatus ? fetchJson('/api/vulnerabilities/stats?status=' + encodeURIComponent(selectedSeverityStatus)) : Promise.resolve(null) ]); // 如果在 await 期间 controller 已被 abort,说明又有新刷新启动了,丢弃本次结果 @@ -191,6 +196,9 @@ async function refreshDashboard() { if (vulnTotalEl) vulnTotalEl.textContent = String(vulnRes.total); const bySeverity = vulnRes.by_severity || {}; const total = vulnRes.total || 0; + const severityDisplayRes = selectedSeverityStatus && severityFilteredStatsRes ? severityFilteredStatsRes : vulnRes; + const displayBySeverity = severityDisplayRes.by_severity || {}; + const displayTotal = typeof severityDisplayRes.total === 'number' ? severityDisplayRes.total : total; criticalCount = bySeverity.critical || 0; highCount = bySeverity.high || 0; mediumCount = bySeverity.medium || 0; @@ -200,17 +208,7 @@ async function refreshDashboard() { openHighCount = pickOpenCount(openHighRes, highCount); openMediumCount = pickOpenCount(openMediumRes, mediumCount); openLowCount = pickOpenCount(openLowRes, lowCount); - severityIds.forEach(sev => { - const count = bySeverity[sev] || 0; - const el = document.getElementById('dashboard-severity-' + sev); - if (el) el.textContent = String(count); - const pctEl = document.getElementById('dashboard-severity-' + sev + '-pct'); - if (pctEl) { - const pct = total > 0 ? Math.round((count / total) * 100) : 0; - pctEl.textContent = pct + '%'; - } - }); - renderSeverityDonut(bySeverity, total); + renderDashboardSeveritySummary(displayBySeverity, displayTotal, severityIds); renderVulnStatusPanel(vulnRes.by_status || {}, total); renderSeverityInsights( { critical: openCriticalCount, high: openHighCount, medium: openMediumCount, low: openLowCount }, @@ -1483,7 +1481,7 @@ function esc(s) { } // 漏洞处置状态 + 修复进度面板 -// byStatus: { open, confirmed, fixed, false_positive }(任一字段缺失视作 0) +// byStatus: { open, confirmed, fixed, false_positive, ignored }(任一字段缺失视作 0) // total: 漏洞总数(来自 stats.total) function renderVulnStatusPanel(byStatus, total) { var get = function (k) { @@ -1494,11 +1492,13 @@ function renderVulnStatusPanel(byStatus, total) { var confirmed = get('confirmed'); var fixed = get('fixed'); var fp = get('false_positive'); + var ignored = get('ignored'); setEl('dashboard-status-open', formatNumber(open)); setEl('dashboard-status-confirmed', formatNumber(confirmed)); setEl('dashboard-status-fixed', formatNumber(fixed)); setEl('dashboard-status-fp', formatNumber(fp)); + setEl('dashboard-status-ignored', formatNumber(ignored)); // 修复率:fixed / total(不计入 false_positive 时也可,按 total 维持一致) var t = Number(total || 0); @@ -1705,6 +1705,149 @@ function navigateToVulnerabilitiesWithFilter(opts) { } window.navigateToVulnerabilitiesWithFilter = navigateToVulnerabilitiesWithFilter; +function normalizeDashboardSeverityStatusFilter(status) { + status = String(status || ''); + return DASHBOARD_SEVERITY_STATUS_FILTER_VALUES.indexOf(status) >= 0 ? status : ''; +} + +function readDashboardSeverityStatusFilterFromStorage() { + try { + return normalizeDashboardSeverityStatusFilter(window.localStorage.getItem(DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY)); + } catch (_) { + return ''; + } +} + +function writeDashboardSeverityStatusFilterToStorage(status) { + try { + if (status) { + window.localStorage.setItem(DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY, status); + } else { + window.localStorage.removeItem(DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY); + } + } catch (_) { + // localStorage 可能被浏览器隐私设置禁用,筛选本身仍可在当前页面生效。 + } +} + +function dashboardSeverityStatusFilterLabel(status) { + status = normalizeDashboardSeverityStatusFilter(status); + if (!status) return dt('dashboard.allStatuses', null, '全部状态'); + return statusShortLabel(status); +} + +function getDashboardSeverityStatusFilter() { + if (dashboardState.severityStatusFilter === null) { + dashboardState.severityStatusFilter = readDashboardSeverityStatusFilterFromStorage(); + } + syncDashboardSeverityStatusFilterUI(); + return dashboardState.severityStatusFilter || ''; +} + +function updateDashboardSeverityStatusFilter(status) { + dashboardState.severityStatusFilter = normalizeDashboardSeverityStatusFilter(status); + writeDashboardSeverityStatusFilterToStorage(dashboardState.severityStatusFilter); + syncDashboardSeverityStatusFilterUI(); + refreshDashboard(); +} +window.updateDashboardSeverityStatusFilter = updateDashboardSeverityStatusFilter; + +function selectDashboardSeverityStatusFilter(status, ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + closeDashboardSeverityStatusFilterMenu(); + updateDashboardSeverityStatusFilter(status); +} +window.selectDashboardSeverityStatusFilter = selectDashboardSeverityStatusFilter; + +function syncDashboardSeverityStatusFilterUI() { + var status = dashboardState.severityStatusFilter; + if (status === null) status = readDashboardSeverityStatusFilterFromStorage(); + status = normalizeDashboardSeverityStatusFilter(status); + + var root = document.getElementById('dashboard-severity-status-filter'); + var textEl = document.getElementById('dashboard-severity-status-filter-text'); + if (root) root.setAttribute('data-value', status); + if (textEl) textEl.textContent = dashboardSeverityStatusFilterLabel(status); + + document.querySelectorAll('.dashboard-severity-status-filter-option[data-status]').forEach(function (item) { + var active = item.getAttribute('data-status') === status; + item.classList.toggle('is-active', active); + item.setAttribute('aria-selected', active ? 'true' : 'false'); + }); +} + +function toggleDashboardSeverityStatusFilterMenu(ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + syncDashboardSeverityStatusFilterUI(); + var root = document.getElementById('dashboard-severity-status-filter'); + var btn = document.getElementById('dashboard-severity-status-filter-btn'); + var menu = document.getElementById('dashboard-severity-status-filter-menu'); + if (!root || !btn || !menu) return; + var willOpen = menu.hasAttribute('hidden'); + menu.toggleAttribute('hidden', !willOpen); + root.classList.toggle('is-open', willOpen); + btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); + ensureDashboardSeverityStatusFilterOutsideListener(); +} +window.toggleDashboardSeverityStatusFilterMenu = toggleDashboardSeverityStatusFilterMenu; + +function closeDashboardSeverityStatusFilterMenu() { + var root = document.getElementById('dashboard-severity-status-filter'); + var btn = document.getElementById('dashboard-severity-status-filter-btn'); + var menu = document.getElementById('dashboard-severity-status-filter-menu'); + if (menu) menu.setAttribute('hidden', ''); + if (root) root.classList.remove('is-open'); + if (btn) btn.setAttribute('aria-expanded', 'false'); +} + +function ensureDashboardSeverityStatusFilterOutsideListener() { + if (dashboardState.severityStatusFilterOutsideBound) return; + dashboardState.severityStatusFilterOutsideBound = true; + document.addEventListener('click', function (ev) { + var root = document.getElementById('dashboard-severity-status-filter'); + if (root && root.contains(ev.target)) return; + closeDashboardSeverityStatusFilterMenu(); + }); + document.addEventListener('keydown', function (ev) { + if (ev.key === 'Escape') closeDashboardSeverityStatusFilterMenu(); + }); +} + +function openDashboardSeverityVulnerabilities() { + navigateToVulnerabilitiesWithFilter({ status: getDashboardSeverityStatusFilter() }); +} +window.openDashboardSeverityVulnerabilities = openDashboardSeverityVulnerabilities; + +function navigateToSeverityWithDashboardStatus(severity) { + navigateToVulnerabilitiesWithFilter({ + severity: severity, + status: getDashboardSeverityStatusFilter() + }); +} + +function renderDashboardSeveritySummary(bySeverity, total, severityIds) { + severityIds = Array.isArray(severityIds) && severityIds.length ? severityIds : ['critical', 'high', 'medium', 'low', 'info']; + total = Number(total || 0); + bySeverity = bySeverity && typeof bySeverity === 'object' ? bySeverity : {}; + severityIds.forEach(function (sev) { + var count = Number(bySeverity[sev] || 0) || 0; + var el = document.getElementById('dashboard-severity-' + sev); + if (el) el.textContent = String(count); + var pctEl = document.getElementById('dashboard-severity-' + sev + '-pct'); + if (pctEl) { + var pct = total > 0 ? Math.round((count / total) * 100) : 0; + pctEl.textContent = pct + '%'; + } + }); + renderSeverityDonut(bySeverity, total); +} + // 漏洞严重程度分布:半环形(donut)渲染 // 几何参数固定,便于配合 viewBox 0 0 560 320 的 SVG 容器 // 段间分隔由 gapRad 几何间隙完成,不使用描边,避免浅色/暗色下白边或黑边过重 @@ -2201,7 +2344,7 @@ function severityDonutClick(ev) { var id = target.getAttribute('data-severity'); if (!id) return; ev.preventDefault(); - navigateToVulnerabilitiesWithFilter({ severity: id }); + navigateToSeverityWithDashboardStatus(id); } function severityDonutKeydown(ev) { @@ -2210,7 +2353,7 @@ function severityDonutKeydown(ev) { if (!target) return; ev.preventDefault(); var id = target.getAttribute('data-severity'); - if (id) navigateToVulnerabilitiesWithFilter({ severity: id }); + if (id) navigateToSeverityWithDashboardStatus(id); } function severityLegendPointerOver(ev) { @@ -2236,7 +2379,7 @@ function severityLegendClick(ev) { var id = item.getAttribute('data-severity'); if (!id) return; ev.preventDefault(); - navigateToVulnerabilitiesWithFilter({ severity: id }); + navigateToSeverityWithDashboardStatus(id); } function severityLegendKeydown(ev) { @@ -2245,7 +2388,7 @@ function severityLegendKeydown(ev) { if (!item) return; ev.preventDefault(); var id = item.getAttribute('data-severity'); - if (id) navigateToVulnerabilitiesWithFilter({ severity: id }); + if (id) navigateToSeverityWithDashboardStatus(id); } // SVG 半环(背景轨迹)路径 @@ -2325,4 +2468,3 @@ document.addEventListener('click', function (ev) { var banner = document.getElementById('dashboard-alert-banner'); if (banner) banner.hidden = true; }); - diff --git a/web/static/js/fact-graph.js b/web/static/js/fact-graph.js index 001dd8d8..c652525e 100644 --- a/web/static/js/fact-graph.js +++ b/web/static/js/fact-graph.js @@ -1,6 +1,6 @@ /** * 项目事实图渲染(Cytoscape + ELK),供项目管理页使用。 - * 节点采用 SVG 卡片背景(图标 + 多行文字),避免 Cytoscape 原生 label 定位问题。 + * 节点采用 SVG 卡片背景(左上角图标 + 多行文字),避免 Cytoscape 原生 label 定位问题。 */ (function (global) { 'use strict'; @@ -25,11 +25,11 @@ const CARD_PAD = 14; const CARD_TEXT_PAD_RIGHT = 12; - const CARD_ICON = 36; - const CARD_ICON_GAP = 12; + const CARD_ICON = 24; + const CARD_ICON_GAP = 8; const CARD_TEXT_X = CARD_PAD + CARD_ICON + CARD_ICON_GAP; - const CARD_MIN_W = 300; - const CARD_TARGET_W = 360; + const CARD_MIN_W = 340; + const CARD_TARGET_W = 380; const CARD_MIN_H = 88; const CARD_MAX_H = 176; const CARD_HEADER_FS = 11; @@ -320,7 +320,7 @@ const isTentative = conf === 'tentative'; const isDeprecated = conf === 'deprecated'; const iconX = CARD_PAD; - const iconY = (height - CARD_ICON) / 2; + const iconY = CARD_PAD + 1; const headerY = CARD_PAD + CARD_HEADER_FS; const keyY = CARD_PAD + headerLines.length * CARD_HEADER_LH + CARD_SECTION_GAP + CARD_KEY_FS; const summaryY = diff --git a/web/static/js/terminal.js b/web/static/js/terminal.js index 1d3b37a2..3f97b666 100644 --- a/web/static/js/terminal.js +++ b/web/static/js/terminal.js @@ -72,6 +72,17 @@ else t.term.writeln(''); } + function writeTermData(tab, data) { + if (!tab || !tab.term || data === undefined || data === null) return; + try { + tab.term.write(data, function () { + try { tab.term.scrollToBottom(); } catch (e) {} + }); + } catch (e) { + tab.term.write(data); + } + } + function writeOutput(tab, text, isError) { var t = tab || getCurrent(); if (!t || !t.term || !text) return; @@ -136,18 +147,18 @@ // 处理二进制消息和文本消息 if (ev.data instanceof ArrayBuffer) { var decoder = new TextDecoder('utf-8'); - tab.term.write(decoder.decode(ev.data)); + writeTermData(tab, decoder.decode(ev.data)); } else if (ev.data instanceof Blob) { // Blob 类型,需要异步读取 var reader = new FileReader(); reader.onload = function () { var decoder = new TextDecoder('utf-8'); - tab.term.write(decoder.decode(reader.result)); + writeTermData(tab, decoder.decode(reader.result)); }; reader.readAsArrayBuffer(ev.data); } else { // 字符串类型 - tab.term.write(ev.data); + writeTermData(tab, ev.data); } }; @@ -183,6 +194,9 @@ fontSize: 13, fontFamily: 'Menlo, Monaco, "Courier New", monospace', lineHeight: 1.2, + smoothScrollDuration: 0, + scrollSensitivity: 1, + fastScrollSensitivity: 5, scrollback: 1000, theme: { background: '#0d1117', @@ -258,6 +272,19 @@ tab.term = term; tab.fitAddon = fitAddon; + if (typeof ResizeObserver !== 'undefined' && fitAddon) { + var resizeTimer; + tab.resizeObserver = new ResizeObserver(function () { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + try { + fitAddon.fit(); + term.scrollToBottom(); + } catch (e) {} + }, 50); + }); + tab.resizeObserver.observe(container); + } // 立即建立 WebSocket,让后端 PTY/Shell 马上启动并输出提示符; // 若等到首次按键才 connect,用户会感觉必须先按回车才能输入(实为连接尚未建立)。 ensureTerminalWS(tab); @@ -278,9 +305,19 @@ }); var t = getCurrent(); if (t && t.term) { + try { + if (t.fitAddon) t.fitAddon.fit(); + t.term.scrollToBottom(); + } catch (e) {} if (prevId !== id) { requestAnimationFrame(function () { - if (currentTabId === id && t.term) t.term.focus(); + if (currentTabId === id && t.term) { + try { + if (t.fitAddon) t.fitAddon.fit(); + t.term.scrollToBottom(); + } catch (e) {} + t.term.focus(); + } }); } else { t.term.focus(); @@ -356,9 +393,11 @@ var switchToIndex = deletingCurrent ? (idx > 0 ? idx - 1 : 0) : -1; var tab = terminals[idx]; + if (tab.resizeObserver && tab.resizeObserver.disconnect) tab.resizeObserver.disconnect(); if (tab.term && tab.term.dispose) tab.term.dispose(); tab.term = null; tab.fitAddon = null; + tab.resizeObserver = null; terminals.splice(idx, 1); var tabDiv = document.querySelector('.terminal-tab[data-tab-id="' + id + '"]'); diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js index 65369d7d..3b74d3b4 100644 --- a/web/static/js/vulnerability.js +++ b/web/static/js/vulnerability.js @@ -39,7 +39,7 @@ function vulnStatusLabel(code) { return m[code] ? vulnT(m[code]) : code; } -const VULN_STATUS_CODES = ['open', 'confirmed', 'fixed', 'false_positive', 'ignored']; +const VULN_STATUS_CODES = ['open', 'confirmed', 'fixed', 'ignored', 'false_positive']; const VULNERABILITY_REMOVE_ANIM_MS = 200; function getVulnerabilityScrollContainer() { diff --git a/web/templates/index.html b/web/templates/index.html index 4c6b5522..d9c48c1b 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -468,9 +468,26 @@
-
+

漏洞严重程度分布

- 查看全部 → +
+
+ + +
+ 查看全部 → +
-
+
@@ -567,7 +584,7 @@ 待处理
-
+
@@ -576,7 +593,7 @@ 已确认
-
+
@@ -585,7 +602,16 @@ 已修复
-
+
+ +
+ 0 + 已忽略 +
+
+
@@ -1978,8 +2004,8 @@ - +
@@ -2178,8 +2204,8 @@ - +
@@ -4983,8 +5009,8 @@ - +