Skip to content

Commit 5937489

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 24f5200 commit 5937489

19 files changed

Lines changed: 630 additions & 504 deletions

history.md

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

33
## Unreleased
44

5+
- ui(proxy-stats): **config tab — align the two group boxes + center the input cluster** — the `Strategy` (2 rows) and `Execution Parameters` (6 rows) group boxes were different heights because `.configGrid` used `align-items: start`; switched to `stretch` so the shorter box grows to match the taller one. Inside each box the label+input cluster was pushed to one edge of the wide `1120px` panel (`.row` was a 2-track grid that spanned the full box width); each `.row` is now a `flex` with `justify-content: center`, a fixed `150px` right-aligned label column (labels line up down the rows regardless of per-label length) and a capped `320px` input cell, so the whole label+input cluster sits centered in the background box with symmetric side whitespace. The `@media (max-width: 640px)` fallback restacks to label-above-input full-width. Pure CSS, no logic change; `.configGrid` keeps `minmax(0,1fr) minmax(0,1fr)` so the layout regression guard still passes.
6+
7+
- 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`). Follow-up polish: chart/helper colors now use the app's real semantic tokens (`--color-primary/success/warning/error` — the branch previously referenced nonexistent `--accent-*/--ok/--warn/--danger` vars that always fell back to light-theme hex), `BarChart` gains a grouped-series `legend` prop, an `ariaLabel` accessible name, direct value labels on wide-enough single-series bars, and reduced-motion guards, KPI cards get a `--stat-accent` left accent bar, and loading/empty states share a `.centerState` class with reserved min-height (no CLS).
8+
59
## 1.7.5 (2026-07-18)
610

711
- 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 +575,3 @@
571575
### 0.0.1 (2026-02-17) — 初始版本
572576

573577
- 拦截并记录 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;
@@ -18,23 +19,48 @@
1819
.bar { transition: none; }
1920
}
2021
.axisLine {
21-
stroke: var(--border-primary, #d9d9d9);
22+
stroke: var(--border-primary, #e0e0e0);
2223
stroke-width: 1;
2324
}
2425
.gridLine {
25-
stroke: var(--border-secondary, #f0f0f0);
26+
stroke: var(--border-secondary, #ebebeb);
2627
stroke-width: 1;
2728
stroke-dasharray: 2 3;
2829
}
2930
.vLabel {
30-
fill: var(--text-secondary, #8c8c8c);
31+
fill: var(--text-secondary, #333);
3132
font-size: 11px;
3233
}
3334
.hLabel {
34-
fill: var(--text-secondary, #8c8c8c);
35+
fill: var(--text-secondary, #333);
3536
font-size: 11px;
3637
}
3738
.vValue {
38-
fill: var(--text-primary, #262626);
39+
fill: var(--text-primary, #1a1a1a);
3940
font-size: 10px;
4041
}
42+
/* Value label rendered INSIDE a (near-full-width) bar to avoid overflowing the
43+
plot area. White on the colored bar for AA contrast against primary/success. */
44+
.vValueIn {
45+
fill: #fff;
46+
font-size: 10px;
47+
}
48+
.legend {
49+
display: flex;
50+
gap: 12px;
51+
flex-wrap: wrap;
52+
margin-bottom: 4px;
53+
font-size: 12px;
54+
color: var(--text-secondary, #333);
55+
}
56+
.legendItem {
57+
display: inline-flex;
58+
align-items: center;
59+
gap: 5px;
60+
}
61+
.legendSwatch {
62+
width: 10px;
63+
height: 10px;
64+
border-radius: 2px;
65+
display: inline-block;
66+
}

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)