Skip to content

Commit bb9ac64

Browse files
committed
feat(proxy-stats): stats-first ProxyStatsModal redesign (tabbed Config|Stats)
ProxyStatsModal shell with a Segmented [Config|Stats] tab control (Config default); hamburger retry-config now opens this unified ui.proxyStats.title panel on every viewport, the standalone RetryConfigModal wrapper is removed so RetryConfigForm is the only settings surface, and config uses a grouped two-column layout (Strategy / Execution Parameters) in a widened 1120px container with mobile fallbacks. Body is a sectioned ProxyStatsDashboard (Range Overview / Availability / Retry Analysis / Duration / By Dimension / Recent Errors) rendering the dual upstream-vs-downstream availability grouped bars, retry-count histogram, and retry-burden bucket bars via the new BarChart; each dimension table gains a Dominant Fail Code column. RetryStatsHelpers renamed .js -> .jsx (contains JSX). The side-by-side UnifiedProxyRetryPage + ProxyStatsPage are removed. i18n: removed dead viewRetryConfig, added tabConfig/tabStats/groupStrategy/groupExecution x18 locales. Tests: expanded retry-config-layout.test.js as a regression guard; modal-mask reverted to the parent-modal mask model (ProxyStatsModal/RetryConfigModal must not consume BLUR_MASK_STYLE). history.md updated.
1 parent 1deaf11 commit bb9ac64

19 files changed

Lines changed: 590 additions & 499 deletions

history.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- ui(proxy-stats): **retry stats UI redesign — unified tabbed Config|Stats modal** — the hamburger `retry-config` entry now opens a single `ProxyStatsModal` with an antd `Segmented` `[Config | Stats]` switch (Config default, the surface most users open this for); the previously divergent desktop (stats-first modal) and mobile (config-only drawer) paths collapse into one shell on every viewport, and the standalone `RetryConfigModal` wrapper is removed so `RetryConfigForm` is the only settings surface. The Stats tab body is a sectioned `ProxyStatsDashboard` (Range Overview / Availability / Retry Analysis / Duration / By Dimension / Recent Errors) rendered with a new dependency-free `BarChart.jsx` (pure SVG/CSS, ResizeObserver-responsive — no chart library added): dual upstream-vs-downstream availability grouped bars (the gap = requests the retry engine rescued), a retry-count histogram, and a retry-burden bucket chart. The Config tab uses a grouped two-column layout (`Strategy` / `Execution Parameters`) inside a widened `1120px` container with mobile fallbacks. Backend `aggregateRecords` (`server/lib/proxy-stats.js`) gains a global `retryBurden` (5 buckets: 0 / 1-5 / 6-20 / 21-50 / >50) and per-bucket `retryCodeCounts` (top-5, sorted desc) / `dominantFailStatus` / `dominantFailCount` on byModel/byPath/byProfile, so each dimension table shows a "Dominant Fail Code" column; `emptyStats()` returns `null` (not `0`) for latency fields so "no data" is distinguishable from a genuine sub-ms value. New `RetryStatsHelpers.jsx` (`fmtMs`/`statusColor`/`availColor`/`dominantFailCell`/`burdenBucketLabel`). New i18n keys across all 18 locales: `ui.proxyStats.{tabConfig,tabStats,upstreamVsDownstream,retryBurden,dominantFail}` and `ui.retryConfig.{groupStrategy,groupExecution}`; the dead `ui.proxyStats.viewRetryConfig` key is removed. The side-by-side `UnifiedProxyRetryPage` + `ProxyStatsPage` are removed. `ProxyStatsDashboard.fetchData` uses a per-request `AbortController` (supersede-on-retrigger + abort-on-unmount) so stale polls can't clobber newer state. New `test/proxy-stats.test.js` pins the bucket boundaries, per-bucket top-5 cap, and null dominant-fail contract; `test/retry-config-layout.test.js` guards the tabbed layout; `test/modal-mask.test.js` reflects the parent-modal mask model (`ProxyStatsModal.jsx`/`RetryConfigModal.jsx` must not consume `BLUR_MASK_STYLE`).
6+
57
## 1.7.5 (2026-07-18)
68

79
- ui(proxy): **fuse retry config and stats into a unified split-page**`RetryConfigForm` extracted from RetryConfigModal as inline component; `UnifiedProxyRetryPage` with left config / right stats panels (independent scroll); recent records table filtered to errors only. Shared `isProxyMode()` utility eliminates duplicated proxy-detection logic. Proxy stats toolbar/sidebar buttons removed; unified page now reachable via hamburger menu. P1–P2 code-review fixes applied.
@@ -571,4 +573,3 @@
571573
### 0.0.1 (2026-02-17) — 初始版本
572574

573575
- 拦截并记录 Claude API 请求/响应
574-

src/App.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { BLUR_MASK_STYLE } from './utils/modalMask';
2626
import { isProxyMode } from './utils/isProxyMode';
2727

2828
// 代理重试面板(配置+统计融合):懒加载
29-
const UnifiedProxyRetryPage = lazy(() => import('./components/proxy-stats/UnifiedProxyRetryPage'));
29+
const ProxyStatsModal = lazy(() => import('./components/proxy-stats/ProxyStatsModal'));
3030

3131
// Lightweight error boundary for the unified proxy page modal.
3232
class ProxyPageErrorBoundary extends React.Component {
@@ -749,7 +749,7 @@ class App extends AppBase {
749749
>
750750
<ProxyPageErrorBoundary>
751751
<Suspense fallback={<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}><Spin size="large" /></div>}>
752-
<UnifiedProxyRetryPage
752+
<ProxyStatsModal
753753
retryConfig={this.state.retryConfig}
754754
retryDefaults={this.state.retryDefaults}
755755
onRetryConfigChange={this.handleRetryConfigChange}

src/AppBase.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2090,7 +2090,7 @@ class AppBase extends React.Component {
20902090
// 代理重试配置保存:POST /api/retry-config(服务端写 retry-config.json + watchFile 热刷新 + SSE 回推)。
20912091
// 乐观更新本地 retryConfig(SSE retry_config 事件会再确认一次);失败回滚并提示。
20922092
// 返回 POST 的 Promise:成功 resolve(SSE retry_config 会刷新 state);
2093-
// 失败则回滚 state + message.error 后 reject,供调用方(RetryConfigModal)据以决定是否关闭/提示成功。
2093+
// 失败则回滚 state + message.error 后 reject,供调用方(ProxyStatsModal/RetryConfigForm)据以决定是否关闭/提示成功。
20942094
handleRetryConfigChange = (config) => {
20952095
const prev = this.state.retryConfig;
20962096
this.setState({ retryConfig: config });

src/components/charts/BarChart.module.css

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
/* BarChart — pure SVG bar chart. CSS vars only, no !important (CLAUDE.md). */
1+
/* BarChart — pure SVG bar chart. CSS vars only, no !important (CLAUDE.md).
2+
Uses the app's REAL semantic tokens (global.css :root). */
23
.wrap {
34
width: 100%;
45
position: relative;
@@ -15,23 +16,48 @@
1516
opacity: 0.75;
1617
}
1718
.axisLine {
18-
stroke: var(--border-primary, #d9d9d9);
19+
stroke: var(--border-primary, #e0e0e0);
1920
stroke-width: 1;
2021
}
2122
.gridLine {
22-
stroke: var(--border-secondary, #f0f0f0);
23+
stroke: var(--border-secondary, #ebebeb);
2324
stroke-width: 1;
2425
stroke-dasharray: 2 3;
2526
}
2627
.vLabel {
27-
fill: var(--text-secondary, #8c8c8c);
28+
fill: var(--text-secondary, #333);
2829
font-size: 11px;
2930
}
3031
.hLabel {
31-
fill: var(--text-secondary, #8c8c8c);
32+
fill: var(--text-secondary, #333);
3233
font-size: 11px;
3334
}
3435
.vValue {
35-
fill: var(--text-primary, #262626);
36+
fill: var(--text-primary, #1a1a1a);
3637
font-size: 10px;
3738
}
39+
/* Value label rendered INSIDE a (near-full-width) bar to avoid overflowing the
40+
plot area. White on the colored bar for AA contrast against primary/success. */
41+
.vValueIn {
42+
fill: #fff;
43+
font-size: 10px;
44+
}
45+
.legend {
46+
display: flex;
47+
gap: 12px;
48+
flex-wrap: wrap;
49+
margin-bottom: 4px;
50+
font-size: 12px;
51+
color: var(--text-secondary, #333);
52+
}
53+
.legendItem {
54+
display: inline-flex;
55+
align-items: center;
56+
gap: 5px;
57+
}
58+
.legendSwatch {
59+
width: 10px;
60+
height: 10px;
61+
border-radius: 2px;
62+
display: inline-block;
63+
}

src/components/dashboard/AppHeader.jsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import ProjectPrefsManagerModal from '../settings/ProjectPrefsManagerModal';
3232
import PluginModal from '../settings/PluginModal';
3333
import ProcessModal from '../settings/ProcessModal';
3434
import ProxyModal, { profileDisplayModel } from '../settings/ProxyModal';
35-
import RetryConfigModal from '../settings/RetryConfigModal';
3635
import SystemTextModal from '../settings/SystemTextModal';
3736
import VoicePackSettings from '../settings/VoicePackSettings';
3837
import ProjectAliasEditor from '../settings/ProjectAliasEditor';
@@ -159,14 +158,7 @@ class AppHeader extends React.Component {
159158
...(isLocalLog ? [] : [{ key: 'messaging', icon: <DialogueIcon />, label: t('ui.messaging.menu'), onClick: () => this.setState({ messagingModalVisible: true, messagingInitialTool: null }) }]),
160159
{ key: 'proxy-switch', icon: <SwapOutlined />, label: t('ui.proxySwitch'), onClick: () => this.setState({ proxyModalVisible: true }) },
161160
// Hidden on official subscription: retry orchestration targets proxy gateways only
162-
...(this._isProxyMode() ? [{ key: 'retry-config', icon: <ThunderboltOutlined />, label: t('ui.retryConfig.title'), onClick: () => {
163-
// PC: open unified page (config + stats); mobile: open standalone config drawer
164-
if (isMobile) {
165-
this.setState({ retryConfigModalVisible: true });
166-
} else {
167-
this.props.onToggleProxyStats?.();
168-
}
169-
} }] : []),
161+
...(this._isProxyMode() ? [{ key: 'retry-config', icon: <ThunderboltOutlined />, label: t('ui.proxyStats.title'), onClick: () => this.props.onToggleProxyStats?.() }] : []),
170162
{ key: 'edit-system-prompt', icon: <EditOutlined />, label: t('ui.expert.systemText'), onClick: () => this.setState({ systemTextModalVisible: true }), dividerAfter: true },
171163
{ key: 'project-stats', icon: <BarChartOutlined />, label: t('ui.projectStats'), onClick: this.handleShowProjectStats },
172164
...(viewMode === 'raw' ? [{ key: 'global-settings', icon: <SettingOutlined />, label: t('ui.globalSettings'), onClick: () => this.setState({ globalSettingsVisible: true }) }] : []),
@@ -2209,13 +2201,6 @@ class AppHeader extends React.Component {
22092201
defaultConfig={this.props.defaultConfig}
22102202
onProxyProfileChange={this.props.onProxyProfileChange}
22112203
/>
2212-
<RetryConfigModal
2213-
open={this.state.retryConfigModalVisible}
2214-
onClose={() => this.setState({ retryConfigModalVisible: false })}
2215-
config={this.props.retryConfig}
2216-
defaults={this.props.retryDefaults}
2217-
onConfigChange={this.props.onRetryConfigChange}
2218-
/>
22192204
<SystemTextModal
22202205
open={this.state.systemTextModalVisible}
22212206
onClose={() => this.setState({ systemTextModalVisible: false })}

0 commit comments

Comments
 (0)