Skip to content

feat: add user savings estimates - #6499

Open
RHZHZ wants to merge 7 commits into
QuantumNous:mainfrom
RHZHZ:codex/user-savings-estimates
Open

feat: add user savings estimates#6499
RHZHZ wants to merge 7 commits into
QuantumNous:mainfrom
RHZHZ:codex/user-savings-estimates

Conversation

@RHZHZ

@RHZHZ RHZHZ commented Jul 28, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

新增面向用户的节省金额估算功能,以同一次请求的实际消耗和管理员确认的官方价格快照为依据,展示累计节省、覆盖率和时间趋势。

  • 后端增加节省估算配置、官方价格覆盖、日志快照、历史日志重算以及汇总/趋势 API。
  • 估算仅处理能够可靠复算的计费模式;固定价格、未知附加倍率、工具附加费或不完整快照会明确跳过,避免展示失真的节省金额。
  • 每条请求的节省值按 max(official_quota - actual_quota, 0) 计算并写入消费日志,汇总时保持逐请求非负语义。
  • 前端增加概览卡片、节省趋势图、用量日志详情和可视化/JSON 管理设置,并补齐 7 种语言。
  • 历史记录仅在重算出的实际配额与原日志配额一致时纳入估算,保证旧数据口径可验证。

AI assistance disclosure: 本次实现、移植和验证由 AI 在 RHZHZ 的指导下协助完成;提交者已结合官方最新代码处理计费安全、工具附加费和多语言兼容性。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • 无直接关联 Issue;已搜索现有 Issues 和 PRs,未发现相同功能提交。

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 此 PR 不属于 Bug fix。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

  • go test ./service -run Savings -count=1
  • go test ./setting/savings_setting ./controller ./router -count=1
  • go test ./model -run Savings -count=1
  • bun run typecheck
  • bun run build
  • bun test src/features/dashboard/lib/__tests__/savings-i18n.test.ts src/features/dashboard/lib/__tests__/savings-chart.test.ts src/features/dashboard/lib/__tests__/savings-time-range.test.ts(14 passed)
  • 受影响文件 oxlintoxfmt --check 通过。
  • bun run i18n:sync:7 种语言均为 0 missing、0 extras、0 untranslated。
  • git diff --check 通过。

补充:全量 bun run copyright:check 仅报告官方基线中未改动的 src/features/channels/lib/channel-field-update.ts;本 PR 新增及修改的前端源码版权头均符合当前脚本要求。

Summary by CodeRabbit

  • New Features
    • Added authenticated APIs for user savings summary, trend, and lifetime; dashboard now includes a savings trend chart and lifetime display (including wallet).
    • Added usage-log “Savings Estimate” details and new savings-estimate configuration with official-pricing snapshot/fingerprint handling.
    • Introduced “Savings lifetime” backfill with start/pause/resume/retry and new system-task UI/statuses.
  • Bug Fixes
    • Improved handling of invalid/missing savings time range, granularity, and UTC offset with localized 400 responses.
    • Savings estimate metadata is hidden in usage logs when the feature is disabled.
  • Documentation
    • Added end-to-end design and remediation docs for savings estimates, trend, and lifetime.
  • Tests
    • Added extensive backend/service and frontend chart/i18n/unit tests.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds configurable savings estimation from usage logs, historical reconstruction, summary and trend APIs, a pausable lifetime savings backfill system task with aggregation worker, dashboard cards and charts, wallet and usage-log details, audit events, localized strings, and design documentation.

Changes

Savings estimates, trend reporting, and lifetime backfill

Layer / File(s) Summary
Configuration and administration
setting/savings_setting/*, model/option.go, model/pricing.go, controller/option.go, controller/audit.go, web/src/features/system-settings/...
Adds normalized savings settings, official-price handling, option persistence and validation, audit actions, pricing lookup caching, and visual/JSON settings editors.
Estimation and aggregation pipeline
service/savings_estimate.go, service/text_quota.go, model/savings_log.go, model/log.go, service/savings_estimate_test.go
Attaches savings snapshots to consume logs, rebuilds historical estimates, matches official prices, computes quotas and coverage, aggregates summaries and trend buckets, and validates edge cases.
Authenticated reporting API
controller/savings.go, router/api-router.go, i18n/*
Adds authenticated summary and trend endpoints with timestamp, granularity, UTC-offset, normalized-window validation, rate limiting, and localized errors.
Lifetime savings backfill and pausable system tasks
model/savings_lifetime.go, model/savings_log.go, model/system_task.go, service/savings_lifetime_backfill.go, service/savings_lifetime_worker.go, service/system_task.go, controller/system_task.go, main.go, model/main.go
Adds lifetime event/daily/total aggregation models, ClickHouse cursor pagination, a resumable backfill system task, background aggregation/compensation worker, and pause/resume/retry state transitions.
Dashboard summary and trend presentation
web/src/features/dashboard/*
Adds typed savings APIs, rolling summary cards, lazy-loaded trend charts, CNY conversion, partial/empty/error states, accessibility tables, and chart utility tests.
Lifetime backfill and wallet UI
web/src/features/system-settings/models/savings-lifetime-backfill.tsx, web/src/features/system-info/components/system-tasks-panel.tsx, web/src/features/wallet/components/wallet-stats-card.tsx, web/src/features/system-settings/api.ts, web/src/features/system-settings/types.ts
Adds an admin backfill control panel, extended system-task status/progress handling, wallet lifetime savings display, and typed backfill task API/types.
Usage-log details and localization
web/src/features/usage-logs/*, web/src/i18n/locales/*
Adds typed savings metadata, conditional usage-log visibility, savings detail sections, audit text, and translations for savings, pricing, coverage, limits, backfill states, and trend states across seven locales.
Design documentation
docs/user-savings-estimate-design.md, docs/user-savings-trend-design.md, docs/user-savings-review-remediation.md
Defines estimation, pricing snapshots, historical rebuilds, aggregation contracts, lifetime backfill, trend bucket behavior, frontend states, testing, rollout, remediation, and scope boundaries.

Estimated code review effort: 5 (Critical) | ~150 minutes

Possibly related PRs

Poem

I’m a rabbit with a ledger neat,
Counting saved coins from logs of wheat.
Prices hop, charts bloom bright,
Backfills pause and resume at night.
Settings twinkle, audits sing—
Savings grows on every wing.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding user savings estimates and related feature work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RHZHZ RHZHZ changed the title [codex] add user savings estimates feat: add user savings estimates Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (10)
service/savings_estimate_test.go (1)

481-493: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Parameter model shadows the imported model package.

Harmless here, but it will break compilation the moment someone references model.X inside this helper. Rename to modelName.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/savings_estimate_test.go` around lines 481 - 493, Rename the
savingsRelayInfo parameter from model to modelName and update its
OriginModelName assignment accordingly, avoiding shadowing of the imported model
package.
model/log.go (1)

117-136: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: hoist the setting lookup out of the loop.

savings_setting.ShowOnUsageLogs() takes an RWMutex read lock on every iteration. Reading it once before the loop is both cheaper and makes the redaction consistent across the whole page.

♻️ Proposed refactor
 func formatUserLogs(logs []*Log, startIdx int) {
+	showSavings := savings_setting.ShowOnUsageLogs()
 	for i := range logs {
 		...
-			if !savings_setting.ShowOnUsageLogs() {
+			if !showSavings {
 				delete(otherMap, "savings_estimate")
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/log.go` around lines 117 - 136, In formatUserLogs, read
savings_setting.ShowOnUsageLogs() once before iterating over logs, store the
result, and use that value for savings_estimate redaction inside the loop.
Preserve the existing field-removal behavior and display-ID assignment.
service/savings_estimate.go (2)

640-657: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confusing nil-map fallback branch.

price, ok := localPrices[candidate] is executed first and then discarded when localPrices == nil, which reads as a bug even though it works. An explicit branch is clearer.

♻️ Proposed refactor
 		for _, candidate := range candidates {
-			price, ok := localPrices[candidate]
-			if localPrices == nil {
-				if localPricing, found := model.GetPricingByModel(candidate); found {
-					price = savingsOfficialPriceFromPricing(localPricing, priceSnapshotAt)
-					ok = true
-				}
-			}
+			var (
+				price savings_setting.OfficialPrice
+				ok    bool
+			)
+			if localPrices != nil {
+				price, ok = localPrices[candidate]
+			} else if localPricing, found := model.GetPricingByModel(candidate); found {
+				price = savingsOfficialPriceFromPricing(localPricing, priceSnapshotAt)
+				ok = true
+			}
 			if !ok {
 				continue
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/savings_estimate.go` around lines 640 - 657, Clarify the candidate
pricing lookup in the LocalPricingOfficialConfirmed branch by explicitly
separating the localPrices-nil fallback from the normal localPrices[candidate]
lookup. Update the loop around finalizeSavingsOfficialPrice so each branch sets
price and ok without performing a lookup whose result is immediately discarded.

916-929: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Undocumented ||| guard.

strings.Contains(expr, "|||") silently rejects an expression form with no explanation. Add a short comment (or reference the rule in pkg/billingexpr/expr.md) so the intent survives future edits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/savings_estimate.go` around lines 916 - 929, Document the purpose of
the strings.Contains(expr, "|||") guard in calculateSavingsTieredTextQuota by
adding a concise comment or referencing the applicable rule in
pkg/billingexpr/expr.md. Keep the existing rejection behavior unchanged.

Source: Coding guidelines

model/savings_log.go (1)

13-33: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Index coverage for the (user_id, type, created_at) filter.

Log indexes user_id, (user_id, id) and (created_at, type), but there is no composite index matching this predicate, so a 31-day window for a heavy user will scan a large slice of logs twice (count + fetch). Consider a (user_id, created_at) (or (user_id, type, created_at)) index, and note that the ClickHouse log backend has different ordering characteristics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/savings_log.go` around lines 13 - 33, The CountUserSavingsConsumeLogs
and GetUserSavingsConsumeLogs queries need composite index coverage for their
user, type, and created_at filters. Add the appropriate database index on Log,
preferably (user_id, type, created_at), and account for the ClickHouse backend’s
differing ordering characteristics without changing the existing query behavior.
controller/savings.go (1)

14-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Four duplicated hand-rolled 400 responses; prefer the shared helper + i18n.

The rest of the codebase returns errors through common.ApiError/message helpers, and user-facing strings elsewhere go through i18n.T(c, ...). These blocks hardcode Chinese and duplicate the same shape four times.

♻️ Suggested consolidation
+func abortSavingsBadRequest(c *gin.Context, message string) {
+	c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": message})
+}

Also applies to: 40-64

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/savings.go` around lines 14 - 29, Replace the duplicated
400-response blocks in the savings controller, including the handlers around
parseSavingsTimeRange and NormalizeSavingsSummaryWindow, with the established
common.ApiError/message helper flow. Route user-facing messages through
i18n.T(c, ...) and preserve each branch’s existing validation error semantics
while consolidating the repeated response shape across all four cases.
setting/savings_setting/config.go (1)

195-208: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Trimming keys can silently drop a config entry.

If the admin JSON contains both "gpt-4o" and " gpt-4o", the trimmed insert overwrites the other entry non-deterministically (map iteration order). Also, re-inserting the trimmed key during range may re-visit it; normalization is idempotent so it's harmless today, but it is fragile.

Consider building a new map instead of mutating in place.

♻️ Proposed refactor
-	for rawModel, price := range s.OfficialPrices {
-		model := strings.TrimSpace(rawModel)
-		if model == "" {
-			delete(s.OfficialPrices, rawModel)
-			continue
-		}
-		if model != rawModel {
-			delete(s.OfficialPrices, rawModel)
-		}
-		price.SourceURL = publicSourceURL(price.SourceURL)
-		price.Source = strings.TrimSpace(price.Source)
-		price.BillingMode = strings.TrimSpace(price.BillingMode)
-		s.OfficialPrices[model] = price
-	}
+	normalized := make(map[string]OfficialPrice, len(s.OfficialPrices))
+	for rawModel, price := range s.OfficialPrices {
+		model := strings.TrimSpace(rawModel)
+		if model == "" {
+			continue
+		}
+		price.SourceURL = publicSourceURL(price.SourceURL)
+		price.Source = strings.TrimSpace(price.Source)
+		price.BillingMode = strings.TrimSpace(price.BillingMode)
+		normalized[model] = price
+	}
+	s.OfficialPrices = normalized
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setting/savings_setting/config.go` around lines 195 - 208, Update the
OfficialPrices normalization loop to build a separate map rather than deleting
and re-inserting entries while ranging over s.OfficialPrices. Trim each model
key and normalize its price fields before adding it to the new map, explicitly
handle collisions between keys that normalize to the same value, then replace
s.OfficialPrices with the resulting map.
setting/savings_setting/config_test.go (1)

9-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use assert for independent, non-fatal checks.

All assertions use require, so in TestUpdateSettingSanitizesOfficialSourceURL a Source mismatch aborts before checking SourceURL, and in TestUpdateSettingUsesLocalPricingAndLegacyRebuildDefaults a failed LocalPricingOfficialConfirmed check hides the RebuildLegacyLogs result. These are independent, non-fatal property checks.

As per coding guidelines, "New or substantially rewritten Go backend tests must use testify/require for setup and fatal assertions and testify/assert for non-fatal checks."

♻️ Proposed fix
 import (
 	"testing"

+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@
 	setting := GetSetting()
 	price, ok := setting.OfficialPrices["gpt-4o-mini"]
 	require.True(t, ok)
-	require.Equal(t, "OpenAI", price.Source)
-	require.Equal(t, "https://example.com/pricing?model=gpt", price.SourceURL)
+	assert.Equal(t, "OpenAI", price.Source)
+	assert.Equal(t, "https://example.com/pricing?model=gpt", price.SourceURL)
 }
@@
 	setting := GetSetting()
-	require.True(t, setting.LocalPricingOfficialConfirmed)
-	require.True(t, setting.RebuildLegacyLogs)
+	assert.True(t, setting.LocalPricingOfficialConfirmed)
+	assert.True(t, setting.RebuildLegacyLogs)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setting/savings_setting/config_test.go` around lines 9 - 46, Update the
independent property checks in TestUpdateSettingSanitizesOfficialSourceURL and
TestUpdateSettingUsesLocalPricingAndLegacyRebuildDefaults to use testify/assert
instead of require, while keeping require for setup, cleanup, and other fatal
assertions.

Source: Coding guidelines

web/src/features/system-settings/models/savings-estimate-settings.tsx (1)

76-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit tests for parseSavingsSetting/formatSavingsSetting.

These are pure functions with meaningful normalization logic (type coercion, legacy-field stripping, default fallback) but ship without a test file.

As per coding guidelines, "新增功能、缺陷修复或行为修改必须同步新增或更新测试" (new features must be accompanied by new/updated tests).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/system-settings/models/savings-estimate-settings.tsx` around
lines 76 - 129, Add unit tests covering parseSavingsSetting and
formatSavingsSetting, including valid JSON normalization, boolean and numeric
fallback behavior, legacy-field removal, invalid JSON handling, and formatting
preservation for unparseable values. Use the existing DEFAULT_SETTING
expectations to assert normalized defaults and verify valid settings are
serialized with the expected indentation.

Source: Coding guidelines

web/src/features/dashboard/lib/savings-chart.ts (1)

27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the shared TimeGranularity union here.

DashboardTimeGranularity duplicates TimeGranularity from @/lib/time, including the same values (hour, day, week). Import TimeGranularity to avoid local drift from the canonical granularity type.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/dashboard/lib/savings-chart.ts` at line 27, Replace the
local DashboardTimeGranularity declaration with the shared TimeGranularity type
from "`@/lib/time`", importing it where needed and updating references in
savings-chart.ts to use the canonical union.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@service/savings_estimate.go`:
- Around line 363-380: In service/savings_estimate.go lines 363-380, update
loadUserSavingsRows to cache the computed per-user summary/trend result for a
short TTL, or replace repeated row processing with database-side aggregation;
preserve existing limits and return values. In router/api-router.go lines 91-92,
apply middleware.SearchRateLimit() or CriticalRateLimit() to both
/savings/summary and /savings/trend, matching other scan-heavy read routes.

In `@web/src/features/dashboard/components/models/savings-trend-chart.tsx`:
- Around line 440-457: Mark the decorative Info icon as aria-hidden="true"
within the labelled TooltipTrigger button in
web/src/features/dashboard/components/models/savings-trend-chart.tsx lines
440-457, and apply the same change to the decorative ArrowUpRight icon inside
the labelled TooltipTrigger/Link in
web/src/features/dashboard/components/overview/summary-cards.tsx lines 440-456.

In `@web/src/features/dashboard/components/overview/summary-cards.tsx`:
- Around line 240-248: Update the savingsAmountDisplay calculation near
savingsSummary to use the shared currency configuration values already used by
the savings trend chart, preferably currency.quotaPerUnit and
currency.usdExchangeRate, instead of status?.quota_per_unit and
status?.usd_exchange_rate. Keep the existing null handling and
formatSavingsQuotaAsCNY call unchanged otherwise.

In `@web/src/i18n/locales/ru.json`:
- Line 703: Update the Russian translation for “Calculate estimated savings
using official model prices.” to use “оценочную экономию” instead of “экономию,”
preserving the estimated qualifier while keeping the rest of the translation
unchanged.
- Line 3681: Update the Russian translation for the “Recalculate legacy usage
logs” locale key from the imperfective “Пересчитывать” to the perfective
“Пересчитать,” preserving the existing key and punctuation.
- Around line 1128-1129: Update the Russian translations for “Covered request
actual cost” and “Covered requests” in the locale entries to use
“охваченных”/“Охваченные” consistently, replacing “учтённых”/“Учтённые” while
preserving the existing sentence structure.

In `@web/src/i18n/locales/vi.json`:
- Around line 3104-3105: Update the Vietnamese translations for "Official Price
Updated" and "Official price updated {{time}}" to use explicit completed-state
wording, such as "Giá chính thức đã được cập nhật" and "Giá chính thức được cập
nhật lúc {{time}}", while preserving the existing interpolation placeholder.

In `@web/src/i18n/locales/zh-TW.json`:
- Line 4235: Update the translation value for “Show the savings summary and
trend on the user dashboard.” in the zh-TW locale to use `用戶` instead of `使用者`,
preserving the rest of the translation unchanged.
- Line 4790: Use consistent terminology for locally configured pricing in both
entries: update web/src/i18n/locales/zh-TW.json lines 4790-4790 and 5046-5046,
replacing 本機 with 本地 (or the same agreed equivalent) in the translations for
local marketplace prices and official pricing.

---

Nitpick comments:
In `@controller/savings.go`:
- Around line 14-29: Replace the duplicated 400-response blocks in the savings
controller, including the handlers around parseSavingsTimeRange and
NormalizeSavingsSummaryWindow, with the established common.ApiError/message
helper flow. Route user-facing messages through i18n.T(c, ...) and preserve each
branch’s existing validation error semantics while consolidating the repeated
response shape across all four cases.

In `@model/log.go`:
- Around line 117-136: In formatUserLogs, read savings_setting.ShowOnUsageLogs()
once before iterating over logs, store the result, and use that value for
savings_estimate redaction inside the loop. Preserve the existing field-removal
behavior and display-ID assignment.

In `@model/savings_log.go`:
- Around line 13-33: The CountUserSavingsConsumeLogs and
GetUserSavingsConsumeLogs queries need composite index coverage for their user,
type, and created_at filters. Add the appropriate database index on Log,
preferably (user_id, type, created_at), and account for the ClickHouse backend’s
differing ordering characteristics without changing the existing query behavior.

In `@service/savings_estimate_test.go`:
- Around line 481-493: Rename the savingsRelayInfo parameter from model to
modelName and update its OriginModelName assignment accordingly, avoiding
shadowing of the imported model package.

In `@service/savings_estimate.go`:
- Around line 640-657: Clarify the candidate pricing lookup in the
LocalPricingOfficialConfirmed branch by explicitly separating the
localPrices-nil fallback from the normal localPrices[candidate] lookup. Update
the loop around finalizeSavingsOfficialPrice so each branch sets price and ok
without performing a lookup whose result is immediately discarded.
- Around line 916-929: Document the purpose of the strings.Contains(expr, "|||")
guard in calculateSavingsTieredTextQuota by adding a concise comment or
referencing the applicable rule in pkg/billingexpr/expr.md. Keep the existing
rejection behavior unchanged.

In `@setting/savings_setting/config_test.go`:
- Around line 9-46: Update the independent property checks in
TestUpdateSettingSanitizesOfficialSourceURL and
TestUpdateSettingUsesLocalPricingAndLegacyRebuildDefaults to use testify/assert
instead of require, while keeping require for setup, cleanup, and other fatal
assertions.

In `@setting/savings_setting/config.go`:
- Around line 195-208: Update the OfficialPrices normalization loop to build a
separate map rather than deleting and re-inserting entries while ranging over
s.OfficialPrices. Trim each model key and normalize its price fields before
adding it to the new map, explicitly handle collisions between keys that
normalize to the same value, then replace s.OfficialPrices with the resulting
map.

In `@web/src/features/dashboard/lib/savings-chart.ts`:
- Line 27: Replace the local DashboardTimeGranularity declaration with the
shared TimeGranularity type from "`@/lib/time`", importing it where needed and
updating references in savings-chart.ts to use the canonical union.

In `@web/src/features/system-settings/models/savings-estimate-settings.tsx`:
- Around line 76-129: Add unit tests covering parseSavingsSetting and
formatSavingsSetting, including valid JSON normalization, boolean and numeric
fallback behavior, legacy-field removal, invalid JSON handling, and formatting
preservation for unparseable values. Use the existing DEFAULT_SETTING
expectations to assert normalized defaults and verify valid settings are
serialized with the expected indentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a0e65be1-d737-40b5-a949-6334bec4b80b

📥 Commits

Reviewing files that changed from the base of the PR and between c3db414 and 9f5dd2e.

📒 Files selected for processing (43)
  • controller/audit.go
  • controller/option.go
  • controller/savings.go
  • docs/user-savings-estimate-design.md
  • docs/user-savings-trend-design.md
  • model/log.go
  • model/log_format_test.go
  • model/option.go
  • model/pricing.go
  • model/savings_log.go
  • router/api-router.go
  • service/savings_estimate.go
  • service/savings_estimate_test.go
  • service/text_quota.go
  • setting/savings_setting/config.go
  • setting/savings_setting/config_test.go
  • web/src/features/dashboard/api.ts
  • web/src/features/dashboard/components/models/savings-trend-chart.tsx
  • web/src/features/dashboard/components/overview/summary-cards.tsx
  • web/src/features/dashboard/index.tsx
  • web/src/features/dashboard/lib/__tests__/savings-chart.test.ts
  • web/src/features/dashboard/lib/__tests__/savings-i18n.test.ts
  • web/src/features/dashboard/lib/__tests__/savings-time-range.test.ts
  • web/src/features/dashboard/lib/savings-chart.ts
  • web/src/features/dashboard/lib/savings.ts
  • web/src/features/dashboard/types.ts
  • web/src/features/models/components/drawers/model-mutate-drawer.tsx
  • web/src/features/system-settings/billing/index.tsx
  • web/src/features/system-settings/billing/section-registry.tsx
  • web/src/features/system-settings/models/index.tsx
  • web/src/features/system-settings/models/ratio-settings-card.tsx
  • web/src/features/system-settings/models/savings-estimate-settings.tsx
  • web/src/features/system-settings/types.ts
  • web/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/src/features/usage-logs/lib/format.ts
  • web/src/features/usage-logs/types.ts
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json

Comment thread service/savings_estimate.go
Comment thread web/src/features/dashboard/components/overview/summary-cards.tsx
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/vi.json Outdated
Comment thread web/src/i18n/locales/zh-TW.json Outdated
Comment thread web/src/i18n/locales/zh-TW.json Outdated
- harden savings aggregation with rate limits, bounded settings, caching, and indexes
- localize validation errors and align dashboard currency, accessibility, and translations
- add regression tests and document remediation decisions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
web/src/features/system-settings/models/savings-estimate-settings.tsx (3)

323-330: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hide decorative icons from assistive technology.

Code2 and Save are next to visible button labels, so they are decorative. Add aria-hidden="true" to comply with the accessibility guideline.

Also applies to: 343-353

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/system-settings/models/savings-estimate-settings.tsx` around
lines 323 - 330, Update the Code2 and Save icon components in the mode-change
buttons handled by handleModeChange to include aria-hidden="true", keeping the
existing visible button labels and behavior unchanged.

Source: Coding guidelines


121-134: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize JSON-mode settings before saving.

JSON validation only checks the root object, while Lines 122-123 send jsonText unchanged. This bypasses parseSavingsSetting for values such as official_prices: [], invalid booleans, and fractional limits. The existing parser contract and its tests expect these values to be normalized before persistence.

Proposed fix
-    const currentText =
-      editMode === 'visual' ? JSON.stringify(setting) : jsonText
-    if (editMode === 'json' && !validation.valid) {
+    const currentSetting =
+      editMode === 'visual' ? setting : parseSavingsSetting(jsonText)
+    if (!currentSetting) {
       toast.error(validationMessage)
       return
     }
+    const currentText = JSON.stringify(currentSetting)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/system-settings/models/savings-estimate-settings.tsx` around
lines 121 - 134, Update handleSave to parse and normalize JSON-mode input with
the existing parseSavingsSetting contract before persistence, rather than
sending jsonText unchanged. Ensure values such as official_prices, booleans, and
fractional limits undergo the parser’s normalization, while preserving
visual-mode behavior and the existing no-changes check.

136-139: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle mutateAsync errors in handleSave.

useUpdateOption already calls onError, but await updateOption.mutateAsync(...) still rejects, and handleSave has no local try/catch, so save failures can surface as unhandled promise rejections. Wrap the mutation await and keep the success toast after the await path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/system-settings/models/savings-estimate-settings.tsx` around
lines 136 - 139, Update handleSave to wrap the updateOption.mutateAsync call in
a local try/catch, handling rejected saves without unhandled promise rejections.
Keep the success toast only after the await completes successfully, and preserve
the existing useUpdateOption onError behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/user-savings-review-remediation.md`:
- Line 63: 修正该 Markdown 表格中 N4 行的 `|||`
内容,避免被解析为额外列;请改写该单元格或转义竖线字符,并确保整行仍与表头列数一致、通过 MD056。

In `@web/src/features/system-settings/models/savings-estimate-setting.ts`:
- Around line 86-94: Update the validation condition in the savings-setting
normalization logic to reject values below 1 before flooring, while retaining
the existing finite-number and fallback behavior. Ensure values such as 0.5 use
DEFAULT_SAVINGS_SETTING rather than becoming 0, and add a regression test
covering this case.

---

Outside diff comments:
In `@web/src/features/system-settings/models/savings-estimate-settings.tsx`:
- Around line 323-330: Update the Code2 and Save icon components in the
mode-change buttons handled by handleModeChange to include aria-hidden="true",
keeping the existing visible button labels and behavior unchanged.
- Around line 121-134: Update handleSave to parse and normalize JSON-mode input
with the existing parseSavingsSetting contract before persistence, rather than
sending jsonText unchanged. Ensure values such as official_prices, booleans, and
fractional limits undergo the parser’s normalization, while preserving
visual-mode behavior and the existing no-changes check.
- Around line 136-139: Update handleSave to wrap the updateOption.mutateAsync
call in a local try/catch, handling rejected saves without unhandled promise
rejections. Keep the success toast only after the await completes successfully,
and preserve the existing useUpdateOption onError behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e64bf6c0-9cf3-47de-a505-a8fd286d6552

📥 Commits

Reviewing files that changed from the base of the PR and between 9f5dd2e and cf35b78.

📒 Files selected for processing (25)
  • controller/savings.go
  • controller/savings_test.go
  • docs/user-savings-review-remediation.md
  • i18n/keys.go
  • i18n/locales/en.yaml
  • i18n/locales/zh-CN.yaml
  • i18n/locales/zh-TW.yaml
  • model/log.go
  • router/api-router.go
  • service/savings_estimate.go
  • service/savings_estimate_test.go
  • setting/savings_setting/config.go
  • setting/savings_setting/config_test.go
  • web/src/features/dashboard/components/models/savings-trend-chart.tsx
  • web/src/features/dashboard/components/overview/summary-cards.tsx
  • web/src/features/dashboard/lib/__tests__/savings-i18n.test.ts
  • web/src/features/dashboard/lib/__tests__/savings-time-range.test.ts
  • web/src/features/dashboard/lib/savings-chart.ts
  • web/src/features/dashboard/lib/savings.ts
  • web/src/features/system-settings/models/__tests__/savings-estimate-setting.test.ts
  • web/src/features/system-settings/models/savings-estimate-setting.ts
  • web/src/features/system-settings/models/savings-estimate-settings.tsx
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
🚧 Files skipped from review as they are similar to previous changes (12)
  • router/api-router.go
  • web/src/features/dashboard/lib/tests/savings-time-range.test.ts
  • web/src/features/dashboard/lib/savings-chart.ts
  • controller/savings.go
  • web/src/features/dashboard/lib/savings.ts
  • web/src/features/dashboard/components/models/savings-trend-chart.tsx
  • setting/savings_setting/config.go
  • web/src/i18n/locales/vi.json
  • web/src/features/dashboard/components/overview/summary-cards.tsx
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/ru.json
  • service/savings_estimate.go

Comment thread docs/user-savings-review-remediation.md Outdated
Comment thread web/src/features/system-settings/models/savings-estimate-setting.ts
- validate numeric limits before flooring and normalize JSON mode
- catch rejected saves and hide decorative setting icons
- update the remediation document for follow-up review
@RHZHZ
RHZHZ marked this pull request as ready for review July 28, 2026 08:48
- aggregate savings into idempotent event, daily, and lifetime totals
- add resumable historical backfill and visual administration controls
- show frozen RMB savings on dashboard and wallet with localized status

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/src/i18n/locales/en.json (1)

45-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid a hard-coded plural for a count of one.

With count = 1, this renders as “1 historical requests…”. Use count-safe wording such as “Recalculated historical requests at current official prices: {{count}}”, or add singular/plural variants consistently across locales and callers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/i18n/locales/en.json` at line 45, Update the locale entry for
“{{count}} historical requests recalculated at current official prices” to use
count-safe wording that remains grammatically correct when count is 1,
preferably by placing the count after a neutral phrase. Keep the interpolation
and corresponding translation key consistent with existing callers.
web/src/i18n/locales/ru.json (1)

5092-5092: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate “overrides” as “переопределения.”

исключений means exceptions, not configuration overrides, and may mislead administrators about how official_prices is used. Prefer official_prices нужен только для переопределений.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/i18n/locales/ru.json` at line 5092, Update the Russian translation
for the key containing “official_prices” to translate “overrides” as
“переопределений” rather than “исключений,” preserving the rest of the localized
message.
🧹 Nitpick comments (7)
model/savings_log_test.go (1)

58-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Note the fidelity limit of simulating ClickHouse over SQLite.

These tests force common.SetLogDatabaseType(common.DatabaseTypeClickHouse) while the connection is in-memory SQLite, so they validate the composite-keyset predicate shape but not ClickHouse-specific SQL acceptance or ordering semantics. Worth a short comment in the test so a future reader does not assume ClickHouse coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/savings_log_test.go` around lines 58 - 82, Add a brief comment above
TestGetSavingsLifetimeLogBatchUsesClickHouseCompositeKeyset explaining that it
uses SQLite while forcing the ClickHouse database type, so it only validates the
composite-keyset predicate shape and not ClickHouse-specific SQL acceptance or
ordering semantics.
service/savings_lifetime_backfill.go (2)

204-229: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Per-user summary does a global pending-events count.

CountPendingSavingsLifetimeEvents() scans the whole events table (not scoped to userID) on every dashboard/wallet summary request, and GetLatestSystemTask adds another query. Consider caching the pending count / backfill status for a few seconds in memory since it is global state, or deriving IsComplete from the task row alone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/savings_lifetime_backfill.go` around lines 204 - 229, The per-user
summary currently performs a global CountPendingSavingsLifetimeEvents query on
every request. Update the summary flow around GetLatestSystemTask and
CountPendingSavingsLifetimeEvents to avoid this repeated table scan, preferably
by deriving IsComplete from the global backfill task status or reusing a
short-lived in-memory cache for the global pending count and status.

400-414: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Compute amountMicros only when the frozen value is unusable.

The fallback conversion runs unconditionally and can fail the whole batch (error return at Line 402) even when a frozen SavingsCNYMicros is present and would have been used. Reordering also avoids the redundant decimal work per row.

♻️ Prefer the frozen snapshot first
 	estimate := result.Estimate
-	amountMicros, err := savingsLifetimeAmountMicros(int64(estimate.SavingsQuota), payload.QuotaPerUnit, payload.USDCNYRateMicros)
-	if err != nil {
-		return model.SavingsLifetimeEvent{}, false, err
-	}
+	var amountMicros int64
+	frozenUsed := false
 	if estimate.SavingsCNYMicros != "" && estimate.QuotaPerUnit > 0 && estimate.USDCNYRateMicros > 0 {
 		if frozen, parseErr := strconv.ParseInt(estimate.SavingsCNYMicros, 10, 64); parseErr == nil {
 			amountMicros = frozen
+			frozenUsed = true
 			event.QuotaPerUnitSnapshot = estimate.QuotaPerUnit
 			event.USDCNYRateMicros = estimate.USDCNYRateMicros
 		}
 	}
-	if event.QuotaPerUnitSnapshot == 0 {
+	if !frozenUsed {
+		converted, err := savingsLifetimeAmountMicros(int64(estimate.SavingsQuota), payload.QuotaPerUnit, payload.USDCNYRateMicros)
+		if err != nil {
+			return model.SavingsLifetimeEvent{}, false, err
+		}
+		amountMicros = converted
 		event.QuotaPerUnitSnapshot = payload.QuotaPerUnit
 		event.USDCNYRateMicros = payload.USDCNYRateMicros
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/savings_lifetime_backfill.go` around lines 400 - 414, Update the
conversion flow around savingsLifetimeAmountMicros to parse and validate the
frozen estimate.SavingsCNYMicros snapshot first; when it is usable, assign
amountMicros and snapshot fields without calling the fallback conversion. Only
invoke savingsLifetimeAmountMicros and return its error when the frozen value is
absent or unusable, while preserving the existing payload snapshot fallback.
web/src/features/system-settings/models/savings-lifetime-backfill.tsx (2)

67-279: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider splitting this component.

The file is ~280 lines with four mutations plus presentation. Extracting a useSavingsLifetimeBackfill() hook for the query/mutations and a small progress/summary subcomponent would keep it within the size guidance.

As per coding guidelines: "组件文件超过约 200 行时,应考虑拆分子组件或提取自定义 Hook."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/system-settings/models/savings-lifetime-backfill.tsx` around
lines 67 - 279, The SavingsLifetimeBackfill component combines query/mutation
state management with presentation and exceeds the recommended size. Extract the
query and four mutation workflows into a useSavingsLifetimeBackfill hook, and
move the task progress/summary markup into a focused subcomponent; keep
SavingsLifetimeBackfill responsible for layout and action controls while
preserving the existing behavior and rendered values.

Source: Coding guidelines


85-89: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Invalidate dependent queries after the mutations succeed.

All four mutations only write the backfill task into its own cache entry. The savings summary shown on the dashboard/wallet (and any other query derived from backfill status) keeps serving stale data until its own refetch. Add an invalidateQueries for the related keys alongside the setQueryData.

As per coding guidelines: "React Query 中数据获取使用 useQuery、变更使用 useMutation;每个查询必须有唯一且层级一致的数组形式 queryKey,成功后使相关 query 失效."

Also applies to: 107-107, 121-121, 135-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/system-settings/models/savings-lifetime-backfill.tsx` around
lines 85 - 89, Update all four mutation success handlers in the savings lifetime
backfill flow to invalidate the related savings summary and other
backfill-derived query keys after calling setQueryData with the successful task
response. Use the existing array-form query key constants and React Query
invalidateQueries pattern, while preserving the current cache update behavior.

Source: Coding guidelines

model/log.go (1)

61-64: 🩺 Stability & Availability | 🔵 Trivial

New composite index on logs — verify rollout plan for large tables.

Adding idx_logs_user_type_created_id (user_id, type, created_at, id) to the Log model is a sensible design for the new savings-log queries, but building this index via AutoMigrate on an already large logs table can be a long-running, potentially locking DDL operation, especially on MySQL 5.7 (the guideline's supported floor) where online DDL support is more limited than 8.0. Confirm the deployment runbook accounts for this (e.g., pre-flight index creation with ALGORITHM=INPLACE, LOCK=NONE on MySQL, or running the migration during a low-traffic window) rather than relying purely on automatic migration at startup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/log.go` around lines 61 - 64, The new idx_logs_user_type_created_id
index on Log requires an explicit large-table rollout plan rather than relying
solely on AutoMigrate at startup. Update the migration or deployment flow to
support pre-creating the index with MySQL-compatible online DDL where available,
or ensure it runs during a documented low-traffic window, including
compatibility with MySQL 5.7.
web/src/features/dashboard/components/overview/summary-cards.tsx (1)

190-203: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Consider extracting the savings/lifetime summary block into its own component or hook.

This file now bundles two extra data-fetching queries (savingsSummaryQuery, savingsLifetimeQuery) plus ~90 lines of derived display-string logic and a matching JSX block, on top of the pre-existing usage/runway summary. The whole reviewed file is well past the ~200-line guideline threshold for splitting.

♻️ Suggested direction

Extract a useSavingsSummaryCard() hook (owning both queries + all derived display strings) and a <SavingsSummaryCard /> presentational component, then have SummaryCards simply render it. This keeps SummaryCards focused on the usage/runway concern and isolates the savings feature for easier future changes.

As per coding guidelines: "组件文件超过约 200 行时,应考虑拆分子组件或提取自定义 Hook。"

Also applies to: 252-338, 467-558

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/dashboard/components/overview/summary-cards.tsx` around
lines 190 - 203, Extract the savings-related logic from SummaryCards into a
dedicated useSavingsSummaryCard hook containing savingsSummaryQuery,
savingsLifetimeQuery, and their derived display strings, plus a
SavingsSummaryCard presentational component for the matching JSX block. Update
SummaryCards to use the new component so it remains focused on the usage/runway
summary while preserving the existing savings data and display behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/user-savings-estimate-design.md`:
- Around line 649-656: Update the wallet-page display rule in the documented
recharge flow to explicitly allow partial backfill values only with “counted so
far” semantics, such as “已统计节省” or “累计统计中”. Ensure partial results are never
presented as final lifetime savings, and retain the requirement that completed,
reliable totals use the final cumulative-savings presentation.

In `@model/savings_lifetime.go`:
- Around line 34-55: Bound the integrity-check work performed by
checkSQLiteDatabaseIntegrity so request-path callers such as
CheckSavingsLifetimeSQLiteIntegrity do not run an unbounded full-database scan;
use a bounded quick-check form that stops after the first error while preserving
the existing error parsing and reporting behavior.

In `@web/src/features/system-info/components/system-tasks-panel.tsx`:
- Line 246: Update the hasActiveTasks logic in the system tasks panel so paused
tasks are excluded from the “Auto-refreshing” indicator. Track whether any task
is actively polling using isPollingStatus, while preserving the existing task
activity detection separately.

In `@web/src/features/system-settings/api.ts`:
- Around line 75-120: Explicitly annotate the return types of
startSavingsLifetimeBackfill, getSavingsLifetimeBackfill,
pauseSavingsLifetimeBackfill, resumeSavingsLifetimeBackfill, and
retrySavingsLifetimeBackfill with their exact Promise response types in
web/src/features/system-settings/api.ts:75-120. Also annotate isActiveStatus and
isPollingStatus with boolean in
web/src/features/system-info/components/system-tasks-panel.tsx:104-117; no other
changes are required.

In `@web/src/features/wallet/components/wallet-stats-card.tsx`:
- Around line 123-129: Update both formatSavingsCNYMicros calls in the lifetime
savings rendering branch to pass the active i18n.language locale, ensuring
formatting follows the selected UI language for complete and in-progress
savings.

In `@web/src/i18n/locales/ru.json`:
- Line 267: Update the Russian translation for “Aggregate new usage into a
frozen lifetime savings total.” to explicitly convey that new usage is accounted
for in the frozen total lifetime savings, preserving both aggregation and total
semantics.
- Line 1586: Update the Russian translation for “Enable and save lifetime
savings before starting a backfill.” so it explicitly refers to saving the
lifetime-savings setting, using the reviewer’s preferred wording and preserving
the instruction to enable it before the backfill.

In `@web/src/i18n/locales/zh.json`:
- Line 1749: Update the zh.json translation for the “Estimated: {{count}}” key
to use neutral wording such as “已估算:{{count}}” or “估算:{{count}}”, removing the
“successfully” claim while preserving the count placeholder.

---

Outside diff comments:
In `@web/src/i18n/locales/en.json`:
- Line 45: Update the locale entry for “{{count}} historical requests
recalculated at current official prices” to use count-safe wording that remains
grammatically correct when count is 1, preferably by placing the count after a
neutral phrase. Keep the interpolation and corresponding translation key
consistent with existing callers.

In `@web/src/i18n/locales/ru.json`:
- Line 5092: Update the Russian translation for the key containing
“official_prices” to translate “overrides” as “переопределений” rather than
“исключений,” preserving the rest of the localized message.

---

Nitpick comments:
In `@model/log.go`:
- Around line 61-64: The new idx_logs_user_type_created_id index on Log requires
an explicit large-table rollout plan rather than relying solely on AutoMigrate
at startup. Update the migration or deployment flow to support pre-creating the
index with MySQL-compatible online DDL where available, or ensure it runs during
a documented low-traffic window, including compatibility with MySQL 5.7.

In `@model/savings_log_test.go`:
- Around line 58-82: Add a brief comment above
TestGetSavingsLifetimeLogBatchUsesClickHouseCompositeKeyset explaining that it
uses SQLite while forcing the ClickHouse database type, so it only validates the
composite-keyset predicate shape and not ClickHouse-specific SQL acceptance or
ordering semantics.

In `@service/savings_lifetime_backfill.go`:
- Around line 204-229: The per-user summary currently performs a global
CountPendingSavingsLifetimeEvents query on every request. Update the summary
flow around GetLatestSystemTask and CountPendingSavingsLifetimeEvents to avoid
this repeated table scan, preferably by deriving IsComplete from the global
backfill task status or reusing a short-lived in-memory cache for the global
pending count and status.
- Around line 400-414: Update the conversion flow around
savingsLifetimeAmountMicros to parse and validate the frozen
estimate.SavingsCNYMicros snapshot first; when it is usable, assign amountMicros
and snapshot fields without calling the fallback conversion. Only invoke
savingsLifetimeAmountMicros and return its error when the frozen value is absent
or unusable, while preserving the existing payload snapshot fallback.

In `@web/src/features/dashboard/components/overview/summary-cards.tsx`:
- Around line 190-203: Extract the savings-related logic from SummaryCards into
a dedicated useSavingsSummaryCard hook containing savingsSummaryQuery,
savingsLifetimeQuery, and their derived display strings, plus a
SavingsSummaryCard presentational component for the matching JSX block. Update
SummaryCards to use the new component so it remains focused on the usage/runway
summary while preserving the existing savings data and display behavior.

In `@web/src/features/system-settings/models/savings-lifetime-backfill.tsx`:
- Around line 67-279: The SavingsLifetimeBackfill component combines
query/mutation state management with presentation and exceeds the recommended
size. Extract the query and four mutation workflows into a
useSavingsLifetimeBackfill hook, and move the task progress/summary markup into
a focused subcomponent; keep SavingsLifetimeBackfill responsible for layout and
action controls while preserving the existing behavior and rendered values.
- Around line 85-89: Update all four mutation success handlers in the savings
lifetime backfill flow to invalidate the related savings summary and other
backfill-derived query keys after calling setQueryData with the successful task
response. Use the existing array-form query key constants and React Query
invalidateQueries pattern, while preserving the current cache update behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b5b9a47-980c-4cfe-b611-4d3b906e4b11

📥 Commits

Reviewing files that changed from the base of the PR and between aa82302 and a2a65db.

📒 Files selected for processing (40)
  • controller/savings.go
  • controller/system_task.go
  • docs/user-savings-estimate-design.md
  • docs/user-savings-trend-design.md
  • main.go
  • model/log.go
  • model/main.go
  • model/savings_lifetime.go
  • model/savings_lifetime_test.go
  • model/savings_log.go
  • model/savings_log_test.go
  • model/system_task.go
  • model/system_task_test.go
  • router/api-router.go
  • service/savings_estimate.go
  • service/savings_lifetime_backfill.go
  • service/savings_lifetime_test.go
  • service/savings_lifetime_worker.go
  • service/system_task.go
  • service/text_quota.go
  • setting/savings_setting/config.go
  • web/src/features/dashboard/api.ts
  • web/src/features/dashboard/components/overview/summary-cards.tsx
  • web/src/features/dashboard/lib/__tests__/savings-time-range.test.ts
  • web/src/features/dashboard/lib/savings.ts
  • web/src/features/dashboard/types.ts
  • web/src/features/system-info/components/system-tasks-panel.tsx
  • web/src/features/system-settings/api.ts
  • web/src/features/system-settings/models/savings-estimate-setting.ts
  • web/src/features/system-settings/models/savings-estimate-settings.tsx
  • web/src/features/system-settings/models/savings-lifetime-backfill.tsx
  • web/src/features/system-settings/types.ts
  • web/src/features/wallet/components/wallet-stats-card.tsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (9)
  • service/text_quota.go
  • web/src/features/dashboard/types.ts
  • web/src/features/dashboard/lib/savings.ts
  • web/src/features/system-settings/models/savings-estimate-setting.ts
  • setting/savings_setting/config.go
  • docs/user-savings-trend-design.md
  • web/src/features/system-settings/models/savings-estimate-settings.tsx
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/vi.json

Comment thread docs/user-savings-estimate-design.md Outdated
Comment thread model/savings_lifetime.go Outdated
Comment thread web/src/features/system-info/components/system-tasks-panel.tsx
Comment thread web/src/features/system-settings/api.ts Outdated
Comment thread web/src/features/wallet/components/wallet-stats-card.tsx Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/zh.json Outdated
@RHZHZ
RHZHZ marked this pull request as draft July 29, 2026 08:10
@RHZHZ RHZHZ closed this Jul 29, 2026
@RHZHZ
RHZHZ deleted the codex/user-savings-estimates branch July 29, 2026 09:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/i18n/locales/ru.json (1)

1609-1609: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe enabling lifetime tracking, not an amount.

Включить накопленную экономию reads as enabling an already accumulated savings amount. Prefer Включить накопление экономии за всё время or Включить учёт экономии за всё время.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/i18n/locales/ru.json` at line 1609, Update the Russian translation
for the “Enable lifetime savings” key to describe enabling lifetime savings
tracking or accumulation, using wording such as “Включить накопление экономии за
всё время” or “Включить учёт экономии за всё время” instead of implying an
existing accumulated amount.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/user-savings-lifetime-review-remediation.md`:
- Around line 113-127: Update the long-term event amount-selection flow so a
valid non-negative int64 SavingsCNYMicros is used immediately, without requiring
positive quota_per_unit or usd_cny_rate_micros. Only validate and use the frozen
conversion parameters when SavingsCNYMicros is missing or invalid, and preserve
batch termination on fallback conversion failure or overflow; avoid calling
savingsLifetimeAmountMicros before deciding this path.

In `@web/src/i18n/locales/ru.json`:
- Line 2547: Update the Russian translation for the “Lifetime savings counted so
far” key to “Накопленная экономия на данный момент,” matching the adjacent
counted-so-far wording.
- Line 4519: Update the Russian translation for the “System historical data is
being counted” locale key to “Идёт подсчёт исторических данных”, preserving the
counting status rather than describing generic processing.
- Line 861: Update the Russian translation for the “Clear usage and balance” key
to use action-oriented reset terminology, such as “Очистить использование и
баланс,” replacing the current wording that means “transparent usage and
balance.”
- Line 4485: Update the Russian translation for the “supported billing models”
key to use billing terminology rather than “моделей оплаты”; replace the value
with wording such as “поддерживаемых моделей тарификации” while preserving the
key and JSON validity.

In `@web/src/i18n/locales/zh-TW.json`:
- Line 3173: Update both endpoint translations in
web/src/i18n/locales/zh-TW.json: at lines 3173 and 5061, consistently translate
“endpoint” as “端點,” using “一個端點、一枚金鑰” and “相容端點” respectively.

---

Outside diff comments:
In `@web/src/i18n/locales/ru.json`:
- Line 1609: Update the Russian translation for the “Enable lifetime savings”
key to describe enabling lifetime savings tracking or accumulation, using
wording such as “Включить накопление экономии за всё время” or “Включить учёт
экономии за всё время” instead of implying an existing accumulated amount.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d5e73b76-bf1d-428f-98d5-52f4c91a0a41

📥 Commits

Reviewing files that changed from the base of the PR and between a2a65db and 0ae2c6b.

📒 Files selected for processing (36)
  • docs/user-savings-estimate-design.md
  • docs/user-savings-lifetime-review-remediation.md
  • docs/user-savings-summary-ui-redesign.md
  • docs/user-savings-trend-design.md
  • model/savings_lifetime.go
  • model/savings_lifetime_test.go
  • model/savings_log_test.go
  • service/savings_estimate.go
  • service/savings_lifetime_backfill.go
  • service/savings_lifetime_test.go
  • setting/savings_setting/config.go
  • setting/savings_setting/config_test.go
  • web/src/features/dashboard/components/models/savings-trend-chart.tsx
  • web/src/features/dashboard/components/overview/__tests__/summary-cards-layout.test.ts
  • web/src/features/dashboard/components/overview/summary-cards-layout.ts
  • web/src/features/dashboard/components/overview/summary-cards.tsx
  • web/src/features/dashboard/lib/__tests__/savings-i18n.test.ts
  • web/src/features/dashboard/lib/__tests__/savings-lifetime.test.ts
  • web/src/features/dashboard/lib/savings-query-keys.ts
  • web/src/features/dashboard/lib/savings.ts
  • web/src/features/system-info/components/system-tasks-panel.tsx
  • web/src/features/system-info/lib/__tests__/system-task-status.test.ts
  • web/src/features/system-info/lib/system-task-status.ts
  • web/src/features/system-settings/api.ts
  • web/src/features/system-settings/lib/__tests__/savings-lifetime-query.test.ts
  • web/src/features/system-settings/lib/savings-lifetime-query.ts
  • web/src/features/system-settings/models/savings-estimate-settings.tsx
  • web/src/features/system-settings/models/savings-lifetime-backfill.tsx
  • web/src/features/wallet/components/wallet-stats-card.tsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (17)
  • web/src/features/system-settings/api.ts
  • setting/savings_setting/config_test.go
  • web/src/features/wallet/components/wallet-stats-card.tsx
  • model/savings_lifetime_test.go
  • web/src/features/dashboard/components/models/savings-trend-chart.tsx
  • model/savings_log_test.go
  • setting/savings_setting/config.go
  • web/src/features/system-info/components/system-tasks-panel.tsx
  • service/savings_estimate.go
  • docs/user-savings-estimate-design.md
  • service/savings_lifetime_backfill.go
  • docs/user-savings-trend-design.md
  • web/src/features/dashboard/components/overview/summary-cards.tsx
  • web/src/features/system-settings/models/savings-estimate-settings.tsx
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json

Comment thread docs/user-savings-lifetime-review-remediation.md
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/i18n/locales/zh-TW.json Outdated
"One API": "One API",
"One domain per line": "每行一個域名",
"One domain per line (only used when domain restriction is enabled)": "每行一個域名 (僅在啟用域名限制時使用)",
"One endpoint, one key, and a clear view of every request.": "一個介面、一枚金鑰,每次請求都清楚可見。",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate “endpoint” consistently as 端點.

  • web/src/i18n/locales/zh-TW.json#L3173-L3173: use 一個端點、一枚金鑰.
  • web/src/i18n/locales/zh-TW.json#L5061-L5061: use 相容端點 instead of 相容介面.
📍 Affects 1 file
  • web/src/i18n/locales/zh-TW.json#L3173-L3173 (this comment)
  • web/src/i18n/locales/zh-TW.json#L5061-L5061
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/i18n/locales/zh-TW.json` at line 3173, Update both endpoint
translations in web/src/i18n/locales/zh-TW.json: at lines 3173 and 5061,
consistently translate “endpoint” as “端點,” using “一個端點、一枚金鑰” and “相容端點”
respectively.

@RHZHZ
RHZHZ restored the codex/user-savings-estimates branch July 29, 2026 09:30
@coderabbitai coderabbitai Bot mentioned this pull request Aug 4, 2026
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant