diff --git a/web/static/css/style.css b/web/static/css/style.css index e5195903..919996aa 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -5984,18 +5984,58 @@ header { .monitor-sections { display: grid; - gap: 24px; + gap: 12px; width: 100%; box-sizing: border-box; min-width: 0; } +/* ── MCP 监控页:页面级布局 ── */ +.mcp-monitor-page .page-content { + padding: 16px 20px; + background: #f1f5f9; +} + +.mcp-monitor-page .page-header { + padding: 16px 24px; + background: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8) inset; +} + +.mcp-monitor-page .page-header-main { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.mcp-monitor-page .page-header h2 { + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.monitor-page-subtitle { + margin: 0; + font-size: 0.8125rem; + color: var(--text-secondary); + font-weight: 500; + line-height: 1.4; +} + +.mcp-monitor-page .btn-icon-text { + display: inline-flex; + align-items: center; + gap: 6px; +} + .monitor-section { background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 14px; - padding: 20px; - box-shadow: var(--shadow-sm); + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 16px; + padding: 20px 22px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 16px rgba(0, 0, 0, 0.03); display: flex; flex-direction: column; gap: 16px; @@ -6004,6 +6044,14 @@ header { box-sizing: border-box; } +.monitor-section.monitor-overview { + background: transparent; + border: none; + box-shadow: none; + padding: 0; + gap: 0; +} + .monitor-section .section-header { display: flex; align-items: center; @@ -6015,7 +6063,9 @@ header { .monitor-section .section-header h3 { margin: 0; - font-size: 1.1rem; + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.01em; color: var(--text-primary); flex-shrink: 0; min-width: 0; @@ -6089,22 +6139,7 @@ header { 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 执行统计 — 最终布局(metrics bar + panel 表格/饼图) */ .mcp-exec-stats-root { width: 100%; min-width: 0; @@ -6113,122 +6148,114 @@ header { .mcp-exec-stats { display: flex; flex-direction: column; - gap: 20px; + gap: 12px; width: 100%; } -.mcp-stats-kpi-row { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; +/* ── KPI 概览条 ── */ +.mcp-stats-kpi { + display: flex; + flex-direction: row; + align-items: stretch; + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.07); + border-radius: 12px; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); + overflow: hidden; } -@media (max-width: 900px) { - .mcp-stats-kpi-row { - grid-template-columns: 1fr; +@media (max-width: 768px) { + .mcp-stats-kpi { + flex-direction: column; } } -.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); +.mcp-stats-kpi__item { + flex: 1; + display: flex; + align-items: stretch; + gap: 12px; + padding: 14px 20px; + min-width: 0; + position: relative; +} + +.mcp-stats-kpi__item:not(:last-child)::after { + content: ''; + position: absolute; + right: 0; + top: 18%; + height: 64%; + width: 1px; + background: rgba(0, 0, 0, 0.06); +} + +@media (max-width: 768px) { + .mcp-stats-kpi__item:not(:last-child)::after { + right: 16px; + left: 16px; + top: auto; + bottom: 0; + width: auto; + height: 1px; + } +} + +.mcp-stats-kpi__accent { + flex: 0 0 3px; + border-radius: 3px; + align-self: stretch; +} + +.mcp-stats-kpi__item--calls .mcp-stats-kpi__accent { background: linear-gradient(180deg, #60a5fa, #2563eb); } +.mcp-stats-kpi__item--rate .mcp-stats-kpi__accent { background: linear-gradient(180deg, #2dd4bf, #0d9488); } +.mcp-stats-kpi__item--time .mcp-stats-kpi__accent { background: linear-gradient(180deg, #a78bfa, #7c3aed); } + +.mcp-stats-kpi__content { 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; + gap: 4px; 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__label { + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.03em; + text-transform: uppercase; + color: var(--text-muted); } -.mcp-stats-kpi-value--time { - font-size: 1.05rem; +.mcp-stats-kpi__value { + font-size: 1.625rem; font-weight: 700; + color: var(--text-primary); + font-variant-numeric: tabular-nums; + line-height: 1.05; + letter-spacing: -0.03em; +} + +.mcp-stats-kpi__value--rate.is-success { color: #15803d; } +.mcp-stats-kpi__value--rate.is-warning { color: #ca8a04; } +.mcp-stats-kpi__value--rate.is-danger { color: #dc2626; } + +.mcp-stats-kpi__value--time { + font-size: 0.875rem; + font-weight: 600; letter-spacing: -0.01em; line-height: 1.35; - word-break: break-word; } -.mcp-stats-kpi-sub { +.mcp-stats-kpi__meta { display: flex; - align-items: center; flex-wrap: wrap; - gap: 8px; - min-height: 22px; + gap: 6px; + margin-top: 2px; } -.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 { +.mcp-stats-kpi__chip { display: inline-flex; align-items: center; - gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 0.6875rem; @@ -6236,187 +6263,968 @@ header { font-variant-numeric: tabular-nums; } -.mcp-stats-pill--success { +.mcp-stats-kpi__chip.is-ok { + color: #166534; background: rgba(34, 197, 94, 0.12); - color: #15803d; } -.mcp-stats-pill--fail { +.mcp-stats-kpi__chip.is-fail { + color: #991b1b; background: rgba(239, 68, 68, 0.1); +} + +.mcp-stats-kpi__status { + font-size: 0.75rem; + font-weight: 500; +} + +.mcp-stats-kpi__status.is-success { color: #15803d; } +.mcp-stats-kpi__status.is-warning { color: #ca8a04; } +.mcp-stats-kpi__status.is-danger { color: #dc2626; } + +/* ── 工具统计 + 调用趋势(合并面板) ── */ +.mcp-stats-combined { + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.07); + border-radius: 10px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + overflow: hidden; +} + +.mcp-stats-combined__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + padding: 12px 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + background: #fafbfc; +} + +.mcp-stats-combined__title { + margin: 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary); +} + +.mcp-stats-combined__meta-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 10px; + margin-top: 6px; +} + +.mcp-stats-combined__meta { + margin: 0; + font-size: 0.6875rem; + color: var(--text-muted); + line-height: 1.4; +} + +.mcp-stats-combined__scopes { + display: inline-flex; + flex-wrap: wrap; + gap: 6px; +} + +.mcp-stats-scope-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.625rem; + font-weight: 600; + letter-spacing: 0.02em; + line-height: 1.3; +} + +.mcp-stats-scope-badge--cumulative { + color: #1e40af; + background: rgba(59, 130, 246, 0.1); +} + +.mcp-stats-scope-badge--timeline { + color: #0f766e; + background: rgba(20, 184, 166, 0.12); +} + +.mcp-stats-scope-badge--inline { + margin-right: 6px; + vertical-align: middle; +} + +.mcp-stats-filter-chip { + display: inline-flex; + align-items: center; + gap: 2px; + max-width: min(280px, 100%); + padding: 4px 8px 4px 10px; + border-radius: 999px; + background: rgba(0, 102, 255, 0.08); + border: 1px solid rgba(0, 102, 255, 0.22); +} + +.mcp-stats-filter-chip__label { + font-size: 0.75rem; + font-weight: 500; + color: var(--accent-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mcp-stats-filter-chip__clear, +.mcp-stats-filter-chip .mcp-stats-clear-filter { + display: inline-flex; + align-items: center; + justify-content: center; + width: auto; + min-width: 18px; + height: auto; + margin: 0; + padding: 0 2px 0 4px; + border: none; + border-radius: 0; + background: transparent; + box-shadow: none; + color: var(--accent-color); + font-size: 1.125rem; + font-weight: 400; + line-height: 1; + cursor: pointer; + flex-shrink: 0; +} + +.mcp-stats-filter-chip__clear:hover, +.mcp-stats-filter-chip .mcp-stats-clear-filter:hover { + background: transparent; + color: #1d4ed8; + opacity: 0.85; +} + +.mcp-stats-combined__actions { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + flex-wrap: wrap; +} + +/* 左侧工具统计 : 右侧调用趋势 = 1 : 1 */ +.mcp-stats-combined__body--full { + display: flex; + flex-direction: row; + align-items: stretch; + min-width: 0; + min-height: 188px; +} + +.mcp-stats-combined__body--tools { + display: flex; + flex-direction: row; + align-items: stretch; + min-width: 0; +} + +.mcp-stats-combined__body--timeline { + display: block; +} + +.mcp-stats-combined__main { + flex: 1 1 50%; + max-width: 50%; + min-width: 0; + padding: 10px 14px 12px; +} + +.mcp-stats-combined__body--full .mcp-stats-combined__timeline { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1 1 50%; + max-width: 50%; + min-width: 0; + padding: 8px 12px 10px; + background: #fff; + border-left: 1px solid rgba(0, 0, 0, 0.06); +} + +.mcp-stats-combined__body--tools .mcp-stats-tool-table { + width: 100%; +} + +@media (max-width: 900px) { + .mcp-stats-combined__body--full { + min-height: 160px; + } +} + +@media (max-width: 720px) { + .mcp-stats-combined__body--full, + .mcp-stats-combined__body--tools { + flex-direction: column; + min-height: 0; + } + .mcp-stats-combined__main { + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + } + .mcp-stats-tool-item__metrics, + .mcp-stats-tool-item__metrics-head { + grid-template-columns: 3rem 2.5rem 4rem; + column-gap: 0.875rem; + padding-left: 12px; + } + .mcp-stats-tool-item__track { + width: min(100%, 180px); + } + .mcp-stats-combined__timeline { + flex: 1 1 auto; + width: 100%; + max-width: none; + min-width: 0; + border-left: none; + } +} + +.mcp-stats-combined__col-label { + margin: 0; + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-muted); +} + +.mcp-stats-combined__timeline-inner { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + min-width: 0; +} + +/* 左侧工具统计:堆叠条 + 排行列表 */ +.mcp-stats-tools-panel { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.mcp-stats-tools-panel__hero { + display: flex; + flex-direction: column; + gap: 6px; +} + +.mcp-stats-proportion-bar { + display: flex; + align-items: stretch; + height: 10px; + border-radius: 999px; + overflow: hidden; + background: rgba(148, 163, 184, 0.2); +} + +.mcp-stats-proportion-seg { + min-width: 3px; + border: none; + padding: 0; + cursor: pointer; + transition: filter 0.15s ease, opacity 0.15s ease; +} + +.mcp-stats-proportion-seg.is-others { + cursor: default; +} + +.mcp-stats-proportion-seg.is-highlighted, +.mcp-stats-proportion-seg.is-active { + filter: brightness(1.08); + z-index: 1; +} + +.mcp-stats-proportion-seg.is-dimmed { + opacity: 0.35; +} + +.mcp-stats-proportion-seg:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.5); + outline-offset: 1px; +} + +.mcp-stats-tools-panel__caption { + margin: 0; + font-size: 0.6875rem; + color: var(--text-muted); + line-height: 1.35; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; +} + +.mcp-stats-timeline__sparse-hint { + margin: 0; + padding: 0 2px; + font-size: 0.625rem; + color: var(--text-muted); + line-height: 1.35; +} + +.mcp-stats-tools-panel__list-head { + display: flex; + align-items: flex-end; + gap: 8px; + padding: 0 10px 4px; + font-size: 0.6875rem; + color: var(--text-muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.mcp-stats-tools-panel__list-head > span:nth-child(1) { + flex: 0 0 26px; + text-align: center; +} + +.mcp-stats-tools-panel__list-head > span:nth-child(2) { + flex: 0 0 10px; +} + +.mcp-stats-tools-panel__list-head > span:nth-child(3) { + flex: 1 1 auto; + min-width: 0; +} + +.mcp-stats-tools-panel__list-head .mcp-stats-tool-item__metrics-head { + margin-left: auto; +} + +.mcp-stats-tools-panel__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.mcp-stats-tool-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + cursor: pointer; + transition: background 0.12s ease; + font-size: 0.8125rem; +} + +.mcp-stats-tool-item__rank { + flex: 0 0 26px; +} + +.mcp-stats-tool-item__dot { + flex: 0 0 10px; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.mcp-stats-tool-item__body { + flex: 1 1 auto; + min-width: 0; + display: flex; + flex-direction: column; + gap: 5px; +} + +.mcp-stats-tool-item__metrics { + margin-left: auto; +} + +.mcp-stats-tool-item__metrics, +.mcp-stats-tool-item__metrics-head { + display: grid; + grid-template-columns: 3.5rem 3rem 4.75rem; + column-gap: 1.375rem; + row-gap: 3px; + align-items: center; + justify-items: end; + flex: 0 0 auto; + padding: 4px 8px 4px 20px; + border-left: 1px solid rgba(0, 0, 0, 0.06); + white-space: nowrap; +} + +.mcp-stats-tool-item__metrics-head { + border-left: none; + padding-left: 18px; + line-height: 1.2; +} + +.mcp-stats-tool-item__metrics-head > span { + text-align: right; +} + +.mcp-stats-tool-item:hover, +.mcp-stats-tool-item.is-highlighted { + background: rgba(0, 102, 255, 0.05); +} + +.mcp-stats-tool-item.is-active { + background: rgba(0, 102, 255, 0.09); +} + +.mcp-stats-tool-item.is-dimmed { + opacity: 0.42; +} + +.mcp-stats-tool-item:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.45); + outline-offset: -2px; +} + +.mcp-stats-tool-item__name { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.75rem; + font-weight: 500; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1 1 auto; +} + +.mcp-stats-tool-item__share { + font-size: 0.75rem; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: var(--text-muted); + line-height: 1.2; +} + +.mcp-stats-tool-item__track { + display: block; + width: min(100%, 240px); + max-width: 100%; + height: 4px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.05); + overflow: hidden; +} + +.mcp-stats-tool-item__fill { + display: block; + height: 100%; + border-radius: 999px; + min-width: 2px; + transition: width 0.2s ease; +} + +.mcp-stats-tool-item__calls { + font-size: 0.875rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--text-primary); + line-height: 1.25; + min-width: 2ch; +} + +.mcp-stats-tool-item__rate { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; + gap: 2px; + font-size: 0.75rem; + font-weight: 600; + font-variant-numeric: tabular-nums; + white-space: nowrap; + line-height: 1.25; + min-width: 3.5rem; +} + +.mcp-stats-tool-item__rate.is-success { color: #15803d; } +.mcp-stats-tool-item__rate.is-warning { color: #ca8a04; } +.mcp-stats-tool-item__rate.is-danger { color: #dc2626; } + +.mcp-stats-tool-item__fail { + display: block; + font-size: 0.625rem; + font-weight: 500; + color: #b91c1c; + line-height: 1.2; +} + +.mcp-stats-tool-item .mcp-stats-rank { + width: 22px; + height: 22px; + font-size: 0.6875rem; +} + +.mcp-stats-combined .mcp-stats-timeline__legend { + margin-top: 4px; + gap: 8px; +} + +/* ── 调用趋势折线图(内嵌于合并面板) ── */ +.mcp-stats-timeline__inline-meta { + margin: 0; + font-size: 0.625rem; + color: var(--text-muted); + line-height: 1.35; +} + +.mcp-stats-timeline__ranges { + display: inline-flex; + gap: 4px; + padding: 2px; + background: rgba(0, 0, 0, 0.04); + border-radius: 8px; +} + +.mcp-stats-timeline__range { + padding: 4px 10px; + font-size: 0.75rem; + font-weight: 500; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + font: inherit; + line-height: 1.2; +} + +.mcp-stats-timeline__range:hover { + color: var(--text-primary); + background: rgba(255, 255, 255, 0.7); +} + +.mcp-stats-timeline__range.is-active { + background: #fff; + color: var(--accent-color); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); +} + +.mcp-stats-timeline__chart-wrap { + flex: 1; + min-height: 120px; + min-width: 0; + width: 100%; + padding: 2px 0; +} + +.mcp-stats-combined__timeline .mcp-stats-timeline__chart-wrap { + flex: 1; + min-height: 100px; + height: auto; +} + +.mcp-stats-timeline__chart { + width: 100%; + height: 100%; + display: block; +} + +.mcp-stats-combined__timeline .mcp-stats-timeline__chart { + min-height: 100px; + height: 100%; +} + +.mcp-stats-timeline__legend { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 6px; + padding: 0 4px; + font-size: 0.6875rem; + color: var(--text-muted); +} + +.mcp-stats-timeline__legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.mcp-stats-timeline__legend-item::before { + content: ''; + width: 16px; + height: 3px; + border-radius: 999px; + background: linear-gradient(90deg, #60a5fa, #2563eb); +} + +.mcp-stats-timeline__legend-item--fail::before { + background: transparent; + border-top: 2px dashed #ef4444; + height: 0; + width: 14px; +} + +.mcp-stats-timeline-line { + fill: none; + stroke: #3b82f6; + stroke-width: 1.75; + stroke-linecap: round; + stroke-linejoin: round; + vector-effect: non-scaling-stroke; +} + +.mcp-stats-timeline-area { + stroke: none; +} + +.mcp-stats-timeline-line--fail { + stroke: #f87171; + stroke-width: 1.75; + stroke-dasharray: 5 4; + opacity: 0.9; + filter: none; +} + +.mcp-stats-timeline-grid { + stroke: rgba(148, 163, 184, 0.35); + stroke-width: 1; + stroke-dasharray: 3 4; +} + +.mcp-stats-timeline-grid--base { + stroke: rgba(148, 163, 184, 0.55); + stroke-dasharray: none; +} + +.mcp-stats-timeline-axis { + font-size: 9px; + fill: var(--text-muted); +} + +.mcp-stats-timeline-y { + font-size: 9px; + fill: var(--text-muted); + text-anchor: end; +} + +.mcp-stats-timeline-peak-glow { + fill: rgba(59, 130, 246, 0.08); + stroke: none; + pointer-events: none; +} + +.mcp-stats-timeline-dot { + fill: #fff; + stroke: #3b82f6; + stroke-width: 1.5; + cursor: crosshair; + opacity: 0; + transition: opacity 0.12s ease; +} + +.mcp-stats-timeline-dot--peak { + opacity: 0.7; +} + +.mcp-stats-timeline__chart-wrap:hover .mcp-stats-timeline-dot, +.mcp-stats-timeline-dot.is-active { + opacity: 1; +} + +.mcp-stats-timeline-dot.is-active { + fill: #2563eb; + stroke: #fff; + stroke-width: 2; +} + +.mcp-stats-timeline-empty, +.mcp-stats-timeline-error { + margin: 0; + padding: 20px 8px; + text-align: center; + font-size: 0.75rem; + color: var(--text-muted); +} + +.mcp-stats-timeline-error { color: #b91c1c; } -.mcp-stats-stacked-bar { - display: flex; - height: 6px; - border-radius: 999px; +.mcp-stats-timeline-tooltip { + position: fixed; + z-index: 10000; + pointer-events: none; + padding: 6px 10px; + font-size: 0.75rem; + line-height: 1.35; + color: #fff; + background: rgba(15, 23, 42, 0.92); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + white-space: nowrap; + transform: translate(-50%, -100%); + margin-top: -8px; + display: none; +} + +/* ── 工具统计面板:表格 + 饼图 ── */ +.mcp-stats-panel { + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.07); + border-radius: 10px; overflow: hidden; - background: rgba(0, 0, 0, 0.06); - gap: 1px; - margin-top: 2px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); } -.mcp-stats-stacked-bar-seg { - min-width: 0; - transition: flex-grow 0.35s ease; +.mcp-stats-panel__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + background: #fafbfc; } -.mcp-stats-stacked-bar-seg--success { - background: linear-gradient(90deg, #22c55e, #16a34a); +.mcp-stats-panel__title { + margin: 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary); } -.mcp-stats-stacked-bar-seg--fail { - background: linear-gradient(90deg, #f87171, #dc2626); +.mcp-stats-panel__meta { + margin: 2px 0 0; + font-size: 0.6875rem; + color: var(--text-muted); } -.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 { +.mcp-stats-panel__body { display: grid; - grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr); - gap: 16px; + grid-template-columns: minmax(0, 1fr) 272px; align-items: stretch; } -@media (max-width: 1024px) { - .mcp-stats-split { +@media (max-width: 900px) { + .mcp-stats-panel__body { grid-template-columns: 1fr; } - - .mcp-stats-dist-body--stacked { - grid-template-columns: 1fr; - grid-template-rows: auto minmax(0, 1fr); - } - - .mcp-stats-dist-chart-stage { - width: 100%; - min-width: 0; - } - - .mcp-stats-dist-chart-wrap { - width: min(228px, 100%); - height: auto; - aspect-ratio: 1; - } - - .mcp-stats-dist-legend--grid { - justify-content: flex-start; - gap: 5px; - } - - .mcp-stats-dist-legend-item-wrap, - .mcp-stats-dist-legend--grid > .mcp-stats-dist-legend-item { - flex: 0 0 auto; - } - - .mcp-stats-tool-list { - justify-content: flex-start; - } } -.mcp-stats-split-left, -.mcp-stats-split-right { +.mcp-stats-panel__table-wrap { min-width: 0; - display: flex; - flex-direction: column; + overflow-x: auto; } -.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; -} - -/* 调用分布:左圆图 + 右图例,与左侧 Top6 同高;固定正方形尺寸,避免 cqh 失效导致饼图消失 */ -.mcp-stats-split-right { - align-items: stretch; -} - -.mcp-stats-dist-panel.mcp-stats-tools-panel { - gap: 8px; - padding: 12px 14px; - flex: 1; - min-height: 100%; +.mcp-stats-tool-table { width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; } -.mcp-stats-dist-panel .mcp-stats-tools-header { - flex-shrink: 0; - gap: 8px; +.mcp-stats-tool-table thead { + background: #f8fafc; } -.mcp-stats-dist-panel .mcp-stats-tools-legend { - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; +.mcp-stats-tool-table th { + padding: 8px 14px; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); + text-align: left; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + white-space: nowrap; +} + +.mcp-stats-tool-table td { + padding: 9px 14px; + border-bottom: 1px solid rgba(0, 0, 0, 0.04); + vertical-align: middle; +} + +.mcp-stats-tool-table tbody tr:last-child td { + border-bottom: none; +} + +.mcp-stats-tool-table .col-rank { + width: 44px; + text-align: center; +} + +.mcp-stats-tool-table .col-tool { + min-width: 140px; + max-width: 360px; +} + +.mcp-stats-tool-table .col-num, +.mcp-stats-tool-table .col-share { + width: 64px; + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.mcp-stats-tool-table .col-num { + font-weight: 600; + color: var(--text-primary); +} + +.mcp-stats-tool-table .col-share { + color: var(--text-secondary); + font-weight: 500; +} + +.mcp-stats-tool-table .col-rate { + width: 108px; + text-align: right; + white-space: nowrap; +} + +.mcp-stats-tool-table th.col-num, +.mcp-stats-tool-table th.col-share, +.mcp-stats-tool-table th.col-rate { + text-align: right; +} + +.mcp-stats-tool-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 8px; + vertical-align: middle; +} + +.mcp-stats-tool-label { + vertical-align: middle; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.8125rem; + font-weight: 500; +} + +.mcp-stats-tool-table .col-tool { overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.mcp-stats-dist-body--stacked { - flex: 1 1 auto; - display: grid; - grid-template-columns: minmax(168px, 40%) minmax(0, 1fr); - gap: 10px; - align-items: stretch; - min-height: 0; - width: 100%; +.mcp-stats-rank { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.05); + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 700; } -.mcp-stats-dist-chart-stage { +.mcp-stats-rank.rank-1 { background: rgba(234, 179, 8, 0.18); color: #a16207; } +.mcp-stats-rank.rank-2 { background: rgba(148, 163, 184, 0.22); color: #64748b; } +.mcp-stats-rank.rank-3 { background: rgba(180, 83, 9, 0.12); color: #b45309; } + +.mcp-stats-rate { font-weight: 600; font-variant-numeric: tabular-nums; } +.mcp-stats-rate.is-success { color: #15803d; } +.mcp-stats-rate.is-warning { color: #ca8a04; } +.mcp-stats-rate.is-danger { color: #dc2626; } + +.mcp-stats-fail-note { + margin-left: 6px; + font-size: 0.6875rem; + color: #b91c1c; + font-weight: 500; +} + +tr.mcp-stats-tool-row[data-tool-name] { + cursor: pointer; + transition: background 0.12s ease; +} + +tr.mcp-stats-tool-row[data-tool-name]:hover, +tr.mcp-stats-tool-row[data-tool-name].is-highlighted { + background: rgba(0, 102, 255, 0.04); +} + +tr.mcp-stats-tool-row[data-tool-name].is-active { + background: rgba(0, 102, 255, 0.07); +} + +tr.mcp-stats-tool-row[data-tool-name].is-dimmed { + opacity: 0.4; +} + +tr.mcp-stats-tool-row[data-tool-name]:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.45); + outline-offset: -2px; +} + +/* 饼图侧栏 */ +.mcp-stats-panel__aside { display: flex; align-items: center; justify-content: center; - min-width: 0; - min-height: 0; + padding: 16px; + border-left: 1px solid rgba(0, 0, 0, 0.06); + background: linear-gradient(180deg, #f8fafc 0%, #fff 100%); } -.mcp-stats-dist-chart-wrap { +@media (max-width: 900px) { + .mcp-stats-panel__aside { + border-left: none; + border-top: 1px solid rgba(0, 0, 0, 0.06); + padding: 20px 16px 24px; + } +} + +.mcp-stats-dist-panel--compact { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + max-width: 220px; + padding: 0; +} + +.mcp-stats-panel__aside-title { + margin: 0; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + text-align: center; +} + +.mcp-stats-panel__chart { position: relative; width: 100%; - max-width: 212px; + max-width: 196px; aspect-ratio: 1; - height: auto; - flex-shrink: 0; +} + +.mcp-stats-panel__aside-hint { + margin: 0; + font-size: 0.6875rem; + color: var(--text-muted); + text-align: center; + line-height: 1.35; } .mcp-stats-dist-svg { display: block; width: 100%; height: 100%; - filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.08)); } .mcp-stats-dist-segment { @@ -6425,24 +7233,10 @@ header { outline: none; } -.mcp-stats-dist-segment.is-others { - cursor: default; -} - -.mcp-stats-dist-segment.is-dimmed { - opacity: 0.45; -} - -.mcp-stats-dist-segment.is-highlighted { - opacity: 1; - filter: brightness(1.06); -} - -.mcp-stats-dist-segment.is-active { - filter: brightness(1.08); - stroke: rgba(0, 102, 255, 0.55); - stroke-width: 0.6; -} +.mcp-stats-dist-segment.is-others { cursor: default; } +.mcp-stats-dist-segment.is-dimmed { opacity: 0.35; } +.mcp-stats-dist-segment.is-highlighted, +.mcp-stats-dist-segment.is-active { filter: brightness(1.06); } .mcp-stats-dist-segment:focus-visible { outline: 2px solid rgba(0, 102, 255, 0.45); @@ -6451,509 +7245,76 @@ header { .mcp-stats-dist-donut-hole { position: absolute; - inset: 22%; + inset: 27%; border-radius: 50%; - background: var(--bg-primary); + background: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 2px; - box-shadow: inset 0 0 0 1px var(--border-color); + gap: 0; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05); pointer-events: none; } .mcp-stats-dist-donut-label { - font-size: 0.6875rem; + font-size: 0.5625rem; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.05em; color: var(--text-muted); - max-width: 92%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; text-align: center; } .mcp-stats-dist-donut-label:not(.is-default) { text-transform: none; - font-size: 0.625rem; - font-weight: 600; + font-size: 0.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + max-width: 90%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .mcp-stats-dist-donut-value { display: flex; align-items: baseline; - justify-content: center; gap: 1px; - font-size: 1.625rem; + font-size: 1.125rem; 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-size: 0.625rem; font-weight: 700; color: var(--text-secondary); } -.mcp-stats-dist-legend--grid { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; - min-height: 0; - height: 100%; - justify-content: space-between; -} - -.mcp-stats-dist-legend-item-wrap { - list-style: none; - margin: 0; - padding: 0; - width: 100%; - flex: 1 1 0; - min-height: 0; - display: flex; -} - -.mcp-stats-dist-legend--grid > .mcp-stats-dist-legend-item { - flex: 1 1 0; - min-height: 0; -} - -.mcp-stats-dist-legend-item { - display: grid; - grid-template-columns: 4px minmax(0, 1fr) auto; - grid-template-rows: auto; - gap: 0 6px; - align-items: center; - align-content: center; - width: 100%; - height: 100%; - min-height: 28px; - padding: 4px 6px; - border-radius: 8px; - background: var(--bg-primary); - border: 1px solid var(--border-color); - transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; - box-sizing: border-box; - font: inherit; - color: inherit; - text-align: left; - cursor: pointer; -} - -.mcp-stats-dist-legend-item:hover, -.mcp-stats-dist-legend-item.is-highlighted { - border-color: rgba(0, 102, 255, 0.22); - background: rgba(0, 102, 255, 0.03); -} - -.mcp-stats-dist-legend-item.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-dist-legend-item.is-dimmed { - opacity: 0.5; -} - -.mcp-stats-dist-legend-item:focus-visible { - outline: 2px solid rgba(0, 102, 255, 0.45); - outline-offset: 2px; -} - -.mcp-stats-dist-legend-item.is-others { - cursor: default; - opacity: 0.72; -} - -.mcp-stats-dist-legend-item.is-others:hover { - border-color: var(--border-color); - background: var(--bg-primary); -} - -.mcp-stats-dist-swatch { - grid-row: 1; - grid-column: 1; - align-self: stretch; - width: 4px; - min-height: 22px; - border-radius: 2px; - background: var(--swatch-color, #94a3b8); -} - -.mcp-stats-dist-legend-name { - grid-column: 2; - grid-row: 1; - 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.25; - min-width: 0; -} - -.mcp-stats-dist-legend-meta { - grid-column: 3; - grid-row: 1; - display: flex; - align-items: center; - gap: 5px; - font-size: 0.625rem; - font-variant-numeric: tabular-nums; - white-space: nowrap; - flex-shrink: 0; -} - -.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; + padding: 5px 12px; font-size: 0.75rem; - border-radius: 999px; - border: 1px solid var(--border-color); - background: var(--bg-primary); + border-radius: 6px; + border: 1px solid rgba(0, 102, 255, 0.25); + background: #fff; color: var(--accent-color); cursor: pointer; font-weight: 500; - transition: background 0.15s ease, border-color 0.15s ease; + font: inherit; + flex-shrink: 0; } .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 { + tr.mcp-stats-tool-row[data-tool-name], + .mcp-stats-dist-segment { transition: none; } - - .mcp-stats-tool-row:hover { - transform: none; - } } /* 兼容 Skills 监控等复用 monitor-stats-grid 的页面 */ @@ -7017,6 +7378,36 @@ header { overflow-x: auto; } +.mcp-monitor-page .monitor-executions { + padding: 14px 16px; +} + +.mcp-monitor-page .monitor-executions .section-header { + margin-bottom: 10px; +} + +.mcp-monitor-page .monitor-table th, +.mcp-monitor-page .monitor-table td { + padding: 8px 12px; +} + +.mcp-monitor-page .monitor-table-container { + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 12px; + box-shadow: none; +} + +.mcp-monitor-page .monitor-table thead { + background: rgba(0, 0, 0, 0.02); +} + +.mcp-monitor-page .monitor-empty { + padding: 48px 24px; + text-align: center; + color: var(--text-muted); + font-size: 0.875rem; +} + .monitor-table { width: 100%; border-collapse: collapse; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 1ab0e671..b4230512 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1514,6 +1514,17 @@ "unknownTool": "Unknown tool", "successFailedRate": "Success {{success}} / Failed {{failed}} · {{rate}}% success rate", "topToolsTitle": "Top {{n}} tools by calls", + "toolRankingTitle": "Tool call ranking", + "toolStatsTitle": "Tool statistics", + "toolStatsHint": "Click a bar segment or row to filter records below; hover to highlight", + "scopeCumulative": "All time", + "scopeTimeline": "Trend period", + "filterActive": "Filtered: {{tool}}", + "kpiScopeNote": "Lifetime totals", + "columnCalls": "Calls", + "columnShare": "Share", + "columnSuccessRate": "Success rate", + "rankingSummary": "Top {{n}} {{pct}}% · {{total}} total 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.", @@ -1526,9 +1537,21 @@ "rateWarning": "Some failures detected", "rateCritical": "High failure rate", "statsSubtitle": "Refreshed {{time}} · {{count}} tools", + "timelineTitle": "Call trend", + "timelineHint": "All tools combined (not split by tool)", + "timelineRange24h": "24h", + "timelineRange7d": "7d", + "timelineRange30d": "30d", + "timelineSummary": "{{total}} calls in range · peak {{peak}}", + "timelineSparseHint": "Most buckets are empty; peak {{peak}} calls at {{peakTime}}", + "timelineNoData": "No calls in this period", + "timelineLoadError": "Failed to load call trend", + "timelineTotalLegend": "Total calls", + "timelineFailedLegend": "Failed", + "timelineTooltip": "{{time}}: {{total}} calls ({{failed}} failed)", "distTitle": "Call distribution", "distLegend": "Slice area shows share of all calls", - "distClickHint": "Click legend or slice to filter records", + "distClickHint": "Click a bar segment to filter records", "distHeaderHint": "{{n}} total calls", "distSegmentAria": "{{name}}, {{pct}}% of calls, {{calls}} times", "distOthersNoFilter": "Other tools cannot be filtered individually", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 0af6f76b..ab653a16 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1503,6 +1503,17 @@ "unknownTool": "未知工具", "successFailedRate": "成功 {{success}} / 失败 {{failed}} · 成功率 {{rate}}%", "topToolsTitle": "工具调用 Top {{n}}", + "toolRankingTitle": "工具调用排行", + "toolStatsTitle": "工具统计", + "toolStatsHint": "点击色条或列表行筛选下方执行记录;悬停联动高亮", + "scopeCumulative": "累计", + "scopeTimeline": "趋势时段", + "filterActive": "已筛选:{{tool}}", + "kpiScopeNote": "累计统计(全时段)", + "columnCalls": "调用", + "columnShare": "占比", + "columnSuccessRate": "成功率", + "rankingSummary": "Top {{n}} 占 {{pct}}% · 共 {{total}} 次调用", "barVolumeLegend": "条长表示相对调用量,条内绿/红为成功/失败占比", "clickToFilterTool": "点击行筛选下方执行记录", "toolRowAriaLabel": "{{name}},{{total}} 次调用,成功率 {{rate}}%,点击查看执行记录", @@ -1515,9 +1526,21 @@ "rateWarning": "存在失败调用", "rateCritical": "失败率偏高", "statsSubtitle": "最后刷新 {{time}} · 共 {{count}} 个工具", + "timelineTitle": "调用趋势", + "timelineHint": "全部工具合计,不按工具拆分", + "timelineRange24h": "24 小时", + "timelineRange7d": "7 天", + "timelineRange30d": "30 天", + "timelineSummary": "区间内 {{total}} 次 · 峰值 {{peak}}", + "timelineSparseHint": "该时段多数时间为 0,峰值 {{peak}} 次出现在 {{peakTime}}", + "timelineNoData": "该时段暂无调用", + "timelineLoadError": "无法加载调用趋势", + "timelineTotalLegend": "总调用", + "timelineFailedLegend": "失败", + "timelineTooltip": "{{time}}:{{total}} 次(失败 {{failed}})", "distTitle": "调用分布", "distLegend": "扇区面积为占全部调用比例", - "distClickHint": "点击图例或扇区筛选执行记录", + "distClickHint": "点击色条筛选执行记录", "distHeaderHint": "共 {{n}} 次调用", "distSegmentAria": "{{name}},占 {{pct}}%,{{calls}} 次", "distOthersNoFilter": "其他工具无法单独筛选", diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index a52ea6cf..a3d65ef8 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -3329,6 +3329,9 @@ let monitorPanelFetchSeq = 0; const monitorState = { executions: [], stats: {}, + timeline: null, + timelineRange: null, + timelineError: null, lastFetchedAt: null, pagination: { page: 1, @@ -3415,17 +3418,15 @@ async function refreshMonitorPanel(page = null) { url += `&tool=${encodeURIComponent(currentToolFilter)}`; } - const response = await apiFetch(url, { method: 'GET' }); - const result = await response.json().catch(() => ({})); - if (!response.ok) { - throw new Error(result.error || '获取监控数据失败'); - } + const { result, timeline, timelineError } = await fetchMonitorAndTimeline(url); if (mySeq !== monitorPanelFetchSeq) { return; } monitorState.executions = Array.isArray(result.executions) ? result.executions : []; monitorState.stats = result.stats || {}; + monitorState.timeline = timeline; + monitorState.timelineError = timelineError; monitorState.lastFetchedAt = new Date(); // 更新分页信息 @@ -3499,17 +3500,15 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = url += `&tool=${encodeURIComponent(toolFilter)}`; } - const response = await apiFetch(url, { method: 'GET' }); - const result = await response.json().catch(() => ({})); - if (!response.ok) { - throw new Error(result.error || '获取监控数据失败'); - } + const { result, timeline, timelineError } = await fetchMonitorAndTimeline(url); if (mySeq !== monitorPanelFetchSeq) { return; } monitorState.executions = Array.isArray(result.executions) ? result.executions : []; monitorState.stats = result.stats || {}; + monitorState.timeline = timeline; + monitorState.timelineError = timelineError; monitorState.lastFetchedAt = new Date(); // 更新分页信息 @@ -3541,6 +3540,399 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = const MCP_STATS_TOP_N = 6; +const MCP_TIMELINE_RANGES = ['24h', '7d', '30d']; + +function getMcpMonitorTimelineRange() { + if (monitorState.timelineRange && MCP_TIMELINE_RANGES.includes(monitorState.timelineRange)) { + return monitorState.timelineRange; + } + const saved = localStorage.getItem('mcpMonitorTimelineRange'); + const range = MCP_TIMELINE_RANGES.includes(saved) ? saved : '7d'; + monitorState.timelineRange = range; + return range; +} + +async function fetchMonitorAndTimeline(monitorUrl) { + const range = getMcpMonitorTimelineRange(); + const [monitorResp, timelineResp] = await Promise.all([ + apiFetch(monitorUrl, { method: 'GET' }), + apiFetch(`/api/monitor/calls-timeline?range=${encodeURIComponent(range)}`, { method: 'GET' }) + ]); + const result = await monitorResp.json().catch(() => ({})); + if (!monitorResp.ok) { + throw new Error(result.error || '获取监控数据失败'); + } + let timeline = null; + let timelineError = null; + try { + const timelineJson = await timelineResp.json().catch(() => ({})); + if (timelineResp.ok) { + timeline = timelineJson; + } else { + timelineError = timelineJson.error || 'timeline failed'; + } + } catch (err) { + timelineError = err && err.message ? err.message : 'timeline failed'; + } + return { result, timeline, timelineError }; +} + +function formatMcpTimelineLabel(isoOrDate, rangeKey, locale) { + const d = isoOrDate instanceof Date ? isoOrDate : new Date(isoOrDate); + if (Number.isNaN(d.getTime())) return ''; + if (rangeKey === '24h') { + return d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }); + } + if (rangeKey === '30d') { + return d.toLocaleDateString(locale, { month: 'numeric', day: 'numeric' }); + } + return d.toLocaleString(locale, { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }); +} + +function buildMcpTimelineSvg(points, rangeKey) { + if (!Array.isArray(points) || points.length === 0) return ''; + const W = 400; + const H = 140; + const padL = 32; + const padR = 8; + const padT = 12; + const padB = 24; + const plotW = W - padL - padR; + const plotH = H - padT - padB; + 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 coords = points.map((p, i) => { + const x = padL + (points.length <= 1 ? plotW / 2 : (i / (points.length - 1)) * plotW); + const y = padT + plotH - ((p.total || 0) / maxVal) * plotH; + return { x, y, p, i }; + }); + + const linePath = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(2)} ${c.y.toFixed(2)}`).join(' '); + const baseY = padT + plotH; + const areaPath = `${linePath} L ${coords[coords.length - 1].x.toFixed(2)} ${baseY} L ${coords[0].x.toFixed(2)} ${baseY} Z`; + + let failPath = ''; + if (hasFailed) { + failPath = coords.map((c, i) => { + const fy = padT + plotH - ((c.p.failed || 0) / maxVal) * plotH; + return `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(2)} ${fy.toFixed(2)}`; + }).join(' '); + } + + let peakIdx = 0; + points.forEach((p, i) => { + if ((p.total || 0) >= (points[peakIdx].total || 0)) peakIdx = i; + }); + + const yTicks = [0, Math.ceil(maxVal / 2), maxVal]; + const yLines = yTicks.map((v) => { + const y = padT + plotH - (v / maxVal) * plotH; + const isBase = v === 0; + return `` + + `${v}`; + }).join(''); + + const tickIdx = points.length <= 2 + ? points.map((_, i) => i) + : [0, Math.floor((points.length - 1) / 2), points.length - 1]; + const xLabels = tickIdx.map((idx) => { + const c = coords[idx]; + const label = formatMcpTimelineLabel(c.p.t, rangeKey, locale); + return `${escapeHtml(label)}`; + }).join(''); + + const dots = coords.map((c) => { + const tipTime = formatMcpTimelineLabel(c.p.t, rangeKey, locale); + const isPeak = c.i === peakIdx && (c.p.total || 0) > 0; + const dotClass = 'mcp-stats-timeline-dot' + (isPeak ? ' mcp-stats-timeline-dot--peak' : ''); + return ``; + }).join(''); + + const peakC = coords[peakIdx]; + const peakMarker = (peakC.p.total || 0) > 0 + ? `` + : ''; + + return ``; +} + +let mcpTimelineEventsBound = false; +let mcpTimelineTooltipEl = null; + +function bindMcpStatsTimelineEvents() { + const root = document.getElementById('monitor-stats'); + if (!root) return; + + root.querySelectorAll('.mcp-stats-timeline__range').forEach((btn) => { + btn.onclick = function () { + const range = btn.getAttribute('data-range'); + if (range) setMcpMonitorTimelineRange(range); + }; + }); + + if (mcpTimelineEventsBound) return; + if (!mcpTimelineTooltipEl) { + mcpTimelineTooltipEl = document.createElement('div'); + mcpTimelineTooltipEl.className = 'mcp-stats-timeline-tooltip'; + mcpTimelineTooltipEl.setAttribute('role', 'tooltip'); + document.body.appendChild(mcpTimelineTooltipEl); + } + + root.addEventListener('mousemove', function (e) { + const dot = e.target.closest('.mcp-stats-timeline-dot'); + if (!dot || !mcpTimelineTooltipEl) { + root.querySelectorAll('.mcp-stats-timeline-dot.is-active').forEach((d) => d.classList.remove('is-active')); + mcpTimelineTooltipEl.style.display = 'none'; + return; + } + root.querySelectorAll('.mcp-stats-timeline-dot.is-active').forEach((d) => d.classList.remove('is-active')); + dot.classList.add('is-active'); + const time = dot.getAttribute('data-time') || ''; + const total = dot.getAttribute('data-total') || '0'; + const failed = dot.getAttribute('data-failed') || '0'; + const tip = mcpMonitorT('timelineTooltip', { time, total, failed }) + || `${time}:${total} 次(失败 ${failed})`; + mcpTimelineTooltipEl.textContent = tip; + mcpTimelineTooltipEl.style.display = 'block'; + mcpTimelineTooltipEl.style.left = `${e.clientX}px`; + mcpTimelineTooltipEl.style.top = `${e.clientY}px`; + }); + + root.addEventListener('mouseleave', function (e) { + if (!e.target.closest || !e.target.closest('.mcp-stats-combined__timeline, .mcp-stats-timeline')) return; + if (e.relatedTarget && root.contains(e.relatedTarget)) return; + root.querySelectorAll('.mcp-stats-timeline-dot.is-active').forEach((d) => d.classList.remove('is-active')); + if (mcpTimelineTooltipEl) mcpTimelineTooltipEl.style.display = 'none'; + }); + + mcpTimelineEventsBound = true; +} + +function getMcpTimelineRangeLabel(rangeKey) { + const key = rangeKey === '24h' ? 'timelineRange24h' : rangeKey === '30d' ? 'timelineRange30d' : 'timelineRange7d'; + return mcpMonitorT(key) || rangeKey; +} + +function syncMcpMonitorTimelineRangeUI(activeRange) { + const range = activeRange || getMcpMonitorTimelineRange(); + document.querySelectorAll('#monitor-stats .mcp-stats-timeline__range').forEach((btn) => { + const r = btn.getAttribute('data-range'); + const on = r === range; + btn.classList.toggle('is-active', on); + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + }); + const scopeBadge = document.querySelector('#monitor-stats .mcp-stats-scope-badge--timeline'); + if (scopeBadge) scopeBadge.textContent = getMcpTimelineRangeLabel(range); +} + +function renderMcpStatsScopeBadges(showTools, showTimeline) { + const parts = []; + if (showTools) { + const cumulative = mcpMonitorT('scopeCumulative') || '累计'; + parts.push(`${escapeHtml(cumulative)}`); + } + if (showTimeline) { + const range = getMcpMonitorTimelineRange(); + parts.push(`${escapeHtml(getMcpTimelineRangeLabel(range))}`); + } + if (!parts.length) return ''; + return `
${parts.join('')}
`; +} + +function buildTimelineSparseHint(points, timeline) { + if (!Array.isArray(points) || points.length < 4 || !timeline || !timeline.summary) return ''; + const summaryTotal = timeline.summary.totalCalls || 0; + const peak = timeline.summary.peak || 0; + if (summaryTotal === 0 || peak === 0) return ''; + + const nonZero = points.filter((p) => (p.total || 0) > 0).length; + const nonZeroRatio = nonZero / points.length; + let peakIdx = 0; + points.forEach((p, i) => { + if ((p.total || 0) >= (points[peakIdx].total || 0)) peakIdx = i; + }); + const peakNearEnd = peakIdx >= Math.floor(points.length * 0.8); + if (nonZeroRatio > 0.3 && !peakNearEnd) return ''; + + const rangeKey = timeline.range || getMcpMonitorTimelineRange(); + const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US'; + const peakTime = timeline.summary.peakAt + ? formatMcpTimelineLabel(timeline.summary.peakAt, rangeKey, locale) + : formatMcpTimelineLabel(points[peakIdx].t, rangeKey, locale); + return mcpMonitorT('timelineSparseHint', { peak, peakTime }) + || `该时段多数时间为 0,峰值 ${peak} 次出现在 ${peakTime}`; +} + +async function setMcpMonitorTimelineRange(range) { + if (!MCP_TIMELINE_RANGES.includes(range)) return; + localStorage.setItem('mcpMonitorTimelineRange', range); + monitorState.timelineRange = range; + monitorState.timelineError = null; + syncMcpMonitorTimelineRangeUI(range); + try { + const timelineResp = await apiFetch(`/api/monitor/calls-timeline?range=${encodeURIComponent(range)}`, { method: 'GET' }); + const timelineJson = await timelineResp.json().catch(() => ({})); + if (!timelineResp.ok) { + throw new Error(timelineJson.error || '加载趋势失败'); + } + monitorState.timeline = timelineJson; + const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); + if (timelineInner) { + timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError); + bindMcpStatsTimelineEvents(); + syncMcpMonitorTimelineRangeUI(range); + } else if (monitorState.stats && Object.keys(monitorState.stats).length > 0) { + renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt); + } + } catch (err) { + monitorState.timelineError = err.message || 'error'; + const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); + if (timelineInner) { + timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError); + bindMcpStatsTimelineEvents(); + syncMcpMonitorTimelineRangeUI(range); + } + } +} +window.setMcpMonitorTimelineRange = setMcpMonitorTimelineRange; + +function renderMcpStatsTimelineRangeButtons() { + const activeRange = getMcpMonitorTimelineRange(); + return MCP_TIMELINE_RANGES.map((r) => { + const labelKey = r === '24h' ? 'timelineRange24h' : r === '30d' ? 'timelineRange30d' : 'timelineRange7d'; + const label = mcpMonitorT(labelKey) || r; + return ``; + }).join(''); +} + +function renderMcpStatsTimelineBody(timeline, timelineError) { + const hint = mcpMonitorT('timelineHint') || '全部工具合计'; + + if (timelineError) { + const errText = mcpMonitorT('timelineLoadError') || '无法加载调用趋势'; + return `

${escapeHtml(errText)}:${escapeHtml(timelineError)}

`; + } + + const points = timeline && Array.isArray(timeline.points) ? timeline.points : []; + const summaryTotal = timeline && timeline.summary ? (timeline.summary.totalCalls || 0) : 0; + const peak = timeline && timeline.summary ? (timeline.summary.peak || 0) : 0; + const summaryText = mcpMonitorT('timelineSummary', { total: summaryTotal, peak }) + || `区间内 ${summaryTotal} 次 · 峰值 ${peak}`; + + if (points.length === 0 || summaryTotal === 0) { + const noData = mcpMonitorT('timelineNoData') || '该时段暂无调用'; + return `

${escapeHtml(noData)}

`; + } + + const rangeKey = timeline.range || getMcpMonitorTimelineRange(); + const chartSvg = buildMcpTimelineSvg(points, rangeKey); + const totalLegend = mcpMonitorT('timelineTotalLegend') || '总调用'; + const failLegend = mcpMonitorT('timelineFailedLegend') || '失败'; + const hasFailed = points.some((p) => (p.failed || 0) > 0); + const sparseHint = buildTimelineSparseHint(points, timeline); + const sparseHtml = sparseHint + ? `

${escapeHtml(sparseHint)}

` + : ''; + + return ` +

${escapeHtml(hint)} · ${escapeHtml(summaryText)}

+
${chartSvg}
+ ${sparseHtml} +
+ ${escapeHtml(totalLegend)} + ${hasFailed ? `${escapeHtml(failLegend)}` : ''} +
`; +} + +function renderMcpStatsCombinedSection(topTools, totals, activeToolFilter, timeline, timelineError, showTimeline) { + const statsTitle = mcpMonitorT('toolStatsTitle') || '工具统计'; + const timelineTitle = mcpMonitorT('timelineTitle') || '调用趋势'; + const statsHint = mcpMonitorT('toolStatsHint') || '点击色条或列表行筛选下方执行记录'; + const hasTools = topTools.length > 0; + + if (!hasTools && !showTimeline) return ''; + + const filterChip = activeToolFilter + ? ` + ${escapeHtml(mcpMonitorT('filterActive', { tool: activeToolFilter }) || `已筛选:${activeToolFilter}`)} + + ` + : ''; + + const rangeButtons = showTimeline + ? `
${renderMcpStatsTimelineRangeButtons()}
` + : ''; + + const panelTitle = showTimeline && hasTools + ? `${statsTitle} · ${timelineTitle}` + : (hasTools ? statsTitle : timelineTitle); + + const scopeBadges = renderMcpStatsScopeBadges(hasTools, showTimeline); + const metaHint = hasTools ? statsHint : ''; + + const timelineCol = showTimeline + ? `
+

${escapeHtml(timelineTitle)}

+
${renderMcpStatsTimelineBody(timeline, timelineError)}
+
` + : ''; + + let bodyMod = 'mcp-stats-combined__body'; + if (hasTools && showTimeline) bodyMod += ' mcp-stats-combined__body--full'; + else if (hasTools) bodyMod += ' mcp-stats-combined__body--tools'; + else bodyMod += ' mcp-stats-combined__body--timeline'; + + const mainBlock = hasTools + ? `
${renderMcpStatsToolsPanel(topTools, totals, activeToolFilter)}
` + : ''; + + return ` +
+
+
+

${escapeHtml(panelTitle)}

+
+ ${scopeBadges} + ${metaHint ? `

${escapeHtml(metaHint)}

` : ''} +
+
+
+ ${filterChip} + ${rangeButtons} +
+
+
+ ${mainBlock} + ${timelineCol} +
+
`; +} function mcpMonitorT(key, params) { if (typeof window.t !== 'function') return ''; @@ -3611,6 +4003,69 @@ function getMcpToolRateClass(rateNum) { } const MCP_STATS_DIST_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#14b8a6', '#ec4899']; +const MCP_STATS_CHART_MIN_PCT = 5; + +function buildMcpStatsChartSegments(topTools, totals, options = {}) { + const groupSmall = options.groupSmall !== false; + const minPct = options.minPct ?? MCP_STATS_CHART_MIN_PCT; + const othersLabel = mcpMonitorT('distOthers') || '其他工具'; + const topNTotal = topTools.reduce((s, t) => s + (t.totalCalls || 0), 0); + const otherCalls = Math.max(0, totals.total - topNTotal); + + const segments = []; + let bundledCalls = otherCalls; + + topTools.forEach((tool, i) => { + const calls = tool.totalCalls || 0; + if (calls <= 0 || totals.total <= 0) return; + const pct = (calls / totals.total) * 100; + if (groupSmall && pct < minPct) { + bundledCalls += calls; + return; + } + segments.push({ + color: MCP_STATS_DIST_COLORS[i % MCP_STATS_DIST_COLORS.length], + name: tool.toolName || '', + calls, + pct: pct.toFixed(1), + pctNum: pct, + isOthers: false, + colorIndex: i, + }); + }); + + if (bundledCalls > 0 && totals.total > 0) { + const pct = (bundledCalls / totals.total) * 100; + segments.push({ + color: '#cbd5e1', + name: othersLabel, + calls: bundledCalls, + pct: pct.toFixed(1), + pctNum: pct, + isOthers: true, + colorIndex: topTools.length, + }); + } + + let acc = 0; + return segments.map((s) => { + const start = acc; + acc += s.pctNum; + return { ...s, start, end: acc }; + }); +} + +function renderMcpStatsShareCell(sharePct, color) { + const width = Math.min(100, Math.max(0, parseFloat(sharePct) || 0)); + return ` +
+ ${escapeHtml(sharePct)}% + +
+ `; +} function mcpStatsDescribeDonutSegment(startPct, endPct, outerR, innerR) { if (endPct <= startPct) return ''; @@ -3675,21 +4130,37 @@ function previewMcpStatsDistCenter(panel, toolName, pct) { function setMcpStatsDistHover(toolName) { const panel = document.querySelector('.mcp-stats-dist-panel'); - if (!panel) return; - const esc = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(toolName) : toolName.replace(/"/g, '\\"'); - panel.querySelectorAll('.mcp-stats-dist-segment, .mcp-stats-dist-legend-item').forEach((el) => { - const t = el.getAttribute('data-tool-name') || ''; - const match = toolName && t === toolName; - el.classList.toggle('is-highlighted', !!match); - el.classList.toggle('is-dimmed', !!toolName && !match && t); - }); - if (toolName) { - const el = panel.querySelector(`[data-tool-name="${esc}"]`); - if (el) { - previewMcpStatsDistCenter(panel, toolName, el.getAttribute('data-pct') || ''); + const root = document.getElementById('monitor-stats'); + const esc = toolName && typeof CSS !== 'undefined' && CSS.escape + ? CSS.escape(toolName) + : (toolName || '').replace(/"/g, '\\"'); + + if (panel) { + panel.querySelectorAll('.mcp-stats-dist-segment, .mcp-stats-dist-legend-item').forEach((el) => { + const t = el.getAttribute('data-tool-name') || ''; + const match = toolName && t === toolName; + el.classList.toggle('is-highlighted', !!match); + el.classList.toggle('is-dimmed', !!toolName && !match && t); + }); + if (toolName) { + const el = panel.querySelector(`[data-tool-name="${esc}"]`); + if (el) { + previewMcpStatsDistCenter(panel, toolName, el.getAttribute('data-pct') || ''); + } + } else { + resetMcpStatsDistCenter(panel); } - } else { - resetMcpStatsDistCenter(panel); + } + + if (root) { + root.querySelectorAll( + 'tr.mcp-stats-tool-row[data-tool-name], .mcp-stats-tool-item[data-tool-name], .mcp-stats-proportion-seg[data-tool-name]' + ).forEach((el) => { + const t = el.getAttribute('data-tool-name') || ''; + const match = toolName && t === toolName; + el.classList.toggle('is-highlighted', !!match); + el.classList.toggle('is-dimmed', !!toolName && !match && t); + }); } } @@ -3703,48 +4174,17 @@ function handleMonitorStatsToolFilter(toolName) { filterMonitorByTool(toolName); } -function renderMcpStatsInsightPanel(topTools, totals, activeToolFilter = '') { +function renderMcpStatsInsightPanel(topTools, totals, activeToolFilter = '', options = {}) { + const embedded = !!options.embedded; const distTitle = mcpMonitorT('distTitle') || '调用分布'; - const distLegend = mcpMonitorT('distLegend') || '扇区面积为占全部调用比例'; - const distClickHint = mcpMonitorT('distClickHint') || '点击图例或扇区筛选执行记录'; + const distClickHint = mcpMonitorT('distClickHint') || '点击扇区筛选执行记录'; const distOthersTitle = mcpMonitorT('distOthersNoFilter') || '其他工具无法单独筛选'; 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), - isOthers: false, - }); - 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), - isOthers: true, - }); - } + const segments = buildMcpStatsChartSegments(topTools, totals, { groupSmall: embedded }); const segmentPathsHtml = segments.map((s) => { const pathD = mcpStatsDescribeDonutSegment(s.start, s.end, 48, 30); @@ -3766,12 +4206,12 @@ function renderMcpStatsInsightPanel(topTools, totals, activeToolFilter = '') { aria-label="${segAria}" />`; }).join(''); - const legendHtml = segments.map((s) => { + const legendHtml = embedded ? '' : segments.map((s) => { const isActive = !s.isOthers && activeToolFilter && activeToolFilter === s.name; const inner = ` ${escapeHtml(s.name)} - ${s.pct}%${escapeHtml(callsUnit(s.calls))}`; + ${s.pct}%`; if (s.isOthers) { return `
  • ${inner}
  • `; } @@ -3788,24 +4228,34 @@ function renderMcpStatsInsightPanel(topTools, totals, activeToolFilter = '') { `; }).join(''); - const centerLabel = `Top ${MCP_STATS_TOP_N}`; + const centerLabel = embedded ? (mcpMonitorT('distTitle') || '占比') : `Top ${MCP_STATS_TOP_N}`; const distHint = totals.total > 0 ? (mcpMonitorT('distTotalCalls', { n: totals.total }) || `共 ${totals.total} 次调用`) : ''; - return ` -
    + const bodyClass = embedded ? 'mcp-stats-dist-body mcp-stats-dist-body--chart-only' : 'mcp-stats-dist-body mcp-stats-dist-body--side'; + const legendBlock = legendHtml + ? `
      ${legendHtml}
    ` + : ''; + + const headerHtml = embedded + ? `
    ${escapeHtml(distTitle)}
    ` + : `

    ${escapeHtml(distTitle)}

    - ${escapeHtml(distLegend)} · ${escapeHtml(distClickHint)} + ${escapeHtml(distClickHint)}
    ${escapeHtml(distHint)} -
    -
    +
    `; + + return ` +
    + ${headerHtml} +
    @@ -3817,7 +4267,7 @@ function renderMcpStatsInsightPanel(topTools, totals, activeToolFilter = '') {
    -
      ${legendHtml}
    + ${legendBlock}
    `; @@ -3883,45 +4333,53 @@ function bindMonitorStatsPanelEvents() { clearMonitorToolFilter(); return; } - const distEl = e.target.closest('.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name]'); - if (distEl && distEl.getAttribute('data-is-others') !== '1') { - const tool = distEl.getAttribute('data-tool-name'); + const filterEl = e.target.closest( + '.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name], ' + + '.mcp-stats-proportion-seg[data-tool-name], .mcp-stats-tool-item[data-tool-name], tr.mcp-stats-tool-row[data-tool-name]' + ); + if (filterEl && filterEl.getAttribute('data-is-others') !== '1') { + const tool = filterEl.getAttribute('data-tool-name'); if (tool) { e.preventDefault(); handleMonitorStatsToolFilter(tool); } return; } - const row = e.target.closest('.mcp-stats-tool-row'); - if (!row) return; - const tool = row.getAttribute('data-tool-name'); - if (tool) { - e.preventDefault(); - handleMonitorStatsToolFilter(tool); - } }); root.addEventListener('keydown', function (e) { if (e.key !== 'Enter' && e.key !== ' ') return; - const distSeg = e.target.closest('.mcp-stats-dist-segment[data-tool-name]'); - if (!distSeg || distSeg.getAttribute('data-is-others') === '1') return; - const tool = distSeg.getAttribute('data-tool-name'); + const filterEl = e.target.closest( + '.mcp-stats-dist-segment[data-tool-name], .mcp-stats-proportion-seg[data-tool-name], ' + + '.mcp-stats-tool-item[data-tool-name], tr.mcp-stats-tool-row[data-tool-name]' + ); + if (!filterEl || filterEl.getAttribute('data-is-others') === '1') return; + const tool = filterEl.getAttribute('data-tool-name'); if (tool) { e.preventDefault(); handleMonitorStatsToolFilter(tool); } }); root.addEventListener('mouseover', function (e) { - const el = e.target.closest('.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name]'); + const el = e.target.closest( + '.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name], ' + + '.mcp-stats-proportion-seg[data-tool-name], .mcp-stats-tool-item[data-tool-name], tr.mcp-stats-tool-row[data-tool-name]' + ); if (!el || el.getAttribute('data-is-others') === '1') return; const tool = el.getAttribute('data-tool-name'); if (tool) setMcpStatsDistHover(tool); }); root.addEventListener('mouseout', function (e) { - const el = e.target.closest('.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name]'); + const el = e.target.closest( + '.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name], ' + + '.mcp-stats-proportion-seg[data-tool-name], .mcp-stats-tool-item[data-tool-name], tr.mcp-stats-tool-row[data-tool-name]' + ); if (!el) return; const related = e.relatedTarget; const next = related && related.closest - ? related.closest('.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name]') + ? related.closest( + '.mcp-stats-dist-segment[data-tool-name], .mcp-stats-dist-legend-item[data-tool-name], ' + + '.mcp-stats-proportion-seg[data-tool-name], .mcp-stats-tool-item[data-tool-name], tr.mcp-stats-tool-row[data-tool-name]' + ) : null; if (next) return; setMcpStatsDistHover(''); @@ -3929,6 +4387,255 @@ function bindMonitorStatsPanelEvents() { monitorStatsPanelEventsBound = true; } +function renderMcpStatsMetricsBar(totals, successRate, rateTone, rateSubText, lastCallText) { + const totalCallsLabel = mcpMonitorT('totalCalls') || '总调用次数'; + const successRateLabel = mcpMonitorT('successRate') || '成功率'; + const lastCallLabel = mcpMonitorT('lastCall') || '最近一次调用'; + const successPill = mcpMonitorT('successCount', { n: totals.success }) || `成功 ${totals.success}`; + const failedPill = mcpMonitorT('failedCount', { n: totals.failed }) || `失败 ${totals.failed}`; + + return ` +
    +
    + +
    + ${escapeHtml(totalCallsLabel)} + ${totals.total} +
    + ${escapeHtml(successPill)} + ${escapeHtml(failedPill)} +
    +
    +
    +
    + +
    + ${escapeHtml(successRateLabel)} + ${successRate}% + ${escapeHtml(rateSubText)} +
    +
    +
    + +
    + ${escapeHtml(lastCallLabel)} + +
    +
    +
    `; +} + +function renderMcpStatsToolTable(topTools, totals, activeToolFilter = '') { + const colTool = mcpMonitorT('columnTool') || '工具'; + const colCalls = mcpMonitorT('columnCalls') || '调用'; + const colShare = mcpMonitorT('columnShare') || '占比'; + const colRate = mcpMonitorT('columnSuccessRate') || '成功率'; + const unknownToolLabel = mcpMonitorT('unknownTool') || '未知工具'; + + let rowsHtml = ''; + 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 sharePct = totals.total > 0 ? ((total / totals.total) * 100).toFixed(1) : '0.0'; + const dotColor = MCP_STATS_DIST_COLORS[index % MCP_STATS_DIST_COLORS.length]; + const isActive = activeToolFilter && activeToolFilter === name; + const rateClass = getMcpToolRateClass(toolRateNum); + const rankClass = index === 0 ? ' rank-1' : index === 1 ? ' rank-2' : index === 2 ? ' rank-3' : ''; + const rowAria = mcpMonitorT('toolRowAriaLabel', { name, total, rate: toolRate }) + || `${name},${total} 次调用,成功率 ${toolRate}%`; + rowsHtml += ` + + ${index + 1} + + + ${escapeHtml(name)} + + ${total} + ${sharePct}% + + ${toolRate}% + ${failed > 0 ? `${escapeHtml(mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`)}` : ''} + + `; + }); + + return ` + + + + + + + + + + + ${rowsHtml} +
    #${escapeHtml(colTool)}${escapeHtml(colCalls)}${escapeHtml(colRate)}
    `; +} + +/** MCP 合并面板左侧:堆叠占比条 + 工具排行列表(无饼图/表格套娃) */ +function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') { + const segments = buildMcpStatsChartSegments(topTools, totals, { groupSmall: false }); + const topNTotal = topTools.reduce((s, t) => s + (t.totalCalls || 0), 0); + const topNSharePct = totals.total > 0 ? ((topNTotal / totals.total) * 100).toFixed(1) : '0.0'; + const caption = mcpMonitorT('rankingSummary', { n: MCP_STATS_TOP_N, pct: topNSharePct, total: totals.total }) + || `Top ${MCP_STATS_TOP_N} 占 ${topNSharePct}% · 共 ${totals.total} 次`; + const unknownToolLabel = mcpMonitorT('unknownTool') || '未知工具'; + const colTool = mcpMonitorT('columnTool') || '工具'; + const colCalls = mcpMonitorT('columnCalls') || '调用'; + const colShare = mcpMonitorT('columnShare') || '占比'; + const colRate = mcpMonitorT('columnSuccessRate') || '成功率'; + const distAria = mcpMonitorT('distTitle') || '调用分布'; + + const stackedHtml = segments.map((s) => { + const isActive = !s.isOthers && activeToolFilter && activeToolFilter === s.name; + const title = `${s.name} · ${s.pct}% · ${s.calls}`; + if (s.isOthers) { + return ``; + } + const segAria = mcpMonitorT('distSegmentAria', { name: s.name, pct: s.pct, calls: s.calls }) + || `${s.name},占 ${s.pct}%,${s.calls} 次`; + return ``; + }).join(''); + + const maxCalls = Math.max(1, ...topTools.map((t) => t.totalCalls || 0)); + const listHtml = topTools.map((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 sharePct = totals.total > 0 ? ((total / totals.total) * 100).toFixed(1) : '0.0'; + const color = MCP_STATS_DIST_COLORS[index % MCP_STATS_DIST_COLORS.length]; + const barPct = maxCalls > 0 ? ((total / maxCalls) * 100).toFixed(1) : '0'; + const isActive = activeToolFilter && activeToolFilter === name; + const rateClass = getMcpToolRateClass(toolRateNum); + const rankClass = index === 0 ? ' rank-1' : index === 1 ? ' rank-2' : index === 2 ? ' rank-3' : ''; + const rowAria = mcpMonitorT('toolRowAriaLabel', { name, total, rate: toolRate }) + || `${name},${total} 次,成功率 ${toolRate}%`; + const failNote = failed > 0 + ? `${escapeHtml(mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`)}` + : ''; + return `
  • + ${index + 1} + +
    + ${escapeHtml(name)} + +
    +
    + + ${total} + ${toolRate}%${failNote} +
    +
  • `; + }).join(''); + + return ` +
    +
    + +

    + ${escapeHtml(mcpMonitorT('scopeCumulative') || '累计')} + ${escapeHtml(caption)} +

    +
    + +
      ${listHtml}
    +
    `; +} + +function renderMcpStatsChartAside(topTools, totals, activeToolFilter = '') { + const distTitle = mcpMonitorT('distTitle') || '调用分布'; + const distClickHint = mcpMonitorT('distClickHint') || '点击扇区筛选'; + const top6ShareLabel = mcpMonitorT('distTop6Share', { n: MCP_STATS_TOP_N }) || `Top ${MCP_STATS_TOP_N} 占全部调用`; + const topNTotal = topTools.reduce((s, t) => s + (t.totalCalls || 0), 0); + const top6SharePct = totals.total > 0 ? ((topNTotal / totals.total) * 100).toFixed(1) : '0.0'; + const centerLabel = `Top ${MCP_STATS_TOP_N}`; + + const segments = buildMcpStatsChartSegments(topTools, totals, { groupSmall: true }); + const segmentPathsHtml = segments.map((s) => { + const pathD = mcpStatsDescribeDonutSegment(s.start, s.end, 48, 30); + if (!pathD) return ''; + const isActive = !s.isOthers && activeToolFilter && activeToolFilter === s.name; + const segAria = s.isOthers + ? escapeHtml(s.name) + : escapeHtml(mcpMonitorT('distSegmentAria', { name: s.name, pct: s.pct, calls: s.calls }) + || `${s.name},占 ${s.pct}%,${s.calls} 次`); + return ``; + }).join(''); + + return ` +
    +

    ${escapeHtml(distTitle)}

    +
    + + ${segmentPathsHtml} + + +
    +

    ${escapeHtml(distClickHint)}

    +
    `; +} + +function renderMcpStatsDetailSection(topTools, totals, activeToolFilter = '', timeline = null, timelineError = null) { + const showTimeline = timeline != null || !!timelineError; + return renderMcpStatsCombinedSection(topTools, totals, activeToolFilter, timeline, timelineError, showTimeline); +} + +/** @deprecated 保留供其他页面;MCP 监控主面板请用 renderMcpStatsToolTable */ +function renderMcpStatsToolRanking(topTools, totals, activeToolFilter = '', options = {}) { + if (options.bare || options.embedded) { + return renderMcpStatsToolTable(topTools, totals, activeToolFilter); + } + return renderMcpStatsDetailSection(topTools, totals, activeToolFilter); +} + function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { const container = document.getElementById('monitor-stats'); if (!container) { @@ -3936,7 +4643,8 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { } const entries = normalizeMonitorStatsEntries(statsMap); - if (entries.length === 0) { + const showTimeline = monitorState.timeline != null || !!monitorState.timelineError; + if (entries.length === 0 && !showTimeline) { const noStats = mcpMonitorT('noStatsData') || '暂无统计数据'; container.innerHTML = '
    ' + escapeHtml(noStats) + '
    '; const subtitle = document.getElementById('monitor-stats-subtitle'); @@ -3966,12 +4674,6 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { ? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale) : String(totals.lastCallTime)) : noCallsYet; - 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') || '失败率偏高'; @@ -3986,120 +4688,24 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { .sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0)) .slice(0, MCP_STATS_TOP_N); - 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 += ` -
  • - -
  • - `; - }); - - const clearFilterBtn = activeToolFilter - ? `` - : ''; - + const showCombined = showTimeline || topTools.length > 0; 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, activeToolFilter)} -
    -
    - ` : ''} + ${renderMcpStatsMetricsBar(totals, successRate, rateTone, rateSubText, lastCallText)} + ${showCombined ? renderMcpStatsCombinedSection( + topTools, + totals, + activeToolFilter, + monitorState.timeline, + monitorState.timelineError, + showTimeline + ) : ''}
    `; container.innerHTML = html; bindMonitorStatsPanelEvents(); + bindMcpStatsTimelineEvents(); if (toolFilterEl && activeToolFilter) { toolFilterEl.classList.add('is-filter-active'); } else if (toolFilterEl) { diff --git a/web/templates/index.html b/web/templates/index.html index 4e5337b4..400014f3 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1114,20 +1114,22 @@ -
    +
    -
    -
    -

    执行统计

    - -
    -
    加载中...