diff --git a/web/static/css/style.css b/web/static/css/style.css
index dcc7c5ac..a64b5ccc 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -9574,6 +9574,369 @@ html[data-theme="dark"] .openapi-doc-btn:hover {
display: none;
}
+/* MCP 监控统计重构:工具卡 + 柱线趋势 */
+.mcp-stats-combined {
+ border-radius: 8px;
+}
+
+.mcp-stats-combined__head {
+ padding: 14px 18px;
+}
+
+.mcp-stats-combined__body--full {
+ min-height: 248px;
+}
+
+.mcp-stats-combined__main {
+ padding: 14px 16px 16px;
+}
+
+.mcp-stats-combined__body--full .mcp-stats-combined__timeline {
+ gap: 8px;
+ padding: 14px 16px 12px;
+}
+
+.mcp-stats-tools-panel {
+ gap: 12px;
+}
+
+.mcp-stats-proportion-bar {
+ height: 12px;
+ box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.05);
+}
+
+.mcp-stats-tools-panel__caption {
+ gap: 6px;
+}
+
+.mcp-stats-tools-panel__list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 10px;
+}
+
+.mcp-stats-tool-item {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 10px;
+ min-width: 0;
+ min-height: 132px;
+ padding: 12px;
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ background:
+ linear-gradient(180deg, rgba(248, 250, 252, 0.92), rgba(255, 255, 255, 0.96));
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8) inset;
+}
+
+.mcp-stats-tool-item:hover,
+.mcp-stats-tool-item.is-highlighted {
+ background:
+ linear-gradient(180deg, rgba(239, 246, 255, 0.96), rgba(255, 255, 255, 0.98));
+ border-color: rgba(59, 130, 246, 0.26);
+}
+
+.mcp-stats-tool-item.is-active {
+ background: rgba(59, 130, 246, 0.08);
+ border-color: rgba(59, 130, 246, 0.38);
+}
+
+.mcp-stats-tool-item__top,
+.mcp-stats-tool-item__middle,
+.mcp-stats-tool-item__bottom {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+}
+
+.mcp-stats-tool-item__top {
+ gap: 8px;
+}
+
+.mcp-stats-tool-item__rank {
+ flex: 0 0 auto;
+}
+
+.mcp-stats-tool-item__name {
+ font-size: 0.8125rem;
+}
+
+.mcp-stats-tool-item__share {
+ margin-left: auto;
+ padding: 2px 7px;
+ border-radius: 999px;
+ background: rgba(15, 23, 42, 0.05);
+}
+
+.mcp-stats-tool-item__middle {
+ gap: 8px;
+}
+
+.mcp-stats-tool-item__calls {
+ font-size: 1.45rem;
+ line-height: 1;
+}
+
+.mcp-stats-tool-item__calls-label {
+ flex: 0 0 auto;
+ font-size: 0.6875rem;
+ color: var(--text-muted);
+}
+
+.mcp-stats-tool-item__track {
+ flex: 1 1 auto;
+ width: auto;
+ height: 6px;
+ background: rgba(15, 23, 42, 0.07);
+}
+
+.mcp-stats-tool-item__bottom {
+ gap: 6px;
+ flex-wrap: wrap;
+ margin-top: auto;
+}
+
+.mcp-stats-tool-item__pill {
+ display: inline-flex;
+ align-items: center;
+ min-height: 20px;
+ padding: 2px 7px;
+ border-radius: 999px;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ background: rgba(148, 163, 184, 0.13);
+}
+
+.mcp-stats-tool-item__pill.is-success {
+ color: #15803d;
+ background: rgba(34, 197, 94, 0.13);
+}
+
+.mcp-stats-tool-item__pill.is-danger {
+ color: #b91c1c;
+ background: rgba(239, 68, 68, 0.12);
+}
+
+.mcp-stats-tool-item__rate {
+ margin-left: auto;
+ flex-direction: row;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+}
+
+.mcp-stats-tool-item__fail {
+ font-size: 0.625rem;
+}
+
+.mcp-stats-timeline-moments {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ margin: 2px 0 0;
+}
+
+.mcp-stats-timeline-moments__label {
+ flex: 0 0 auto;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: var(--text-muted);
+}
+
+.mcp-stats-timeline-moments__list {
+ display: flex;
+ gap: 6px;
+ min-width: 0;
+ overflow: hidden;
+ flex: 1 1 auto;
+ flex-wrap: nowrap;
+}
+
+.mcp-stats-timeline-moment {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ max-width: 144px;
+ padding: 4px 8px;
+ border-radius: 999px;
+ background: rgba(59, 130, 246, 0.08);
+ color: #1d4ed8;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ flex: 0 0 auto;
+}
+
+.mcp-stats-timeline-moment__time {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.mcp-stats-timeline-moment__count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: 999px;
+ color: #fff;
+ background: #2563eb;
+ box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.04);
+}
+
+.mcp-stats-timeline-moment__fail {
+ display: inline-flex;
+ align-items: center;
+ min-height: 18px;
+ padding: 0 6px;
+ border-radius: 999px;
+ color: #b91c1c;
+ background: rgba(239, 68, 68, 0.12);
+ font-size: 0.625rem;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
+.mcp-stats-timeline-moment--more {
+ color: var(--text-secondary);
+ background: rgba(148, 163, 184, 0.14);
+ font-weight: 700;
+}
+
+.mcp-stats-timeline__chart-wrap {
+ min-height: 146px;
+ padding: 4px 0 0;
+}
+
+.mcp-stats-combined__timeline .mcp-stats-timeline__chart-wrap,
+.mcp-stats-combined__timeline .mcp-stats-timeline__chart {
+ min-height: 132px;
+}
+
+.mcp-stats-timeline-bar {
+ fill: rgba(59, 130, 246, 0.14);
+ cursor: crosshair;
+ transition: fill 0.12s ease, opacity 0.12s ease;
+}
+
+.mcp-stats-timeline-bar.is-active {
+ fill: rgba(59, 130, 246, 0.42);
+}
+
+.mcp-stats-timeline-bar.is-hover {
+ fill: rgba(37, 99, 235, 0.68);
+}
+
+.mcp-stats-timeline-bar-fail {
+ fill: rgba(239, 68, 68, 0.72);
+ cursor: crosshair;
+}
+
+.mcp-stats-timeline-bar-fail.is-hover {
+ fill: rgba(220, 38, 38, 0.9);
+}
+
+.mcp-stats-timeline-area {
+ opacity: 0.55;
+}
+
+.mcp-stats-timeline-line {
+ stroke-width: 2;
+}
+
+.mcp-stats-timeline-dot {
+ opacity: 0;
+}
+
+@media (max-width: 1180px) {
+ .mcp-stats-tools-panel__list {
+ grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
+ }
+}
+
+@media (max-width: 720px) {
+ .mcp-stats-tools-panel__list {
+ grid-template-columns: 1fr;
+ }
+ .mcp-stats-combined__main {
+ max-width: none;
+ }
+ .mcp-stats-combined__timeline {
+ max-width: none !important;
+ }
+ .mcp-stats-timeline-moments {
+ align-items: center;
+ flex-direction: row;
+ }
+ .mcp-stats-timeline-moments__list {
+ flex-wrap: nowrap;
+ overflow: hidden;
+ }
+}
+
+html[data-theme="dark"] .mcp-stats-tool-item {
+ background: linear-gradient(180deg, #111827 0%, #0f172a 100%);
+ border-color: #263244;
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset;
+}
+
+html[data-theme="dark"] .mcp-stats-tool-item:hover,
+html[data-theme="dark"] .mcp-stats-tool-item.is-highlighted {
+ background: linear-gradient(180deg, #172033 0%, #111827 100%);
+ border-color: rgba(96, 165, 250, 0.36);
+}
+
+html[data-theme="dark"] .mcp-stats-tool-item.is-active {
+ background: rgba(96, 165, 250, 0.12);
+ border-color: rgba(96, 165, 250, 0.46);
+}
+
+html[data-theme="dark"] .mcp-stats-tool-item__share,
+html[data-theme="dark"] .mcp-stats-tool-item__track {
+ background: rgba(148, 163, 184, 0.12);
+}
+
+html[data-theme="dark"] .mcp-stats-tool-item__pill {
+ background: rgba(148, 163, 184, 0.14);
+}
+
+html[data-theme="dark"] .mcp-stats-tool-item__pill.is-success {
+ color: #86efac;
+ background: rgba(34, 197, 94, 0.14);
+}
+
+html[data-theme="dark"] .mcp-stats-tool-item__pill.is-danger,
+html[data-theme="dark"] .mcp-stats-timeline-moment__fail {
+ color: #fca5a5;
+ background: rgba(239, 68, 68, 0.14);
+}
+
+html[data-theme="dark"] .mcp-stats-timeline-moment {
+ color: #93c5fd;
+ background: rgba(96, 165, 250, 0.14);
+}
+
+html[data-theme="dark"] .mcp-stats-timeline-moment--more {
+ color: var(--text-secondary);
+ background: rgba(148, 163, 184, 0.14);
+}
+
+html[data-theme="dark"] .mcp-stats-timeline-bar {
+ fill: rgba(96, 165, 250, 0.16);
+}
+
+html[data-theme="dark"] .mcp-stats-timeline-bar.is-active {
+ fill: rgba(96, 165, 250, 0.46);
+}
+
+html[data-theme="dark"] .mcp-stats-timeline-bar.is-hover {
+ fill: rgba(96, 165, 250, 0.78);
+}
+
/* ── 工具统计面板:表格 + 饼图 ── */
.mcp-stats-panel {
background: #fff;
diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js
index 8ffbbeab..cb6238cb 100644
--- a/web/static/js/monitor.js
+++ b/web/static/js/monitor.js
@@ -4377,6 +4377,8 @@ function buildMcpTimelineSvg(points, rangeKey) {
const maxVal = Math.max(1, ...points.map((p) => p.total || 0));
const hasFailed = points.some((p) => (p.failed || 0) > 0);
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
+ const barGap = points.length > 48 ? 1 : 2;
+ const barW = Math.max(1.6, Math.min(8, (plotW / Math.max(1, points.length)) - barGap));
const coords = points.map((p, i) => {
const x = padL + (points.length <= 1 ? plotW / 2 : (i / (points.length - 1)) * plotW);
@@ -4433,6 +4435,21 @@ function buildMcpTimelineSvg(points, rangeKey) {
data-failed="${c.p.failed || 0}" />`;
}).join('');
+ const bars = coords.map((c) => {
+ const total = c.p.total || 0;
+ const failed = c.p.failed || 0;
+ const h = total > 0 ? Math.max(3, (total / maxVal) * plotH) : 1;
+ const y = baseY - h;
+ const failedH = failed > 0 ? Math.max(2, (failed / maxVal) * plotH) : 0;
+ const tipTime = formatMcpTimelineLabel(c.p.t, rangeKey, locale);
+ return `
${escapeHtml(sparseHint)}
` : ''; return ` + ${momentsHtml}