diff --git a/web/static/css/style.css b/web/static/css/style.css index c4f6e012..2d4f2c3c 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -5640,10 +5640,738 @@ header { border-color: var(--accent-color); } +.monitor-section .section-actions input#monitor-tool-filter.is-filter-active { + border-color: rgba(0, 102, 255, 0.45); + background: rgba(0, 102, 255, 0.04); + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.12); +} + .monitor-section .section-actions input[type="text"]::placeholder { color: var(--text-muted); } +/* MCP 执行统计:概览 KPI + 工具排行 */ +.monitor-stats-section-header { + align-items: flex-start; +} + +.monitor-stats-header-text h3 { + margin: 0; +} + +.monitor-stats-subtitle { + margin: 4px 0 0; + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 500; +} + +.mcp-exec-stats-root { + width: 100%; + min-width: 0; +} + +.mcp-exec-stats { + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; +} + +.mcp-stats-kpi-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +@media (max-width: 900px) { + .mcp-stats-kpi-row { + grid-template-columns: 1fr; + } +} + +.mcp-stats-kpi-card { + background: #fff; + border-radius: 14px; + padding: 18px 20px; + border: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + gap: 10px; + min-height: 118px; + min-width: 0; + transition: transform 0.2s ease, box-shadow 0.25s ease, border-color 0.2s ease; +} + +.mcp-stats-kpi-card--calls { + background: linear-gradient(145deg, #fff 0%, #f0f9ff 100%); +} + +.mcp-stats-kpi-card--rate { + background: linear-gradient(145deg, #fff 0%, #f0fdfa 100%); +} + +.mcp-stats-kpi-card--time { + background: linear-gradient(145deg, #fff 0%, #faf5ff 100%); +} + +.mcp-stats-kpi-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.mcp-stats-kpi-label { + font-size: 0.8125rem; + color: var(--text-secondary); + font-weight: 500; +} + +.mcp-stats-kpi-icon { + width: 28px; + height: 28px; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.mcp-stats-kpi-icon--calls { background: rgba(59, 130, 246, 0.1); color: #3b82f6; } +.mcp-stats-kpi-icon--rate { background: rgba(20, 184, 166, 0.1); color: #14b8a6; } +.mcp-stats-kpi-icon--time { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; } + +.mcp-stats-kpi-body { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; +} + +.mcp-stats-kpi-value { + font-size: 1.875rem; + font-weight: 800; + color: var(--text-primary); + line-height: 1.1; + letter-spacing: -0.03em; + font-variant-numeric: tabular-nums; +} + +.mcp-stats-kpi-value--time { + font-size: 1.05rem; + font-weight: 700; + letter-spacing: -0.01em; + line-height: 1.35; + word-break: break-word; +} + +.mcp-stats-kpi-sub { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + min-height: 22px; +} + +.mcp-stats-kpi-sub-text { + font-size: 0.75rem; + color: var(--text-secondary); + font-weight: 500; +} + +.mcp-stats-kpi-sub-text.is-success { color: #15803d; } +.mcp-stats-kpi-sub-text.is-warning { color: #ca8a04; } +.mcp-stats-kpi-sub-text.is-danger { color: #dc2626; } + +.mcp-stats-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.6875rem; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.mcp-stats-pill--success { + background: rgba(34, 197, 94, 0.12); + color: #15803d; +} + +.mcp-stats-pill--fail { + background: rgba(239, 68, 68, 0.1); + color: #b91c1c; +} + +.mcp-stats-stacked-bar { + display: flex; + height: 6px; + border-radius: 999px; + overflow: hidden; + background: rgba(0, 0, 0, 0.06); + gap: 1px; + margin-top: 2px; +} + +.mcp-stats-stacked-bar-seg { + min-width: 0; + transition: flex-grow 0.35s ease; +} + +.mcp-stats-stacked-bar-seg--success { + background: linear-gradient(90deg, #22c55e, #16a34a); +} + +.mcp-stats-stacked-bar-seg--fail { + background: linear-gradient(90deg, #f87171, #dc2626); +} + +.mcp-stats-ring-wrap { + position: relative; + width: 56px; + height: 56px; + flex-shrink: 0; +} + +.mcp-stats-ring-svg { + width: 56px; + height: 56px; + transform: rotate(-90deg); +} + +.mcp-stats-ring-track { + stroke: rgba(0, 0, 0, 0.08); +} + +.mcp-stats-ring-fill { + stroke: #14b8a6; + stroke-linecap: round; + transition: stroke-dashoffset 0.5s ease, stroke 0.2s ease; +} + +.mcp-stats-ring-fill.is-warning { stroke: #eab308; } +.mcp-stats-ring-fill.is-danger { stroke: #ef4444; } + +.mcp-stats-split { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr); + gap: 16px; + align-items: stretch; +} + +@media (max-width: 1024px) { + .mcp-stats-split { + grid-template-columns: 1fr; + } + + .mcp-stats-dist-chart-wrap { + width: min(72vw, 200px); + height: min(72vw, 200px); + } + + .mcp-stats-dist-legend--grid { + grid-template-columns: 1fr; + } + + .mcp-stats-tool-list { + justify-content: flex-start; + } +} + +.mcp-stats-split-left, +.mcp-stats-split-right { + min-width: 0; + display: flex; + flex-direction: column; +} + +.mcp-stats-insight-panel { + display: flex; + flex-direction: column; + flex: 1; + min-height: 100%; +} + +.mcp-stats-insight-block { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 14px; + padding: 14px 16px; +} + +.mcp-stats-dist-body--stacked { + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; +} + +.mcp-stats-dist-chart-stage { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 140px; + padding: 2px 0; +} + +.mcp-stats-dist-chart-wrap { + position: relative; + width: min(100%, 220px); + height: min(100%, 220px); + max-width: 220px; + max-height: 220px; + aspect-ratio: 1; + flex-shrink: 0; +} + +.mcp-stats-dist-donut { + position: relative; + width: 100%; + height: 100%; + border-radius: 50%; + box-shadow: + 0 2px 12px rgba(0, 0, 0, 0.08), + inset 0 0 0 1px rgba(255, 255, 255, 0.8); +} + +.mcp-stats-dist-donut-hole { + position: absolute; + inset: 22%; + border-radius: 50%; + background: var(--bg-primary); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + box-shadow: inset 0 0 0 1px var(--border-color); +} + +.mcp-stats-dist-donut-label { + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +.mcp-stats-dist-donut-value { + font-size: 1.625rem; + font-weight: 800; + color: var(--text-primary); + font-variant-numeric: tabular-nums; + line-height: 1; + letter-spacing: -0.04em; +} + +.mcp-stats-dist-donut-unit { + font-size: 0.875rem; + font-weight: 700; + color: var(--text-secondary); +} + +.mcp-stats-dist-legend--grid { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; + flex-shrink: 0; +} + +.mcp-stats-dist-legend-item { + display: grid; + grid-template-columns: 6px minmax(0, 1fr); + grid-template-rows: auto auto; + gap: 2px 8px; + align-items: center; + padding: 7px 8px; + border-radius: 8px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + transition: background 0.15s ease, border-color 0.15s ease; +} + +.mcp-stats-dist-legend-item:hover { + border-color: rgba(0, 102, 255, 0.22); + background: rgba(0, 102, 255, 0.03); +} + +.mcp-stats-dist-swatch { + grid-row: 1 / 3; + align-self: stretch; + width: 6px; + border-radius: 3px; + background: var(--swatch-color, #94a3b8); +} + +.mcp-stats-dist-legend-name { + grid-column: 2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-primary); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.625rem; + font-weight: 600; + line-height: 1.2; +} + +.mcp-stats-dist-legend-meta { + grid-column: 2; + display: flex; + align-items: center; + gap: 6px; + font-size: 0.625rem; + font-variant-numeric: tabular-nums; +} + +.mcp-stats-dist-legend-meta em { + font-style: normal; + font-weight: 700; + color: var(--text-primary); +} + +.mcp-stats-dist-legend-meta span { + color: var(--text-muted); + font-weight: 500; +} + +.mcp-stats-risk-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.mcp-stats-risk-btn { + width: 100%; + text-align: left; + padding: 8px 10px; + border-radius: 8px; + border: 1px solid rgba(239, 68, 68, 0.2); + background: rgba(254, 242, 242, 0.6); + color: #b91c1c; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; + font: inherit; +} + +.mcp-stats-risk-btn:hover { + background: rgba(254, 242, 242, 1); + border-color: rgba(239, 68, 68, 0.35); +} + +.mcp-stats-risk-empty, +.mcp-stats-selected-empty { + margin: 0; + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.5; +} + +.mcp-stats-selected-card { + display: flex; + flex-direction: column; + gap: 8px; +} + +.mcp-stats-selected-name { + font-size: 0.8125rem; + font-weight: 600; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + color: var(--text-primary); + word-break: break-all; +} + +.mcp-stats-selected-meta { + margin: 0; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.mcp-stats-tools-panel { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 14px; + padding: 16px 18px; + display: flex; + flex-direction: column; + gap: 14px; + flex: 1; + min-height: 100%; + box-sizing: border-box; +} + +.mcp-stats-tools-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.mcp-stats-tools-heading { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.mcp-stats-tools-title { + margin: 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary); +} + +.mcp-stats-tools-legend { + font-size: 0.6875rem; + color: var(--text-muted); + font-weight: 500; + line-height: 1.4; +} + +.mcp-stats-tools-hint { + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 500; + white-space: nowrap; + padding-top: 2px; +} + +.mcp-stats-tool-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + justify-content: space-between; +} + +.mcp-stats-tool-item { + margin: 0; + padding: 0; +} + +.mcp-stats-tool-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid transparent; + background: var(--bg-primary); + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; + text-align: left; + width: 100%; + box-sizing: border-box; + font: inherit; + color: var(--text-primary); +} + +.mcp-stats-tool-row:hover { + border-color: rgba(0, 102, 255, 0.22); + box-shadow: 0 2px 10px rgba(0, 102, 255, 0.1); + transform: translateX(2px); +} + +.mcp-stats-tool-row:hover .mcp-stats-tool-chevron { + color: var(--accent-color); + opacity: 1; +} + +.mcp-stats-tool-row:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.45); + outline-offset: 2px; +} + +.mcp-stats-tool-row.is-active { + border-color: rgba(0, 102, 255, 0.35); + background: rgba(0, 102, 255, 0.05); + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1); +} + +.mcp-stats-tool-row.is-active .mcp-stats-tool-chevron { + color: var(--accent-color); + opacity: 1; +} + +.mcp-stats-tool-rank { + flex-shrink: 0; + width: 24px; + height: 24px; + border-radius: 6px; + background: rgba(0, 102, 255, 0.08); + color: #2563eb; + font-size: 0.75rem; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; + font-variant-numeric: tabular-nums; +} + +.mcp-stats-tool-item:first-child .mcp-stats-tool-rank { + background: rgba(234, 179, 8, 0.15); + color: #a16207; +} + +.mcp-stats-tool-main { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.mcp-stats-tool-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-width: 0; +} + +.mcp-stats-tool-name { + font-size: 0.8125rem; + font-weight: 600; + color: #1a1a1a; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 4rem; + flex: 1 1 auto; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.mcp-stats-tool-metrics { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; + white-space: nowrap; + flex-shrink: 0; +} + +.mcp-stats-tool-count { + font-weight: 600; + color: var(--text-primary); +} + +.mcp-stats-tool-rate.is-success { color: #15803d; font-weight: 600; } +.mcp-stats-tool-rate.is-warning { color: #ca8a04; font-weight: 600; } +.mcp-stats-tool-rate.is-danger { color: #dc2626; font-weight: 600; } + +.mcp-stats-tool-fail-badge { + display: inline-flex; + padding: 1px 6px; + border-radius: 999px; + font-size: 0.625rem; + font-weight: 600; + background: rgba(239, 68, 68, 0.1); + color: #b91c1c; +} + +.mcp-stats-tool-chevron { + flex-shrink: 0; + color: var(--text-muted); + opacity: 0.45; + transition: color 0.15s ease, opacity 0.15s ease, transform 0.15s ease; +} + +.mcp-stats-tool-row:hover .mcp-stats-tool-chevron { + transform: translateX(2px); +} + +.mcp-stats-tool-bar-track { + width: 100%; + height: 8px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.06); + overflow: hidden; +} + +.mcp-stats-tool-bar-fill { + height: 100%; + min-width: 3px; + border-radius: 999px; + overflow: hidden; + transition: width 0.4s ease; +} + +.mcp-stats-tool-bar-inner { + display: flex; + width: 100%; + height: 100%; + gap: 1px; +} + +.mcp-stats-tool-bar-seg { + height: 100%; + min-width: 0; + transition: width 0.35s ease; +} + +.mcp-stats-tool-bar-seg--success { + background: linear-gradient(90deg, #22c55e, #16a34a); +} + +.mcp-stats-tool-bar-seg--fail { + background: linear-gradient(90deg, #f87171, #dc2626); +} + +.mcp-stats-clear-filter { + align-self: flex-start; + padding: 4px 12px; + font-size: 0.75rem; + border-radius: 999px; + border: 1px solid var(--border-color); + background: var(--bg-primary); + color: var(--accent-color); + cursor: pointer; + font-weight: 500; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.mcp-stats-clear-filter:hover { + background: rgba(0, 102, 255, 0.06); + border-color: rgba(0, 102, 255, 0.25); +} + +@media (prefers-reduced-motion: reduce) { + .mcp-stats-tool-row, + .mcp-stats-stacked-bar-seg, + .mcp-stats-tool-bar-seg, + .mcp-stats-tool-bar-fill, + .mcp-stats-tool-chevron, + .mcp-stats-ring-fill { + transition: none; + } + + .mcp-stats-tool-row:hover { + transform: none; + } +} + +/* 兼容 Skills 监控等复用 monitor-stats-grid 的页面 */ .monitor-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); @@ -11057,9 +11785,25 @@ header { .dashboard-stats { display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 16px; - margin-bottom: 24px; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 20px; +} + +@media (max-width: 1200px) { + .dashboard-stats { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .dashboard-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .stat-card-total { + grid-column: 1 / -1; + } } .stat-card { @@ -11107,8 +11851,118 @@ header { font-size: 2rem; font-weight: 700; color: var(--text-primary); + line-height: 1.1; + transition: color 0.2s ease; } +.stat-card.is-clickable { + cursor: pointer; + user-select: none; +} + +.stat-card.is-clickable:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.stat-card.is-clickable:hover:not(.is-active) { + border-color: rgba(59, 130, 246, 0.35); +} + +/* 选中态:浅底 + 外发光 ring,避免深色描边过硬 */ +.stat-card.is-clickable.is-active { + transform: none; + border-color: rgba(59, 130, 246, 0.35); + box-shadow: var(--shadow-sm), 0 0 0 2px rgba(59, 130, 246, 0.14); + background: rgba(59, 130, 246, 0.06); +} + +.stat-card.stat-card-total.is-active { + border-color: rgba(148, 163, 184, 0.5); + box-shadow: var(--shadow-sm), 0 0 0 2px rgba(148, 163, 184, 0.18); + background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%); +} + +.stat-card.stat-card-total.is-active .stat-label { + color: #475569; + font-weight: 600; +} + +.stat-card.stat-critical.is-active { + border-color: rgba(220, 38, 38, 0.35); + box-shadow: var(--shadow-sm), 0 0 0 2px rgba(220, 38, 38, 0.12); + background: rgba(254, 242, 242, 0.65); +} + +.stat-card.stat-high.is-active { + border-color: rgba(234, 88, 12, 0.35); + box-shadow: var(--shadow-sm), 0 0 0 2px rgba(234, 88, 12, 0.12); + background: rgba(255, 247, 237, 0.75); +} + +.stat-card.stat-medium.is-active { + border-color: rgba(202, 138, 4, 0.4); + box-shadow: var(--shadow-sm), 0 0 0 2px rgba(202, 138, 4, 0.12); + background: rgba(254, 252, 232, 0.75); +} + +.stat-card.stat-low.is-active { + border-color: rgba(15, 118, 110, 0.35); + box-shadow: var(--shadow-sm), 0 0 0 2px rgba(15, 118, 110, 0.12); + background: rgba(240, 253, 250, 0.75); +} + +.stat-card.stat-info.is-active { + border-color: rgba(37, 99, 235, 0.35); + box-shadow: var(--shadow-sm), 0 0 0 2px rgba(37, 99, 235, 0.12); + background: rgba(239, 246, 255, 0.75); +} + +.stat-pct { + font-size: 0.8125rem; + color: var(--text-secondary); + margin-top: 6px; + font-weight: 500; + letter-spacing: 0.02em; +} + +.stat-card .stat-value.is-zero { + color: var(--text-secondary); + opacity: 0.5; +} + +.stat-card.stat-critical .stat-value:not(.is-zero) { color: #dc2626; } +.stat-card.stat-high .stat-value:not(.is-zero) { color: #ea580c; } +.stat-card.stat-medium .stat-value:not(.is-zero) { color: #ca8a04; } +.stat-card.stat-low .stat-value:not(.is-zero) { color: #0f766e; } +.stat-card.stat-info .stat-value:not(.is-zero) { color: #2563eb; } + +.stat-stacked-bar { + display: flex; + height: 5px; + border-radius: 3px; + overflow: hidden; + margin-top: 12px; + background: rgba(0, 0, 0, 0.06); + gap: 1px; +} + +.stat-stacked-bar.is-empty { + opacity: 0.35; +} + +.stat-stacked-seg { + flex: 0 0 auto; + min-width: 0; + transition: flex-grow 0.35s ease, opacity 0.2s ease; +} + +.stat-stacked-seg.critical { background: #f87171; } +.stat-stacked-seg.high { background: #fb923c; } +.stat-stacked-seg.medium { background: #facc15; } +.stat-stacked-seg.low { background: #2dd4bf; } +.stat-stacked-seg.info { background: #60a5fa; } + /* WebShell 管理页面样式 - 美化版 */ .webshell-page-content { height: calc(100vh - 140px); @@ -15715,37 +16569,254 @@ header { } } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .vulnerability-controls { - margin-bottom: 24px; + margin-bottom: 16px; + padding: 10px 12px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 10px; + box-shadow: var(--shadow-sm); + transition: border-color 0.2s ease, box-shadow 0.2s ease; } -.vulnerability-filters { +.vulnerability-controls.is-filtered { + border-color: rgba(59, 130, 246, 0.4); + box-shadow: var(--shadow-sm), 0 0 0 1px rgba(59, 130, 246, 0.08); +} + +.vulnerability-filter-toolbar { + display: block; +} + +.vulnerability-filter-primary { display: flex; - gap: 16px; flex-wrap: wrap; - align-items: flex-end; + align-items: center; + gap: 8px; + min-width: 0; } -.vulnerability-filters label { +.vulnerability-filter-field { display: flex; - flex-direction: column; - gap: 4px; - font-size: 0.875rem; - color: var(--text-secondary); + align-items: center; + min-width: 0; + margin: 0; } -.vulnerability-filters input, -.vulnerability-filters select { +.vulnerability-filter-field--grow { + flex: 1 1 220px; + min-width: 160px; +} + +.vulnerability-filter-field--status { + flex: 0 0 auto; +} + +.vulnerability-filter-field input, +.vulnerability-filter-field select { + width: 100%; + min-width: 0; padding: 8px 12px; border: 1px solid var(--border-color); - border-radius: 6px; + border-radius: 8px; font-size: 0.875rem; - min-width: 150px; + background: var(--bg-primary); + color: var(--text-primary); } -/* Keep action buttons visually aligned in vulnerability filters */ -.vulnerability-filters .btn-primary { - border: 1px solid transparent; +.vulnerability-filter-field--status select { + min-width: 120px; + cursor: pointer; +} + +.vulnerability-filter-field input[type="search"] { + -webkit-appearance: none; + appearance: none; +} + +.vulnerability-filter-field input:focus, +.vulnerability-filter-field select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12); +} + +.vulnerability-filter-clear-btn { + flex-shrink: 0; + padding: 8px 12px; + border: none; + background: transparent; + color: var(--text-secondary); + font-size: 0.875rem; + cursor: pointer; + border-radius: 6px; + transition: color 0.15s ease, background 0.15s ease; +} + +.vulnerability-filter-clear-btn:hover { + color: #2563eb; + background: rgba(59, 130, 246, 0.06); +} + +/* tasks-filters 的 display:flex 会覆盖 [hidden],必须显式隐藏 */ +#vulnerability-advanced-filters[hidden] { + display: none !important; +} + +.vulnerability-filter-advanced-wrap { + margin-top: 6px; +} + +.vulnerability-filter-advanced-wrap.is-expanded { + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed var(--border-color); +} + +.vulnerability-filter-advanced { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 14px; +} + +.vulnerability-filter-advanced .vulnerability-filter-field { + flex-direction: column; + align-items: stretch; + gap: 4px; +} + +.vulnerability-filter-advanced .vulnerability-filter-field > span { + max-width: none; + font-size: 0.75rem; +} + +.vulnerability-filter-advanced .vulnerability-filter-field input { + width: 100%; +} + +@media (max-width: 768px) { + .vulnerability-filter-advanced { + grid-template-columns: 1fr; + } +} + +.vulnerability-filter-advanced-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + margin: 0 0 8px; + border: none; + background: transparent; + color: var(--text-secondary); + font-size: 0.8125rem; + cursor: pointer; + border-radius: 6px; + transition: color 0.15s ease, background 0.15s ease; +} + +.vulnerability-filter-advanced-toggle:hover { + color: var(--text-primary); + background: rgba(0, 0, 0, 0.04); +} + +/* 高级筛选生效数量:弱提示文字,避免实心蓝点过于抢眼 */ +.vulnerability-filter-advanced-badge { + display: inline; + margin-left: 2px; + font-size: 0.75rem; + font-weight: 600; + line-height: 1; + color: #64748b; + letter-spacing: 0.01em; +} + +.vulnerability-filter-advanced-badge[hidden] { + display: none !important; +} + +.vulnerability-filter-advanced-chevron { + display: inline-block; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid currentColor; + transition: transform 0.2s ease; +} + +.vulnerability-filter-advanced-toggle[aria-expanded="true"] .vulnerability-filter-advanced-chevron { + transform: rotate(180deg); +} + +.vulnerability-filter-chips { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border-color); +} + +.vulnerability-filter-chips-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.vulnerability-filter-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px 4px 10px; + font-size: 0.75rem; + line-height: 1.3; + color: #1e40af; + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.25); + border-radius: 999px; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.vulnerability-filter-chip:hover { + background: rgba(59, 130, 246, 0.16); + border-color: rgba(59, 130, 246, 0.4); +} + +.vulnerability-filter-chip-remove { + font-size: 0.875rem; + line-height: 1; + opacity: 0.7; +} + +@media (max-width: 768px) { + .vulnerability-filter-primary { + flex-direction: column; + align-items: stretch; + } + + .vulnerability-filter-field--grow, + .vulnerability-filter-field--status { + flex: 1 1 100%; + } + + .vulnerability-filter-clear-btn { + align-self: flex-end; + } } .vulnerabilities-list { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index f2a87da9..2121fff3 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1306,6 +1306,31 @@ "noCallsYet": "No calls yet", "unknownTool": "Unknown tool", "successFailedRate": "Success {{success}} / Failed {{failed}} · {{rate}}% success rate", + "topToolsTitle": "Top {{n}} tools by calls", + "barVolumeLegend": "Bar length: relative call volume; green/red: success vs failure share", + "clickToFilterTool": "Click a row to filter records below", + "toolRowAriaLabel": "{{name}}, {{total}} calls, {{rate}}% success rate. Click to filter records.", + "successRateAria": "Success rate {{rate}}%", + "filterByToolTitle": "Filtered by: {{tool}}", + "clearToolFilter": "Clear tool filter", + "successCount": "Success {{n}}", + "failedCount": "Failed {{n}}", + "rateHealthy": "Running smoothly", + "rateWarning": "Some failures detected", + "rateCritical": "High failure rate", + "statsSubtitle": "Refreshed {{time}} · {{count}} tools", + "distTitle": "Call distribution", + "distLegend": "Slice area shows share of all calls", + "distTotalCalls": "{{n}} total calls", + "distTop6Share": "Top {{n}} share of all calls", + "distOthers": "Other tools", + "distCallsUnit": "{{n}} calls", + "riskTitle": "Failure alerts", + "riskNone": "No recent failures", + "riskItem": "{{name}}: {{failed}} / {{total}} failed", + "selectedToolTitle": "Active filter", + "selectedToolEmpty": "Click a tool on the left to filter records below", + "selectedToolStats": "{{total}} calls · {{success}} ok · {{failed}} failed · {{rate}}% success", "columnTool": "Tool", "columnStatus": "Status", "columnStartTime": "Start time", @@ -1486,6 +1511,11 @@ }, "vulnerabilityPage": { "statTotal": "Total", + "statClickAll": "View all (clear severity filter)", + "statClickFilter": "Click to filter by this severity; click again to clear", + "advancedFilters": "Advanced filters", + "activeFilters": "Active filters", + "chipRemove": "Remove", "filter": "Filter", "clear": "Clear", "vulnId": "Vuln ID", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 6a6568ac..dff884aa 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1295,6 +1295,31 @@ "noCallsYet": "暂无调用", "unknownTool": "未知工具", "successFailedRate": "成功 {{success}} / 失败 {{failed}} · 成功率 {{rate}}%", + "topToolsTitle": "工具调用 Top {{n}}", + "barVolumeLegend": "条长表示相对调用量,条内绿/红为成功/失败占比", + "clickToFilterTool": "点击行筛选下方执行记录", + "toolRowAriaLabel": "{{name}},{{total}} 次调用,成功率 {{rate}}%,点击查看执行记录", + "successRateAria": "成功率 {{rate}}%", + "filterByToolTitle": "筛选工具:{{tool}}", + "clearToolFilter": "清除工具筛选", + "successCount": "成功 {{n}}", + "failedCount": "失败 {{n}}", + "rateHealthy": "运行平稳", + "rateWarning": "存在失败调用", + "rateCritical": "失败率偏高", + "statsSubtitle": "最后刷新 {{time}} · 共 {{count}} 个工具", + "distTitle": "调用分布", + "distLegend": "扇区面积为占全部调用比例", + "distTotalCalls": "共 {{n}} 次调用", + "distTop6Share": "Top {{n}} 占全部调用", + "distOthers": "其他工具", + "distCallsUnit": "{{n}} 次", + "riskTitle": "失败提醒", + "riskNone": "近期无失败调用", + "riskItem": "{{name}}:失败 {{failed}} / {{total}} 次", + "selectedToolTitle": "当前筛选", + "selectedToolEmpty": "点击左侧工具行,可筛选下方执行记录", + "selectedToolStats": "调用 {{total}} 次 · 成功 {{success}} · 失败 {{failed}} · 成功率 {{rate}}%", "columnTool": "工具", "columnStatus": "状态", "columnStartTime": "开始时间", @@ -1475,6 +1500,11 @@ }, "vulnerabilityPage": { "statTotal": "总漏洞数", + "statClickAll": "查看全部(清除严重度筛选)", + "statClickFilter": "点击按此严重度筛选;再次点击清除", + "advancedFilters": "高级筛选", + "activeFilters": "已选条件", + "chipRemove": "移除", "filter": "筛选", "clear": "清除", "vulnId": "漏洞ID", diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index dab04583..2c0bf3cd 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -2982,6 +2982,9 @@ async function applyMonitorFilters() { const toolFilter = document.getElementById('monitor-tool-filter'); const status = statusFilter ? statusFilter.value : 'all'; const tool = toolFilter ? (toolFilter.value.trim() || 'all') : 'all'; + if (toolFilter) { + toolFilter.classList.toggle('is-filter-active', tool !== 'all'); + } // 当筛选条件改变时,从后端重新获取数据 await refreshMonitorPanelWithFilter(status, tool); } @@ -3045,20 +3048,244 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = } +const MCP_STATS_TOP_N = 6; + +function mcpMonitorT(key, params) { + if (typeof window.t !== 'function') return ''; + return window.t('mcpMonitor.' + key, { + ...(params || {}), + interpolation: { escapeValue: false }, + }); +} + +function normalizeMonitorStatsEntries(statsMap) { + if (!statsMap || typeof statsMap !== 'object') return []; + return Object.entries(statsMap).map(([key, item]) => { + const stat = item && typeof item === 'object' ? { ...item } : {}; + if (!stat.toolName) stat.toolName = key; + return stat; + }); +} + +const MCP_STATS_TOOL_CHEVRON = ''; + +function getMcpStatsRateTone(rateNum) { + if (rateNum >= 95) return 'is-success'; + if (rateNum >= 80) return 'is-warning'; + return 'is-danger'; +} + +function getMcpStatsRingStrokeClass(rateNum) { + if (rateNum >= 95) return ''; + if (rateNum >= 80) return 'is-warning'; + return 'is-danger'; +} + +function renderMcpStatsSuccessRing(percent) { + const p = Math.min(100, Math.max(0, parseFloat(percent) || 0)); + const r = 15.9155; + const circumference = 2 * Math.PI * r; + const offset = circumference - (p / 100) * circumference; + const strokeClass = getMcpStatsRingStrokeClass(p); + return ``; +} + +function renderMcpStatsToolVolumeBar(total, success, failed, maxTotal) { + const volumePct = maxTotal > 0 && total > 0 ? (total / maxTotal) * 100 : 0; + const successPct = total > 0 ? (success / total) * 100 : 0; + const failPct = total > 0 ? (failed / total) * 100 : 0; + const legend = mcpMonitorT('barVolumeLegend') || '条长表示相对调用量'; + const volumeTitle = `${total} / ${maxTotal}`; + return `
+
+
+ + +
+
+
`; +} + +function getMcpToolRateClass(rateNum) { + if (rateNum >= 95) return 'is-success'; + if (rateNum >= 80) return 'is-warning'; + return 'is-danger'; +} + +const MCP_STATS_DIST_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#14b8a6', '#ec4899']; + +function renderMcpStatsInsightPanel(topTools, totals) { + const distTitle = mcpMonitorT('distTitle') || '调用分布'; + const distLegend = mcpMonitorT('distLegend') || '扇区面积为占全部调用比例'; + const top6ShareLabel = mcpMonitorT('distTop6Share', { n: MCP_STATS_TOP_N }) || `Top ${MCP_STATS_TOP_N} 占全部调用`; + const othersLabel = mcpMonitorT('distOthers') || '其他工具'; + const callsUnit = (n) => mcpMonitorT('distCallsUnit', { n }) || `${n} 次`; + + const top6Total = topTools.reduce((s, t) => s + (t.totalCalls || 0), 0); + const top6SharePct = totals.total > 0 ? ((top6Total / totals.total) * 100).toFixed(1) : '0.0'; + const otherCalls = Math.max(0, totals.total - top6Total); + + let acc = 0; + const segments = []; + topTools.forEach((tool, i) => { + const calls = tool.totalCalls || 0; + if (calls <= 0 || totals.total <= 0) return; + const pct = (calls / totals.total) * 100; + segments.push({ + color: MCP_STATS_DIST_COLORS[i % MCP_STATS_DIST_COLORS.length], + start: acc, + end: acc + pct, + name: tool.toolName || '', + calls, + pct: pct.toFixed(1), + }); + acc += pct; + }); + if (otherCalls > 0 && totals.total > 0) { + const pct = (otherCalls / totals.total) * 100; + segments.push({ + color: '#cbd5e1', + start: acc, + end: acc + pct, + name: othersLabel, + calls: otherCalls, + pct: pct.toFixed(1), + }); + } + const conic = segments.length > 0 + ? segments.map(s => `${s.color} ${s.start.toFixed(2)}% ${s.end.toFixed(2)}%`).join(', ') + : '#e2e8f0 0% 100%'; + + const legendHtml = segments.map(s => ` +
  • + + ${escapeHtml(s.name)} + ${s.pct}%${escapeHtml(callsUnit(s.calls))} +
  • + `).join(''); + + const centerLabel = `Top ${MCP_STATS_TOP_N}`; + const distHint = totals.total > 0 + ? (mcpMonitorT('distTotalCalls', { n: totals.total }) || `共 ${totals.total} 次调用`) + : ''; + + return ` +
    +
    +
    +

    ${escapeHtml(distTitle)}

    + ${escapeHtml(distLegend)} +
    + ${distHint ? `${escapeHtml(distHint)}` : ''} +
    +
    +
    +
    + +
    + ${centerLabel} + ${top6SharePct}% +
    +
    +
    + +
    +
    + `; +} + + +function renderMcpStatsStackedBar(success, failed) { + const total = success + failed; + if (total <= 0) { + return ''; + } + const successFlex = Math.max(0, (success / total) * 100); + const failFlex = Math.max(0, (failed / total) * 100); + return ``; +} + +function updateMonitorStatsSubtitle(lastFetchedAt, toolCount) { + const subtitle = document.getElementById('monitor-stats-subtitle'); + if (!subtitle) return; + const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US'; + const timeText = lastFetchedAt + ? (lastFetchedAt.toLocaleString ? lastFetchedAt.toLocaleString(locale) : String(lastFetchedAt)) + : '—'; + const text = mcpMonitorT('statsSubtitle', { time: timeText, count: toolCount }) + || `最后刷新 ${timeText} · 共 ${toolCount} 个工具`; + subtitle.textContent = text; + subtitle.hidden = false; +} + +function filterMonitorByTool(toolName) { + const toolFilter = document.getElementById('monitor-tool-filter'); + if (!toolFilter || !toolName) return; + toolFilter.value = toolName; + toolFilter.classList.add('is-filter-active'); + applyMonitorFilters(); + const execSection = document.querySelector('.monitor-executions'); + if (execSection && typeof execSection.scrollIntoView === 'function') { + execSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } +} + +function clearMonitorToolFilter() { + const toolFilter = document.getElementById('monitor-tool-filter'); + if (!toolFilter) return; + toolFilter.value = ''; + toolFilter.classList.remove('is-filter-active'); + applyMonitorFilters(); +} + +let monitorStatsPanelEventsBound = false; + +function bindMonitorStatsPanelEvents() { + if (monitorStatsPanelEventsBound) return; + const root = document.getElementById('monitor-stats'); + if (!root) return; + root.addEventListener('click', function (e) { + const clearBtn = e.target.closest('.mcp-stats-clear-filter'); + if (clearBtn) { + e.preventDefault(); + clearMonitorToolFilter(); + return; + } +const row = e.target.closest('.mcp-stats-tool-row'); + if (!row) return; + const tool = row.getAttribute('data-tool-name'); + if (tool) { + e.preventDefault(); + filterMonitorByTool(tool); + } + }); + monitorStatsPanelEventsBound = true; +} + function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { const container = document.getElementById('monitor-stats'); if (!container) { return; } - const entries = Object.values(statsMap); + const entries = normalizeMonitorStatsEntries(statsMap); if (entries.length === 0) { - const noStats = typeof window.t === 'function' ? window.t('mcpMonitor.noStatsData') : '暂无统计数据'; + const noStats = mcpMonitorT('noStatsData') || '暂无统计数据'; container.innerHTML = '
    ' + escapeHtml(noStats) + '
    '; + const subtitle = document.getElementById('monitor-stats-subtitle'); + if (subtitle) subtitle.hidden = true; return; } - // 计算总体汇总 const totals = entries.reduce( (acc, item) => { acc.total += item.totalCalls || 0; @@ -3073,59 +3300,154 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { { total: 0, success: 0, failed: 0, lastCallTime: null } ); - const successRate = totals.total > 0 ? ((totals.success / totals.total) * 100).toFixed(1) : '0.0'; - const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined; - const lastUpdatedText = lastFetchedAt ? (lastFetchedAt.toLocaleString ? lastFetchedAt.toLocaleString(locale || 'en-US') : String(lastFetchedAt)) : 'N/A'; - const noCallsYet = typeof window.t === 'function' ? window.t('mcpMonitor.noCallsYet') : '暂无调用'; - const lastCallText = totals.lastCallTime ? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale || 'en-US') : String(totals.lastCallTime)) : noCallsYet; - const totalCallsLabel = typeof window.t === 'function' ? window.t('mcpMonitor.totalCalls') : '总调用次数'; - const successFailedLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successFailed', { success: totals.success, failed: totals.failed }) : `成功 ${totals.success} / 失败 ${totals.failed}`; - const successRateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successRate') : '成功率'; - const statsFromAll = typeof window.t === 'function' ? window.t('mcpMonitor.statsFromAllTools') : '统计自全部工具调用'; - const lastCallLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastCall') : '最近一次调用'; - const lastRefreshLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastRefreshTime') : '最后刷新时间'; + const successRateNum = totals.total > 0 ? (totals.success / totals.total) * 100 : 0; + const successRate = successRateNum.toFixed(1); + const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US'; + const noCallsYet = mcpMonitorT('noCallsYet') || '暂无调用'; + const lastCallText = totals.lastCallTime + ? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale) : String(totals.lastCallTime)) + : noCallsYet; - let html = ` -
    -

    ${escapeHtml(totalCallsLabel)}

    -
    ${totals.total}
    -
    ${escapeHtml(successFailedLabel)}
    -
    -
    -

    ${escapeHtml(successRateLabel)}

    -
    ${successRate}%
    -
    ${escapeHtml(statsFromAll)}
    -
    -
    -

    ${escapeHtml(lastCallLabel)}

    -
    ${escapeHtml(lastCallText)}
    -
    ${escapeHtml(lastRefreshLabel)}:${escapeHtml(lastUpdatedText)}
    -
    - `; + const totalCallsLabel = mcpMonitorT('totalCalls') || '总调用次数'; + const successRateLabel = mcpMonitorT('successRate') || '成功率'; + const lastCallLabel = mcpMonitorT('lastCall') || '最近一次调用'; + const statsFromAll = mcpMonitorT('statsFromAllTools') || '统计自全部工具调用'; + const successPill = mcpMonitorT('successCount', { n: totals.success }) || `成功 ${totals.success}`; + const failedPill = mcpMonitorT('failedCount', { n: totals.failed }) || `失败 ${totals.failed}`; + const rateTone = getMcpStatsRateTone(successRateNum); + let rateSubText = mcpMonitorT('rateHealthy') || '运行平稳'; + if (successRateNum < 80) rateSubText = mcpMonitorT('rateCritical') || '失败率偏高'; + else if (successRateNum < 95) rateSubText = mcpMonitorT('rateWarning') || '存在失败调用'; + + const toolFilterEl = document.getElementById('monitor-tool-filter'); + const activeToolFilter = toolFilterEl ? toolFilterEl.value.trim() : ''; - // 显示最多前4个工具的统计(过滤掉 totalCalls 为 0 的工具) const topTools = entries .filter(tool => (tool.totalCalls || 0) > 0) .slice() .sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0)) - .slice(0, 4); + .slice(0, MCP_STATS_TOP_N); - const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具'; - topTools.forEach(tool => { - const toolSuccessRate = tool.totalCalls > 0 ? ((tool.successCalls || 0) / tool.totalCalls * 100).toFixed(1) : '0.0'; - const toolMeta = typeof window.t === 'function' ? window.t('mcpMonitor.successFailedRate', { success: tool.successCalls || 0, failed: tool.failedCalls || 0, rate: toolSuccessRate }) : `成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%`; - html += ` -
    -

    ${escapeHtml(tool.toolName || unknownToolLabel)}

    -
    ${tool.totalCalls || 0}
    -
    - ${escapeHtml(toolMeta)} -
    -
    + const maxToolCalls = topTools.length > 0 ? (topTools[0].totalCalls || 0) : 0; + const unknownToolLabel = mcpMonitorT('unknownTool') || '未知工具'; + const topToolsTitle = mcpMonitorT('topToolsTitle', { n: MCP_STATS_TOP_N }) || `工具调用 Top ${MCP_STATS_TOP_N}`; + const toolsHint = mcpMonitorT('clickToFilterTool') || '点击行筛选下方执行记录'; + const barLegend = mcpMonitorT('barVolumeLegend') || '条长表示相对调用量'; + const successRateAria = mcpMonitorT('successRateAria', { rate: successRate }) || `成功率 ${successRate}%`; + + const iconCalls = ''; + const iconRate = ''; + const iconTime = ''; + + let toolRowsHtml = ''; + topTools.forEach((tool, index) => { + const name = tool.toolName || unknownToolLabel; + const total = tool.totalCalls || 0; + const success = tool.successCalls || 0; + const failed = tool.failedCalls || 0; + const toolRateNum = total > 0 ? (success / total) * 100 : 0; + const toolRate = toolRateNum.toFixed(1); + const isActive = activeToolFilter && activeToolFilter === name; + const rowAria = mcpMonitorT('toolRowAriaLabel', { name, total, rate: toolRate }) + || `${name},${total} 次调用,成功率 ${toolRate}%`; + const rateClass = getMcpToolRateClass(toolRateNum); + toolRowsHtml += ` +
  • + +
  • `; }); - container.innerHTML = `
    ${html}
    `; + const clearFilterBtn = activeToolFilter + ? `` + : ''; + + const html = ` +
    +
    +
    +
    + ${escapeHtml(totalCallsLabel)} + +
    +
    ${totals.total}
    + ${renderMcpStatsStackedBar(totals.success, totals.failed)} +
    + ${escapeHtml(successPill)} + ${escapeHtml(failedPill)} +
    +
    +
    +
    + ${escapeHtml(successRateLabel)} + +
    + +
    + ${escapeHtml(rateSubText)} + ${escapeHtml(statsFromAll)} +
    +
    +
    +
    + ${escapeHtml(lastCallLabel)} + +
    +
    ${escapeHtml(lastCallText)}
    +
    +
    + ${topTools.length > 0 ? ` +
    +
    +
    +
    +
    +

    ${escapeHtml(topToolsTitle)}

    + ${escapeHtml(barLegend)} +
    + ${escapeHtml(toolsHint)} +
    +
      ${toolRowsHtml}
    + ${clearFilterBtn} +
    +
    +
    + ${renderMcpStatsInsightPanel(topTools, totals)} +
    +
    + ` : ''} +
    + `; + + container.innerHTML = html; + bindMonitorStatsPanelEvents(); + if (toolFilterEl && activeToolFilter) { + toolFilterEl.classList.add('is-filter-active'); + } else if (toolFilterEl) { + toolFilterEl.classList.remove('is-filter-active'); + } + updateMonitorStatsSubtitle(lastFetchedAt, entries.length); } function renderMonitorExecutions(executions = [], statusFilter = 'all') { @@ -3622,4 +3944,14 @@ document.addEventListener('languagechange', function () { updateBatchActionsState(); loadActiveTasks(); refreshProgressAndTimelineI18n(); + if (monitorState.stats && Object.keys(monitorState.stats).length > 0) { + renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt); + } }); + +document.addEventListener('DOMContentLoaded', function () { + bindMonitorStatsPanelEvents(); +}); + +window.filterMonitorByTool = filterMonitorByTool; +window.clearMonitorToolFilter = clearMonitorToolFilter; diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js index 82711ee3..42c171ea 100644 --- a/web/static/js/vulnerability.js +++ b/web/static/js/vulnerability.js @@ -61,6 +61,24 @@ let vulnerabilityPagination = { totalPages: 1 }; +const VULN_STAT_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info']; +let vulnerabilityStatCardsBound = false; +let vulnerabilityFilterPanelBound = false; +let vulnerabilityFilterOptionsCache = null; +const VULNERABILITY_ADVANCED_OPEN_KEY = 'vulnerabilityAdvancedFiltersOpen'; +const VULNERABILITY_DATALIST_MAX = 8; +const VULNERABILITY_DATALIST_MIN_QUERY = 2; + +const VULN_FILTER_CHIP_FIELDS = [ + { key: 'id', labelKey: 'vulnerabilityPage.vulnId' }, + { key: 'status', labelKey: null, format: 'status' }, + { key: 'severity', labelKey: null, format: 'severity' }, + { key: 'conversation_id', labelKey: 'vulnerabilityPage.conversationId' }, + { key: 'task_id', labelKey: 'vulnerabilityPage.taskOrQueueId' }, + { key: 'conversation_tag', labelKey: 'vulnerabilityPage.conversationTag' }, + { key: 'task_tag', labelKey: 'vulnerabilityPage.taskTag' } +]; + // 从地址栏 #vulnerabilities?conversation_id= / ?task_id= / ?id= 同步筛选(通知/对话菜单/任务管理联动) function syncVulnerabilityFiltersFromLocationHash() { const hash = window.location.hash.slice(1); @@ -74,23 +92,31 @@ function syncVulnerabilityFiltersFromLocationHash() { const tid = (params.get('task_id') || '').trim(); const sev = (params.get('severity') || '').trim(); const st = (params.get('status') || '').trim(); - if (!vid && !cid && !tid && !sev && !st) { + const convTag = (params.get('conversation_tag') || '').trim(); + const taskTag = (params.get('task_tag') || '').trim(); + if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag) { return; } vulnerabilityFilters.id = ''; vulnerabilityFilters.conversation_id = ''; vulnerabilityFilters.task_id = ''; + vulnerabilityFilters.conversation_tag = ''; + vulnerabilityFilters.task_tag = ''; vulnerabilityFilters.severity = ''; vulnerabilityFilters.status = ''; const idEl = document.getElementById('vulnerability-id-filter'); const convEl = document.getElementById('vulnerability-conversation-filter'); const taskEl = document.getElementById('vulnerability-task-filter'); + const convTagEl = document.getElementById('vulnerability-conversation-tag-filter'); + const taskTagEl = document.getElementById('vulnerability-task-tag-filter'); const sevEl = document.getElementById('vulnerability-severity-filter'); const stEl = document.getElementById('vulnerability-status-filter'); if (idEl) idEl.value = ''; if (convEl) convEl.value = ''; if (taskEl) taskEl.value = ''; + if (convTagEl) convTagEl.value = ''; + if (taskTagEl) taskTagEl.value = ''; if (sevEl) sevEl.value = ''; if (stEl) stEl.value = ''; @@ -106,6 +132,14 @@ function syncVulnerabilityFiltersFromLocationHash() { vulnerabilityFilters.task_id = tid; if (taskEl) taskEl.value = tid; } + if (convTag) { + vulnerabilityFilters.conversation_tag = convTag; + if (convTagEl) convTagEl.value = convTag; + } + if (taskTag) { + vulnerabilityFilters.task_tag = taskTag; + if (taskTagEl) taskTagEl.value = taskTag; + } if (sev) { vulnerabilityFilters.severity = sev; if (sevEl) sevEl.value = sev; @@ -115,17 +149,457 @@ function syncVulnerabilityFiltersFromLocationHash() { if (stEl) stEl.value = st; } vulnerabilityPagination.currentPage = 1; + if (hasVulnerabilityAdvancedFiltersActive()) { + setVulnerabilityAdvancedFiltersOpen(true, false); + } + syncVulnerabilityStatCardActiveState(); + updateVulnerabilityFilterPanelState(); + renderVulnerabilityFilterChips(); } // 初始化漏洞管理页面 function initVulnerabilityPage() { // 从localStorage加载每页条数设置 vulnerabilityPagination.pageSize = getVulnerabilityPageSize(); + initVulnerabilityStatCards(); + initVulnerabilityFilterPanel(); syncVulnerabilityFiltersFromLocationHash(); + updateVulnerabilityFilterPanelState(); + renderVulnerabilityFilterChips(); + loadVulnerabilityFilterOptions(); loadVulnerabilityStats(); loadVulnerabilities(); } +function initVulnerabilityStatCards() { + if (vulnerabilityStatCardsBound) { + syncVulnerabilityStatCardActiveState(); + return; + } + const root = document.getElementById('vulnerability-stat-cards'); + if (!root) return; + vulnerabilityStatCardsBound = true; + root.addEventListener('click', onVulnerabilityStatCardClick); + root.addEventListener('keydown', onVulnerabilityStatCardKeydown); +} + +function onVulnerabilityStatCardClick(ev) { + const totalCard = ev.target.closest('.stat-card.stat-card-total'); + if (totalCard) { + applyVulnerabilitySeverityFilter(''); + return; + } + const card = ev.target.closest('.stat-card.is-clickable[data-severity]'); + if (!card) return; + const sev = card.getAttribute('data-severity'); + if (!sev) return; + const sevEl = document.getElementById('vulnerability-severity-filter'); + const current = sevEl ? sevEl.value : vulnerabilityFilters.severity; + applyVulnerabilitySeverityFilter(current === sev ? '' : sev); +} + +function onVulnerabilityStatCardKeydown(ev) { + if (ev.key !== 'Enter' && ev.key !== ' ') return; + const card = ev.target.closest('.stat-card.is-clickable'); + if (!card || !card.contains(ev.target)) return; + ev.preventDefault(); + card.click(); +} + +function applyVulnerabilitySeverityFilter(severity) { + const sevEl = document.getElementById('vulnerability-severity-filter'); + if (sevEl) sevEl.value = severity || ''; + applyVulnerabilityFilters(); +} + +function readVulnerabilityFiltersFromForm() { + vulnerabilityFilters.id = (document.getElementById('vulnerability-id-filter')?.value || '').trim(); + vulnerabilityFilters.conversation_id = (document.getElementById('vulnerability-conversation-filter')?.value || '').trim(); + vulnerabilityFilters.task_id = (document.getElementById('vulnerability-task-filter')?.value || '').trim(); + vulnerabilityFilters.conversation_tag = (document.getElementById('vulnerability-conversation-tag-filter')?.value || '').trim(); + vulnerabilityFilters.task_tag = (document.getElementById('vulnerability-task-tag-filter')?.value || '').trim(); + vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter')?.value || ''; + vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter')?.value || ''; + return vulnerabilityFilters; +} + +function hasVulnerabilityAdvancedFiltersActive() { + const f = vulnerabilityFilters; + return Boolean(f.conversation_id || f.task_id || f.conversation_tag || f.task_tag); +} + +function hasAnyVulnerabilityFilterActive() { + const f = vulnerabilityFilters; + return Boolean( + f.id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status + ); +} + +function applyVulnerabilityFilters() { + readVulnerabilityFiltersFromForm(); + vulnerabilityPagination.currentPage = 1; + syncVulnerabilityStatCardActiveState(); + updateVulnerabilityLocationHashFromFilters(); + updateVulnerabilityFilterPanelState(); + renderVulnerabilityFilterChips(); + loadVulnerabilityStats(); + loadVulnerabilities(); +} + +function updateVulnerabilityLocationHashFromFilters() { + const hash = window.location.hash.slice(1); + const hashParts = hash.split('?'); + if (hashParts[0] !== 'vulnerabilities') return; + const params = new URLSearchParams(hashParts.length >= 2 ? hashParts.slice(1).join('?') : ''); + const f = vulnerabilityFilters; + const pairs = [ + ['id', f.id], + ['conversation_id', f.conversation_id], + ['task_id', f.task_id], + ['conversation_tag', f.conversation_tag], + ['task_tag', f.task_tag], + ['severity', f.severity], + ['status', f.status] + ]; + pairs.forEach(function (pair) { + if (pair[1]) { + params.set(pair[0], pair[1]); + } else { + params.delete(pair[0]); + } + }); + const qs = params.toString(); + const newHash = qs ? 'vulnerabilities?' + qs : 'vulnerabilities'; + if (window.location.hash.slice(1) === newHash) return; + const newFull = '#' + newHash; + if (typeof history.replaceState === 'function') { + history.replaceState(null, '', window.location.pathname + window.location.search + newFull); + } else { + window.location.hash = newHash; + } +} + +function toggleVulnerabilityAdvancedFilters(ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + const toggleBtn = document.getElementById('vulnerability-advanced-toggle'); + if (!toggleBtn) return; + const expanded = toggleBtn.getAttribute('aria-expanded') === 'true'; + setVulnerabilityAdvancedFiltersOpen(!expanded, true); +} +window.toggleVulnerabilityAdvancedFilters = toggleVulnerabilityAdvancedFilters; + +function initVulnerabilityFilterPanel() { + const panel = document.getElementById('vulnerability-filter-panel'); + if (!panel) return; + + if (vulnerabilityFilterPanelBound) { + updateVulnerabilityFilterPanelState(); + return; + } + vulnerabilityFilterPanelBound = true; + + let savedOpen = false; + try { + savedOpen = localStorage.getItem(VULNERABILITY_ADVANCED_OPEN_KEY) === 'true'; + } catch (e) { /* ignore */ } + setVulnerabilityAdvancedFiltersOpen(savedOpen, false); + + const stEl = document.getElementById('vulnerability-status-filter'); + if (stEl) stEl.addEventListener('change', applyVulnerabilityFilters); + + const textIds = [ + 'vulnerability-id-filter', + 'vulnerability-conversation-filter', + 'vulnerability-task-filter', + 'vulnerability-conversation-tag-filter', + 'vulnerability-task-tag-filter' + ]; + textIds.forEach(function (id) { + const el = document.getElementById(id); + if (!el) return; + el.addEventListener('keydown', function (ev) { + if (ev.key === 'Enter') { + ev.preventDefault(); + applyVulnerabilityFilters(); + } + }); + }); + + bindVulnerabilityFilterTypeaheads(); +} + +function setVulnerabilityAdvancedFiltersOpen(open, persist) { + const toggleBtn = document.getElementById('vulnerability-advanced-toggle'); + const advanced = document.getElementById('vulnerability-advanced-filters'); + const wrap = document.querySelector('#vulnerability-filter-panel .vulnerability-filter-advanced-wrap'); + if (!toggleBtn || !advanced) return; + toggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false'); + advanced.hidden = !open; + advanced.classList.toggle('is-open', open); + if (wrap) wrap.classList.toggle('is-expanded', open); + if (persist) { + try { + localStorage.setItem(VULNERABILITY_ADVANCED_OPEN_KEY, open ? 'true' : 'false'); + } catch (e) { /* ignore */ } + } +} + +function countVulnerabilityAdvancedFiltersActive() { + const f = vulnerabilityFilters; + let n = 0; + if (f.conversation_id) n++; + if (f.task_id) n++; + if (f.conversation_tag) n++; + if (f.task_tag) n++; + return n; +} + +function updateVulnerabilityAdvancedBadge() { + const badge = document.getElementById('vulnerability-advanced-badge'); + if (!badge) return; + readVulnerabilityFiltersFromForm(); + const n = countVulnerabilityAdvancedFiltersActive(); + if (n > 0) { + badge.hidden = false; + badge.textContent = '(' + n + ')'; + badge.setAttribute('aria-label', String(n)); + } else { + badge.hidden = true; + badge.textContent = ''; + badge.removeAttribute('aria-label'); + } +} + +function updateVulnerabilityFilterPanelState() { + const panel = document.getElementById('vulnerability-filter-panel'); + if (!panel) return; + readVulnerabilityFiltersFromForm(); + panel.classList.toggle('is-filtered', hasAnyVulnerabilityFilterActive()); + updateVulnerabilityAdvancedBadge(); +} + +function formatVulnerabilityFilterChipValue(key, value) { + if (key === 'severity') return vulnSeverityLabel(value); + if (key === 'status') return vulnStatusLabel(value); + return value; +} + +function renderVulnerabilityFilterChips() { + const wrap = document.getElementById('vulnerability-filter-chips'); + const list = document.getElementById('vulnerability-filter-chips-list'); + if (!wrap || !list) return; + + readVulnerabilityFiltersFromForm(); + const chips = []; + VULN_FILTER_CHIP_FIELDS.forEach(function (field) { + const val = vulnerabilityFilters[field.key]; + if (!val) return; + const label = field.labelKey ? vulnT(field.labelKey) : ''; + const displayVal = formatVulnerabilityFilterChipValue(field.key, val); + const text = label ? label + ': ' + displayVal : displayVal; + chips.push({ key: field.key, text: text }); + }); + + if (!chips.length) { + wrap.hidden = true; + list.innerHTML = ''; + return; + } + + wrap.hidden = false; + const removeLabel = vulnT('vulnerabilityPage.chipRemove'); + list.innerHTML = chips.map(function (chip) { + return ( + '' + ); + }).join(''); + + list.querySelectorAll('.vulnerability-filter-chip').forEach(function (btn) { + btn.addEventListener('click', function () { + const key = btn.getAttribute('data-filter-key'); + if (key) removeVulnerabilityFilterByKey(key); + }); + }); +} + +function removeVulnerabilityFilterByKey(key) { + const map = { + id: 'vulnerability-id-filter', + conversation_id: 'vulnerability-conversation-filter', + task_id: 'vulnerability-task-filter', + conversation_tag: 'vulnerability-conversation-tag-filter', + task_tag: 'vulnerability-task-tag-filter', + severity: 'vulnerability-severity-filter', + status: 'vulnerability-status-filter' + }; + const elId = map[key]; + if (elId) { + const el = document.getElementById(elId); + if (el) el.value = ''; + } + if (Object.prototype.hasOwnProperty.call(vulnerabilityFilters, key)) { + vulnerabilityFilters[key] = ''; + } + applyVulnerabilityFilters(); +} + +async function loadVulnerabilityFilterOptions() { + if (typeof apiFetch === 'undefined') return; + try { + const response = await apiFetch('/api/vulnerabilities/filter-options'); + if (!response.ok) return; + vulnerabilityFilterOptionsCache = await response.json(); + populateVulnerabilityDatalist( + 'vulnerability-conversation-tag-suggestions', + vulnerabilityFilterOptionsCache.conversation_tags, + { max: 20 } + ); + populateVulnerabilityDatalist( + 'vulnerability-task-tag-suggestions', + vulnerabilityFilterOptionsCache.task_tags, + { max: 20 } + ); + clearVulnerabilityDatalist('vulnerability-conversation-suggestions'); + clearVulnerabilityDatalist('vulnerability-task-suggestions'); + } catch (e) { + console.warn('加载漏洞筛选建议失败', e); + } +} + +function clearVulnerabilityDatalist(listId) { + const list = document.getElementById(listId); + if (list) list.innerHTML = ''; +} + +function populateVulnerabilityDatalist(listId, values, opts) { + const list = document.getElementById(listId); + if (!list || !Array.isArray(values)) return; + const max = (opts && opts.max) || VULNERABILITY_DATALIST_MAX; + const seen = new Set(); + const unique = []; + values.forEach(function (v) { + const s = String(v || '').trim(); + if (!s || seen.has(s)) return; + seen.add(s); + unique.push(s); + if (unique.length >= max) return; + }); + list.innerHTML = unique.slice(0, max).map(function (v) { + return ''; + }).join(''); +} + +function filterVulnerabilitySuggestionPool(pool, query) { + if (!Array.isArray(pool) || !query) return []; + const q = query.toLowerCase(); + const out = []; + for (let i = 0; i < pool.length && out.length < VULNERABILITY_DATALIST_MAX; i++) { + const s = String(pool[i] || '').trim(); + if (s && s.toLowerCase().indexOf(q) !== -1) out.push(s); + } + return out; +} + +function updateVulnerabilityTypeaheadDatalist(inputId, listId, poolKey) { + const el = document.getElementById(inputId); + if (!el || !vulnerabilityFilterOptionsCache) return; + const q = el.value.trim(); + if (q.length < VULNERABILITY_DATALIST_MIN_QUERY) { + clearVulnerabilityDatalist(listId); + return; + } + let pool = vulnerabilityFilterOptionsCache[poolKey] || []; + if (poolKey === 'task_ids') { + pool = (vulnerabilityFilterOptionsCache.task_ids || []).concat(vulnerabilityFilterOptionsCache.queue_ids || []); + } + populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(pool, q)); +} + +function bindVulnerabilityFilterTypeaheads() { + const pairs = [ + { inputId: 'vulnerability-conversation-filter', listId: 'vulnerability-conversation-suggestions', poolKey: 'conversation_ids' }, + { inputId: 'vulnerability-task-filter', listId: 'vulnerability-task-suggestions', poolKey: 'task_ids' } + ]; + pairs.forEach(function (pair) { + const el = document.getElementById(pair.inputId); + if (!el) return; + el.addEventListener('input', function () { + updateVulnerabilityTypeaheadDatalist(pair.inputId, pair.listId, pair.poolKey); + }); + el.addEventListener('blur', function () { + setTimeout(function () { clearVulnerabilityDatalist(pair.listId); }, 150); + }); + }); + + ['vulnerability-conversation-tag-filter', 'vulnerability-task-tag-filter'].forEach(function (inputId) { + const el = document.getElementById(inputId); + if (!el) return; + el.addEventListener('focus', function () { + if (!vulnerabilityFilterOptionsCache) return; + const listId = inputId === 'vulnerability-conversation-tag-filter' + ? 'vulnerability-conversation-tag-suggestions' + : 'vulnerability-task-tag-suggestions'; + const key = inputId === 'vulnerability-conversation-tag-filter' ? 'conversation_tags' : 'task_tags'; + const q = el.value.trim(); + if (q.length >= VULNERABILITY_DATALIST_MIN_QUERY) { + populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(vulnerabilityFilterOptionsCache[key], q), { max: 20 }); + } + }); + }); +} + +function syncVulnerabilityStatCardActiveState() { + const sevEl = document.getElementById('vulnerability-severity-filter'); + const sev = (sevEl && sevEl.value) || vulnerabilityFilters.severity || ''; + const root = document.getElementById('vulnerability-stat-cards'); + if (!root) return; + root.querySelectorAll('.stat-card.is-clickable').forEach(function (card) { + if (card.classList.contains('stat-card-total')) { + card.classList.toggle('is-active', !sev); + card.setAttribute('aria-pressed', sev ? 'false' : 'true'); + } else { + const cardSev = card.getAttribute('data-severity'); + const active = Boolean(sev && cardSev === sev); + card.classList.toggle('is-active', active); + card.setAttribute('aria-pressed', active ? 'true' : 'false'); + } + }); +} + +function updateVulnerabilityStatStackedBar(bySeverity, total) { + const bar = document.getElementById('stat-stacked-bar'); + if (!bar) return; + const segs = bar.querySelectorAll('.stat-stacked-seg'); + if (!total) { + bar.classList.add('is-empty'); + segs.forEach(function (seg) { + seg.style.flex = '0 0 0'; + seg.style.display = 'none'; + }); + return; + } + bar.classList.remove('is-empty'); + segs.forEach(function (seg) { + const sev = seg.getAttribute('data-sev'); + const count = bySeverity[sev] || 0; + if (count <= 0) { + seg.style.display = 'none'; + seg.style.flex = '0 0 0'; + return; + } + seg.style.display = ''; + const pct = Math.max((count / total) * 100, 0); + seg.style.flex = '1 1 ' + pct + '%'; + }); +} + // 加载漏洞统计 async function loadVulnerabilityStats() { try { @@ -169,15 +643,33 @@ function updateVulnerabilityStats(stats) { by_status: {} }; } - - document.getElementById('stat-total').textContent = stats.total || 0; - + + const total = stats.total || 0; const bySeverity = stats.by_severity || {}; - document.getElementById('stat-critical').textContent = bySeverity.critical || 0; - document.getElementById('stat-high').textContent = bySeverity.high || 0; - document.getElementById('stat-medium').textContent = bySeverity.medium || 0; - document.getElementById('stat-low').textContent = bySeverity.low || 0; - document.getElementById('stat-info').textContent = bySeverity.info || 0; + + const totalEl = document.getElementById('stat-total'); + if (totalEl) { + totalEl.textContent = String(total); + totalEl.classList.toggle('is-zero', total === 0); + } + + VULN_STAT_SEVERITIES.forEach(function (sev) { + const count = bySeverity[sev] || 0; + const valEl = document.getElementById('stat-' + sev); + const pctEl = document.getElementById('stat-' + sev + '-pct'); + if (valEl) { + valEl.textContent = String(count); + valEl.classList.toggle('is-zero', count === 0); + } + if (pctEl) { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + pctEl.textContent = pct + '%'; + pctEl.setAttribute('aria-hidden', total === 0 ? 'true' : 'false'); + } + }); + + updateVulnerabilityStatStackedBar(bySeverity, total); + syncVulnerabilityStatCardActiveState(); } // 加载漏洞列表 @@ -591,32 +1083,26 @@ function closeVulnerabilityModal() { currentVulnerabilityId = null; } -// 筛选漏洞 +// 筛选漏洞(应用当前表单条件) function filterVulnerabilities() { - vulnerabilityFilters.id = document.getElementById('vulnerability-id-filter').value.trim(); - vulnerabilityFilters.conversation_id = document.getElementById('vulnerability-conversation-filter').value.trim(); - vulnerabilityFilters.task_id = document.getElementById('vulnerability-task-filter').value.trim(); - vulnerabilityFilters.conversation_tag = document.getElementById('vulnerability-conversation-tag-filter').value.trim(); - vulnerabilityFilters.task_tag = document.getElementById('vulnerability-task-tag-filter').value.trim(); - vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value; - vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value; - - // 重置到第一页 - vulnerabilityPagination.currentPage = 1; - - loadVulnerabilityStats(); - loadVulnerabilities(); + applyVulnerabilityFilters(); } // 清除筛选 function clearVulnerabilityFilters() { - document.getElementById('vulnerability-id-filter').value = ''; - document.getElementById('vulnerability-conversation-filter').value = ''; - document.getElementById('vulnerability-task-filter').value = ''; - document.getElementById('vulnerability-conversation-tag-filter').value = ''; - document.getElementById('vulnerability-task-tag-filter').value = ''; - document.getElementById('vulnerability-severity-filter').value = ''; - document.getElementById('vulnerability-status-filter').value = ''; + const fields = [ + 'vulnerability-id-filter', + 'vulnerability-conversation-filter', + 'vulnerability-task-filter', + 'vulnerability-conversation-tag-filter', + 'vulnerability-task-tag-filter', + 'vulnerability-severity-filter', + 'vulnerability-status-filter' + ]; + fields.forEach(function (id) { + const el = document.getElementById(id); + if (el) el.value = ''; + }); vulnerabilityFilters = { id: '', @@ -628,11 +1114,7 @@ function clearVulnerabilityFilters() { status: '' }; - // 重置到第一页 - vulnerabilityPagination.currentPage = 1; - - loadVulnerabilityStats(); - loadVulnerabilities(); + applyVulnerabilityFilters(); } // 刷新漏洞 @@ -908,6 +1390,7 @@ window.onclick = function(event) { document.addEventListener('languagechange', function () { const page = document.getElementById('page-vulnerabilities'); if (page && page.classList.contains('active')) { + renderVulnerabilityFilterChips(); loadVulnerabilities(); } }); diff --git a/web/templates/index.html b/web/templates/index.html index 85248cd7..2c3c5cbc 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1082,10 +1082,13 @@
    -
    -

    执行统计

    +
    +
    +

    执行统计

    + +
    -
    +
    加载中...
    @@ -1385,89 +1388,124 @@
    - +
    -
    -
    +
    +
    总漏洞数
    -
    +
    -
    +
    严重
    -
    +
    -
    +
    高危
    -
    +
    -
    +
    中危
    -
    +
    -
    +
    低危
    -
    +
    -
    +
    信息
    -
    +
    - -
    -
    - - - - - - - - - - + +
    +
    +
    + + + +
    +
    +
    + + +
    + + + + +
    @@ -3515,7 +3553,7 @@ - +