Skip to content

feat: kanban dashboard i18n, request/response body capture, log auto-cleanup, and GHCR publish - #6400

Closed
molicherry wants to merge 16 commits into
QuantumNous:mainfrom
molicherry:main
Closed

feat: kanban dashboard i18n, request/response body capture, log auto-cleanup, and GHCR publish#6400
molicherry wants to merge 16 commits into
QuantumNous:mainfrom
molicherry:main

Conversation

@molicherry

@molicherry molicherry commented Jul 22, 2026

Copy link
Copy Markdown

Summary

15 commits from inspect branch, including:

  • Dashboard (kanban) enhancements: token charts, channel analysis, all-time stats, daily overview, performance ranking
  • Request/Response capture: headers and body capture with file-based external storage, Inspect dialog in usage logs
  • Log auto-cleanup: configurable DB log retention + body file cleanup
  • i18n: translations for log cleanup and body capture (zh/zh-TW/fr/ja/ru/vi)
  • CI: GHCR multi-arch publish workflow
  • Misc fixes: hasInspectData crash fix, duplicate formatJsonInline removal, Unicode escape fix (footer.newapi keys)

Summary by CodeRabbit

  • New Features
    • Added request and response inspection in consume logs, including headers and provider data, with sensitive headers redacted.
    • Added configurable capture options and automatic cleanup settings for stored bodies and database logs.
    • Expanded dashboards with token analytics, channel usage, daily overviews, performance trends, and rankings.
    • Added an “All Time” reporting range and metric switching between requests and tokens.
    • Added multi-architecture Docker image publishing support.
  • Documentation
    • Added translations for the new dashboard and inspection settings.

…al storage

- Add 8 configurable capture flags (StoreRequestBodyEnabled,
  StoreResponseBodyEnabled, StoreRequestHeadersEnabled,
  StoreResponseHeadersEnabled, StoreProviderRequestBodyEnabled,
  StoreProviderResponseBodyEnabled) + 2 provider body flags
- HeaderCapture middleware captures request/response headers+bodies
  from BodyStorage and via ResponseWriter wrapping; sensitive
  headers (Authorization, etc.) redacted
- Provider format capture: request body after conversion in
  TextHelper, response body before conversion in all 8 relay helpers
  (text, audio, image, claude, gemini, rerank, embedding, responses)
- File-based external storage: bodies > 4KB written to
  data/bodies/YYYYMMDD/ to avoid DB bloat; small bodies stored
  inline in logs.other JSON
- GET /api/log/body/*path endpoint to serve stored body files
- All flags configurable via admin UI (OptionMap)
- Streaming responses skipped for body capture to avoid OOM

AI-generated code
…response capture

- Add type definitions for request_headers, request_body,
  response_headers, response_body, provider_request_body,
  provider_response_body to LogOtherData
- Add InspectHeaderBlock and InspectBodyBlock components to
  details-dialog for admin-only display of captured headers/bodies
- Inline JSON bodies rendered in collapsible pre block with
  pretty-print; file-referenced bodies show disk path
- Add i18n keys for inspect labels (en.json)

AI-generated code
…ettings

- Add 6 boolean toggles for request/response header and body capture
  (StoreRequestBodyEnabled, StoreResponseBodyEnabled,
   StoreRequestHeadersEnabled, StoreResponseHeadersEnabled,
   StoreProviderRequestBodyEnabled, StoreProviderResponseBodyEnabled)
- Update OperationsSettings type and default values
- Pass bodyCaptureSettings prop through section-registry to
  LogSettingsSection component
- Save all changed settings on form submit

AI-generated code
- Add BodyFileRetentionDays config (default 7 days, 0 = keep forever)
- StartBodyFileCleanup() goroutine runs hourly, deletes directories
  older than retention days under data/bodies/
- Cleanup is O(directories) thanks to date-sharded YYYYMMDD structure
- Register BodyFileRetentionDays in OptionMap (configurable via API)

AxonHub uses CleanupOptions with per-resource-type retention days.
This implements the same concept: body files auto-removed after N days
so they don't accumulate indefinitely.

AI-generated code
- Add LogRetentionDays option for automatic DB log record cleanup
- Background goroutine deletes logs older than configured days (hourly)
- Reuses existing CountOldLog/DeleteOldLogBatch for cross-DB safety
- Frontend: Retention Days input in Operations > Log Maintenance
- Add GHCR multi-arch publish workflow (tag-triggered + manual)
…daily overview, and performance ranking

Backend:
- Add GET /api/data/channel endpoint (GROUP BY channel_id, model_name, created_at)
- Add all_time=true param to /api/data and /api/data/self (bypass time range filter)

Frontend - Token consumption charts (Item 1):
- Add token-based chart specs: spec_model_token_line, spec_token_pie, spec_token_rank_bar
- Add MetricMode toggle (Call Count / Token Consumption) in ModelCharts with persistence

Frontend - Channel cross-analysis (Item 2):
- New ChannelCharts component with Requests/Tokens/Quota metric selector
- Admin-only, aggregates quota_data by channel_id over time

Frontend - All-Time stats (Item 3):
- Add 'All Time' preset to time range selector (days=-1)
- Handle all_time param in API calls and filter dialog

Frontend - Daily overview chart (Item 4):
- New DailyOverviewPanel with dual-Y-axis combo chart (bar: Count, line: Tokens)
- Added to overview dashboard section

Frontend - Performance ranking & trends (Item 5):
- New PerformanceRanking: horizontal bar ranking by throughput with confidence badges
- New PerformanceTrends: 30-day line chart with Throughput/Latency/TTFT metric selector
- Both admin-only, using existing perf-metrics API

i18n: Add 6 new English translation keys
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change adds configurable request and provider payload inspection with filesystem offloading and log retention cleanup, extends dashboard quota analytics with all-time, token, channel, daily, and performance charts, updates localized settings and inspection UI, and introduces multi-architecture GHCR publishing automation.

Changes

Request inspection and log maintenance

Layer / File(s) Summary
Capture storage and persistence
common/*, middleware/body_log.go, model/log.go
Capture flags, context values, header redaction, response buffering, filesystem body offloading, and consume-log persistence are added.
Provider response capture
relay/*_handler.go
Provider request and response bodies are captured for streaming and non-streaming relay flows while preserving downstream reads.
Retention cleanup and configuration
common/*, model/log_cleanup.go, model/option.go, main.go
Body-file and database-log retention cleanup is configurable and started during initialization.
Inspection API and UI
controller/log_body.go, router/*, web/src/features/system-settings/*, web/src/features/usage-logs/*, web/src/i18n/locales/*
Admin routes, maintenance settings, body loading, usage-log inspection, types, and translations are added.

Dashboard analytics

Layer / File(s) Summary
Quota API and dashboard contracts
controller/usedata.go, model/usedata.go, web/src/features/dashboard/api.ts, web/src/features/dashboard/types.ts, web/src/features/dashboard/lib/filters.ts
All-time queries, grouped channel quota data, metric-mode preferences, and expanded chart data contracts are added.
Token and daily chart data
web/src/features/dashboard/lib/charts.ts
Token trends, distributions, rankings, totals, and daily overview specifications are derived from quota data.
Interactive dashboard charts
web/src/features/dashboard/components/models/*, web/src/features/dashboard/components/overview/*
Channel, performance, daily overview, and count/token chart components fetch data and render themed VCharts.
Dashboard integration
web/src/features/dashboard/index.tsx, web/src/features/dashboard/components/overview/overview-dashboard.tsx
New analytics widgets are lazy-loaded, integrated, and connected to dashboard preferences and filters.

Release automation

Layer / File(s) Summary
Multi-architecture image publishing
.github/workflows/ghcr-publish.yml, VERSION
The project version is updated and a tag/manual workflow builds amd64 and arm64 images before publishing GHCR manifests.

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

Poem

I’m a rabbit with logs in my burrow bright,
Capturing headers by soft moonlight.
Tokens hop through charts, channels gleam,
Old files fade from the cleanup stream.
Two tiny arches rise and deploy—
New paths to inspect, and dashboards to enjoy!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.87% 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 is concise and clearly reflects the main changes: dashboard updates, capture/cleanup features, i18n, and GHCR publishing.
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.

@molicherry molicherry closed this Jul 22, 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: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (2)
web/src/features/dashboard/components/models/models-filter-dialog.tsx (1)

138-155: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle defaultTimeRangeDays === -1 in buildDefaultDashboardFilters
getRollingDateRange(-1) creates a future/empty range, and the dialog-only guard doesn’t cover the other callers that use buildDefaultDashboardFilters directly. Special-case -1 there and leave start_timestamp / end_timestamp unset.

🤖 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/models/models-filter-dialog.tsx` around
lines 138 - 155, Update buildDefaultDashboardFilters to special-case
defaultTimeRangeDays === -1 by leaving start_timestamp and end_timestamp unset;
remove the dialog-only date-range correction from handleReset so all callers
share this behavior, while preserving rolling-range generation for other values.
model/option.go (1)

294-346: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

BodyFileRetentionDays/LogRetentionDays cases are unreachable — retention settings never take effect.

These two case branches live inside the switch key block guarded by if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" || key == "DefaultUseAutoGroup" || key == "SMTPForceAuthLogin" || key == "SMTPInsecureSkipVerify" at line 294. Since neither key ends in "Enabled" nor matches the explicit comparisons, this guard is always false for them, so the branches never execute — dead code.

Consequence: common.OptionMap[key] = value still stores the raw string, but common.BodyFileRetentionDays / common.LogRetentionDays (the actual variables read by common/log_cleanup.go and common/body_file.go) are never updated, whether via UpdateOption (live save) or loadOptionsFromDatabase() (startup replay of persisted config). The admin-configurable retention feature this PR adds is effectively non-functional — cleanup always runs against the compiled defaults.

🐛 Proposed fix: move the cases out of the "Enabled" guarded switch
 		case "LogConsumeEnabled":
 			common.LogConsumeEnabled = boolValue
 		case "StoreRequestBodyEnabled":
 			common.StoreRequestBodyEnabled = boolValue
 		case "StoreResponseBodyEnabled":
 			common.StoreResponseBodyEnabled = boolValue
 		case "StoreRequestHeadersEnabled":
 			common.StoreRequestHeadersEnabled = boolValue
 		case "StoreResponseHeadersEnabled":
 			common.StoreResponseHeadersEnabled = boolValue
 		case "StoreProviderRequestBodyEnabled":
 			common.StoreProviderRequestBodyEnabled = boolValue
 		case "StoreProviderResponseBodyEnabled":
 			common.StoreProviderResponseBodyEnabled = boolValue
-		case "BodyFileRetentionDays":
-			days, err := strconv.Atoi(value)
-			if err == nil && days >= 0 {
-				common.BodyFileRetentionDays = days
-			}
-		case "LogRetentionDays":
-			days, err := strconv.Atoi(value)
-			if err == nil && days >= 0 {
-				common.LogRetentionDays = days
-			}
 		case "DisplayInCurrencyEnabled":

Then add them to the unconditional switch key { ... } block further down (near case "EmailDomainWhitelist":):

 	switch key {
 	case "EmailDomainWhitelist":
 		common.EmailDomainWhitelist = strings.Split(value, ",")
+	case "BodyFileRetentionDays":
+		days, err := strconv.Atoi(value)
+		if err == nil && days >= 0 {
+			common.BodyFileRetentionDays = days
+		}
+	case "LogRetentionDays":
+		days, err := strconv.Atoi(value)
+		if err == nil && days >= 0 {
+			common.LogRetentionDays = days
+		}
 	case "SMTPServer":
 		common.SMTPServer = value
🤖 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/option.go` around lines 294 - 346, Move the BodyFileRetentionDays and
LogRetentionDays handling out of the Enabled-specific switch guarded by the
boolean-key condition and into the unconditional switch in the option update
flow. Preserve the existing non-negative integer validation and assignments to
common.BodyFileRetentionDays and common.LogRetentionDays so both UpdateOption
and loadOptionsFromDatabase apply persisted retention values.
🟡 Minor comments (11)
web/src/features/dashboard/lib/charts.ts-1060-1078 (1)

1060-1078: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Bind each series to a specific axis. The common chart defines left/right y-axes, but neither series is mapped with seriesId/seriesIndex, so both can fall back to the left scale and leave the right axis unused. Bind Count to the left axis and Tokens to the right axis.

🤖 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/charts.ts` around lines 1060 - 1078, Bind the
Count series to the left y-axis and the Tokens series to the right y-axis in the
common chart configuration. Update the series definitions using their existing
identifiers (seriesId or seriesIndex) so each series explicitly references the
intended axis, while preserving the current axis definitions.
web/src/features/dashboard/components/models/channel-charts.tsx-58-60 (1)

58-60: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize the channel label.

channelLabel returns a literal Channel #${id} that is rendered in the legend, tooltip keys, and color domain but never passed through t(). As per coding guidelines (所有面向用户的文案必须支持 i18n;React 组件使用 useTranslation()t()), the "Channel" prefix should be translatable.

🤖 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/models/channel-charts.tsx` around lines
58 - 60, Update channelLabel and its callers so the user-facing “Channel” prefix
is generated through the component’s useTranslation() t() function, while
preserving the channel ID formatting for legend, tooltip, and color-domain
values.

Source: Coding guidelines

web/src/features/dashboard/components/models/channel-charts.tsx-189-222 (1)

189-222: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Padding drops sparse historical buckets. When sortedTimes.length < MAX_CHART_TREND_POINTS, rebuilding chartTimes from lastTime can push real buckets outside the synthetic window, so older sparse points render as zeros instead of their actual values. Build the padded range from the real data span instead of anchoring only to the latest point.

🤖 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/models/channel-charts.tsx` around lines
189 - 222, The padding logic in the chart time-series construction should
preserve sparse historical buckets instead of rebuilding solely from the latest
timestamp. Update the chartTimes calculation near the channelValues loop to
derive the synthetic range from the real data span, including the earliest and
latest data points, while retaining existing real bucket values through
timeChannelMap lookups.
web/src/i18n/locales/ru.json-722-723 (1)

722-723: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use complete Russian wording for the non-streaming restriction.

"Только не-потоковые." is incomplete and awkward without a noun. Use wording such as "Только для непотоковых запросов." so the limitation is clear.

🤖 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` around lines 722 - 723, Update the Russian
translations for both “Capture raw response body” entries in the locale object
to use complete wording for the non-streaming restriction, replacing the
fragment “Только не-потоковые.” with phrasing such as “Только для непотоковых
запросов.”
web/src/i18n/locales/ru.json-1855-1855 (1)

1855-1855: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve “body” in the error translation.

"Не удалось загрузить файл" loses that this is specifically a request/response body file, making the usage-log error less actionable. Translate it as "Не удалось загрузить файл тела" or equivalent.

🤖 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 1855, Update the “Failed to load body
file” translation in ru.json to preserve the meaning that the failed file is
specifically a body file, using “Не удалось загрузить файл тела” or an
equivalent Russian translation.
web/src/i18n/locales/ru.json-4316-4316 (1)

4316-4316: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not omit the consume-log and database-bloat details.

This translation drops that raw request/response data is stored in consume logs and that disk offloading avoids database bloat. Preserve those details so administrators understand the storage behavior.

🤖 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 4316, Update the Russian translation
for the consume-log storage message to explicitly mention raw request and
response data, that it is stored in consume logs, and that bodies larger than 4
KB are written to disk to avoid database bloat.
web/src/i18n/locales/vi.json-4316-4316 (1)

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

Mention consume logs and database-bloat prevention.

This translation loses where the captured data is stored and why large bodies are written to disk.

Proposed translation
-    "Store raw request and response data in consume logs for debugging. Large bodies (>4KB) are written to disk to avoid database bloat.": "Lưu dữ liệu yêu cầu/phản hồi để gỡ lỗi. Dữ liệu lớn (>4KB) được ghi vào đĩa.",
+    "Store raw request and response data in consume logs for debugging. Large bodies (>4KB) are written to disk to avoid database bloat.": "Lưu dữ liệu yêu cầu và phản hồi thô trong nhật ký tiêu thụ để gỡ lỗi. Dữ liệu lớn (>4 KB) được ghi vào đĩa để tránh làm phình cơ sở dữ liệu.",
🤖 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/vi.json` at line 4316, Update the Vietnamese translation
for the consume-log message to explicitly mention that raw request and response
data is stored in consume logs and that bodies larger than 4KB are written to
disk to prevent database bloat.
web/src/i18n/locales/vi.json-2538-2538 (1)

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

Include the configured retention period in the translation.

The current text says records older than “something” are deleted, but omits that the threshold is the configured number of days.

Proposed translation
-    "Log records older than this many days will be deleted hourly. 0 = disabled (keep forever).": "Bản ghi nhật ký cũ hơn sẽ bị xóa mỗi giờ. 0 = tắt (giữ vĩnh viễn).",
+    "Log records older than this many days will be deleted hourly. 0 = disabled (keep forever).": "Các bản ghi nhật ký cũ hơn số ngày này sẽ bị xóa mỗi giờ. 0 = tắt (giữ vĩnh viễn).",
🤖 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/vi.json` at line 2538, Update the Vietnamese translation
for the retention-period message so it explicitly states that records older than
the configured number of days are deleted hourly, while preserving the existing
“0 = disabled (keep forever)” meaning.
web/src/i18n/locales/vi.json-724-724 (1)

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

Preserve the request-body and upstream JSON meaning.

The Vietnamese text drops both “request body” and “actual JSON sent to upstream,” making this capture option ambiguous.

Proposed translation
-    "Capture request body AFTER format conversion (actual JSON sent to upstream).": "Ghi lại nội dung sau khi chuyển đổi (JSON thực tế gửi lên nguồn).",
+    "Capture request body AFTER format conversion (actual JSON sent to upstream).": "Ghi lại nội dung yêu cầu SAU khi chuyển đổi (JSON thực tế được gửi lên upstream).",
🤖 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/vi.json` at line 724, Update the Vietnamese translation
for the locale key "Capture request body AFTER format conversion (actual JSON
sent to upstream)." to explicitly preserve both meanings: capturing the request
body after format conversion and the actual JSON sent to the upstream service.
web/src/i18n/locales/zh-TW.json-3559-3560 (1)

3559-3560: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use consistent provider terminology.

The existing Provider translation uses 供應商 (Line 3554), but these new labels use 提供者. Use the established terminology to keep the UI consistent.

-    "Provider Request Body": "提供者請求主體",
-    "Provider Response Body": "提供者回應主體",
+    "Provider Request Body": "供應商請求主體",
+    "Provider Response Body": "供應商回應主體",
🤖 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` around lines 3559 - 3560, Update the
“Provider Request Body” and “Provider Response Body” translations in the locale
data to use the established `供應商` terminology from the existing Provider
translation, replacing `提供者` while preserving the rest of each label.
web/src/features/system-settings/maintenance/log-settings-section.tsx-474-482 (1)

474-482: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Do not coerce an empty retention field to zero.

Number('') becomes 0, so clearing this field silently disables automatic deletion when saved. Keep blank invalid (or require an explicit 0) instead.

🤖 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/maintenance/log-settings-section.tsx` around
lines 474 - 482, Update the retention field’s onChange handling in the Input
component so an empty value is not converted to numeric zero by
Number(event.target.value). Preserve a blank value as invalid or otherwise
prevent saving it, while continuing to accept explicit numeric values including
0.
🧹 Nitpick comments (3)
common/body_file.go (1)

144-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid this single-caller package helper.

isAllDigits is only used by CleanupOldBodyFiles; inline the check or replace it with standard-library parsing in the caller. As per coding guidelines, “avoid package-level helpers with only one caller unless required by … complex business logic.”

🤖 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 `@common/body_file.go` around lines 144 - 151, Remove the single-caller
package-level helper isAllDigits and move its digit validation directly into
CleanupOldBodyFiles, or use an appropriate standard-library parsing function
there. Preserve the existing behavior for empty and non-digit strings while
eliminating the standalone helper.

Source: Coding guidelines

model/log_cleanup.go (1)

16-37: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the cleanup context with a timeout. context.Background() has no deadline, so a stalled batch delete can block this background job indefinitely. The batch-delete helpers already handle the database-specific paths.

🤖 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_cleanup.go` around lines 16 - 37, Update cleanOldDbLogs to create a
timeout-bounded context instead of using context.Background(), applying the
timeout to both CountOldLog and DeleteOldLogBatch calls. Ensure the context is
canceled when cleanup finishes while preserving the existing batch loop and
error propagation.

Source: Path instructions

web/src/features/usage-logs/components/dialogs/details-dialog.tsx (1)

1246-1262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use English source strings instead of dotted inspect keys.

The new inspect.* keys conflict with the required flat English-source-key convention; Loading... is also rendered without t().

  • web/src/features/usage-logs/components/dialogs/details-dialog.tsx#L1246-L1262: pass English labels such as Request Body to the block and translate with t(label).
  • web/src/features/usage-logs/components/dialogs/details-dialog.tsx#L1397-L1422: replace inspect.file_reference and hardcoded Loading... with translated English source strings.
  • web/src/i18n/locales/en.json#L5227-L5231: replace dotted keys with flat English source-string keys.

As per coding guidelines, “use useTranslation() and t('English key'), with flat locale JSON files … using English source strings as keys.”

🤖 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/usage-logs/components/dialogs/details-dialog.tsx` around
lines 1246 - 1262, Update
web/src/features/usage-logs/components/dialogs/details-dialog.tsx at lines
1246-1262 to pass English source labels such as “Request Body” to
InspectBodyBlock and translate them via t(label); at lines 1397-1422 replace
inspect.file_reference and the un-translated “Loading...” text with English
source strings passed through t(). Update web/src/i18n/locales/en.json at lines
5227-5231 to use matching flat English source-string keys instead of dotted
inspect.* keys.

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 @.github/workflows/ghcr-publish.yml:
- Around line 117-122: Gate the “Create & push latest manifest” step so it runs
only for confirmed stable release tags, excluding prerelease tags and
dispatch-triggered builds. Preserve the existing manifest creation command for
eligible stable releases.
- Around line 36-46: The tag is generated in multiple places and is unavailable
through env-context expressions. In .github/workflows/ghcr-publish.yml lines
36-46, expose the resolved tag as a job output from the version step; update
lines 72-74 to consume that output for the build and summary, and update lines
101-108 so the manifest job consumes the same output instead of recalculating
the date-based tag. Ensure all jobs use one consistently generated release tag.

In `@common/constants.go`:
- Around line 113-121: Wire provider-request capture through every relay path
using the existing context key and StoreProviderRequestBodyEnabled behavior:
update Claude, Gemini, embeddings, image, rerank, responses, and audio handlers,
plus TextHelper pass-through requests, to store the exact request payload sent
upstream before forwarding. Match the existing implementation in
relay/compatible_handler.go and preserve the disabled-setting behavior.

In `@controller/usedata.go`:
- Around line 71-84: Update GetQuotaDataGroupByChannel to parse the all_time
query parameter and, when it is true, avoid applying the zero start/end
timestamp range by using the established all-time handling used by
GetAllQuotaDates and GetUserQuotaDates. Preserve the existing timestamp-based
query behavior for non-all-time requests and continue returning errors through
common.ApiError.

In `@middleware/body_log.go`:
- Around line 69-76: Bound or spool response-body capture before data is
accumulated, using BodyFileThreshold or the established capture mechanism rather
than unbounded memory buffering. Update middleware/body_log.go:69-76 around
responseBodyWriter, relay/audio_handler.go:59-64,
relay/embedding_handler.go:79-85, relay/gemini_handler.go:308-314,
relay/image_handler.go:101-107, and relay/rerank_handler.go:91-97 to avoid
unrestricted full-body reads; update both capture branches in
relay/claude_handler.go:211-225, relay/compatible_handler.go:208-225,
relay/gemini_handler.go:190-205, and relay/responses_handler.go:137-152. Ensure
forwarding remains timely while RecordConsumeLog receives only bounded or
spooled captured data.
- Around line 11-16: Extend the header-redaction logic centered on
sensitiveRequestHeaders to cover Cookie and Set-Cookie, and apply it
case-insensitively before storing either request or response header map. Ensure
both session-credential headers are replaced with the existing redacted
representation in capture logs.

In `@router/relay-router.go`:
- Line 65: Update each middleware chain in router/relay-router.go at lines 65,
77-85, 182, 194, and 205 so response-header redaction runs before HeaderCapture.
Ensure the redaction covers sensitive headers such as Set-Cookie and
provider-issued credentials, then retain HeaderCapture only after that
protection is applied.

In `@web/src/features/dashboard/components/overview/daily-overview-panel.tsx`:
- Around line 64-81: Convert the Date values returned by getRollingDateRange in
the timeRange useMemo to Unix seconds numbers before passing them to
getUserQuotaDates. Update start_timestamp and end_timestamp using the
established getTime()/1000 conversion with flooring, while preserving the
existing query key and date-range behavior.

In `@web/src/features/dashboard/index.tsx`:
- Around line 418-424: The metricMode change handler must preserve the current
modelFilters instead of routing through handleChartPreferencesChange, which
rebuilds default filters and triggers a refetch. Update the onMetricModeChange
path and its preference persistence so only the client-side metricMode setting
changes while the applied custom or quick time range remains intact.

In `@web/src/features/dashboard/types.ts`:
- Around line 279-280: Remove the duplicate MetricMode type declaration near the
later section of the module, keeping the existing declaration earlier in the
file as the single source of truth.

---

Outside diff comments:
In `@model/option.go`:
- Around line 294-346: Move the BodyFileRetentionDays and LogRetentionDays
handling out of the Enabled-specific switch guarded by the boolean-key condition
and into the unconditional switch in the option update flow. Preserve the
existing non-negative integer validation and assignments to
common.BodyFileRetentionDays and common.LogRetentionDays so both UpdateOption
and loadOptionsFromDatabase apply persisted retention values.

In `@web/src/features/dashboard/components/models/models-filter-dialog.tsx`:
- Around line 138-155: Update buildDefaultDashboardFilters to special-case
defaultTimeRangeDays === -1 by leaving start_timestamp and end_timestamp unset;
remove the dialog-only date-range correction from handleReset so all callers
share this behavior, while preserving rolling-range generation for other values.

---

Minor comments:
In `@web/src/features/dashboard/components/models/channel-charts.tsx`:
- Around line 58-60: Update channelLabel and its callers so the user-facing
“Channel” prefix is generated through the component’s useTranslation() t()
function, while preserving the channel ID formatting for legend, tooltip, and
color-domain values.
- Around line 189-222: The padding logic in the chart time-series construction
should preserve sparse historical buckets instead of rebuilding solely from the
latest timestamp. Update the chartTimes calculation near the channelValues loop
to derive the synthetic range from the real data span, including the earliest
and latest data points, while retaining existing real bucket values through
timeChannelMap lookups.

In `@web/src/features/dashboard/lib/charts.ts`:
- Around line 1060-1078: Bind the Count series to the left y-axis and the Tokens
series to the right y-axis in the common chart configuration. Update the series
definitions using their existing identifiers (seriesId or seriesIndex) so each
series explicitly references the intended axis, while preserving the current
axis definitions.

In `@web/src/features/system-settings/maintenance/log-settings-section.tsx`:
- Around line 474-482: Update the retention field’s onChange handling in the
Input component so an empty value is not converted to numeric zero by
Number(event.target.value). Preserve a blank value as invalid or otherwise
prevent saving it, while continuing to accept explicit numeric values including
0.

In `@web/src/i18n/locales/ru.json`:
- Around line 722-723: Update the Russian translations for both “Capture raw
response body” entries in the locale object to use complete wording for the
non-streaming restriction, replacing the fragment “Только не-потоковые.” with
phrasing such as “Только для непотоковых запросов.”
- Line 1855: Update the “Failed to load body file” translation in ru.json to
preserve the meaning that the failed file is specifically a body file, using “Не
удалось загрузить файл тела” or an equivalent Russian translation.
- Line 4316: Update the Russian translation for the consume-log storage message
to explicitly mention raw request and response data, that it is stored in
consume logs, and that bodies larger than 4 KB are written to disk to avoid
database bloat.

In `@web/src/i18n/locales/vi.json`:
- Line 4316: Update the Vietnamese translation for the consume-log message to
explicitly mention that raw request and response data is stored in consume logs
and that bodies larger than 4KB are written to disk to prevent database bloat.
- Line 2538: Update the Vietnamese translation for the retention-period message
so it explicitly states that records older than the configured number of days
are deleted hourly, while preserving the existing “0 = disabled (keep forever)”
meaning.
- Line 724: Update the Vietnamese translation for the locale key "Capture
request body AFTER format conversion (actual JSON sent to upstream)." to
explicitly preserve both meanings: capturing the request body after format
conversion and the actual JSON sent to the upstream service.

In `@web/src/i18n/locales/zh-TW.json`:
- Around line 3559-3560: Update the “Provider Request Body” and “Provider
Response Body” translations in the locale data to use the established `供應商`
terminology from the existing Provider translation, replacing `提供者` while
preserving the rest of each label.

---

Nitpick comments:
In `@common/body_file.go`:
- Around line 144-151: Remove the single-caller package-level helper isAllDigits
and move its digit validation directly into CleanupOldBodyFiles, or use an
appropriate standard-library parsing function there. Preserve the existing
behavior for empty and non-digit strings while eliminating the standalone
helper.

In `@model/log_cleanup.go`:
- Around line 16-37: Update cleanOldDbLogs to create a timeout-bounded context
instead of using context.Background(), applying the timeout to both CountOldLog
and DeleteOldLogBatch calls. Ensure the context is canceled when cleanup
finishes while preserving the existing batch loop and error propagation.

In `@web/src/features/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 1246-1262: Update
web/src/features/usage-logs/components/dialogs/details-dialog.tsx at lines
1246-1262 to pass English source labels such as “Request Body” to
InspectBodyBlock and translate them via t(label); at lines 1397-1422 replace
inspect.file_reference and the un-translated “Loading...” text with English
source strings passed through t(). Update web/src/i18n/locales/en.json at lines
5227-5231 to use matching flat English source-string keys instead of dotted
inspect.* keys.
🪄 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

Run ID: f9bc1f14-15d8-43b7-9e17-0ce3ae6699d6

📥 Commits

Reviewing files that changed from the base of the PR and between 1721144 and ae17a02.

📒 Files selected for processing (50)
  • .github/workflows/ghcr-publish.yml
  • VERSION
  • common/body_file.go
  • common/constants.go
  • common/log_cleanup.go
  • controller/log_body.go
  • controller/usedata.go
  • main.go
  • middleware/body_log.go
  • model/log.go
  • model/log_cleanup.go
  • model/option.go
  • model/usedata.go
  • relay/audio_handler.go
  • relay/claude_handler.go
  • relay/compatible_handler.go
  • relay/embedding_handler.go
  • relay/gemini_handler.go
  • relay/image_handler.go
  • relay/rerank_handler.go
  • relay/responses_handler.go
  • router/api-router.go
  • router/relay-router.go
  • web/src/features/dashboard/api.ts
  • web/src/features/dashboard/components/models/channel-charts.tsx
  • web/src/features/dashboard/components/models/log-stat-cards.tsx
  • web/src/features/dashboard/components/models/model-charts.tsx
  • web/src/features/dashboard/components/models/models-filter-dialog.tsx
  • web/src/features/dashboard/components/models/performance-ranking.tsx
  • web/src/features/dashboard/components/models/performance-trends.tsx
  • web/src/features/dashboard/components/overview/daily-overview-panel.tsx
  • web/src/features/dashboard/components/overview/overview-dashboard.tsx
  • web/src/features/dashboard/constants.ts
  • web/src/features/dashboard/index.tsx
  • web/src/features/dashboard/lib/charts.ts
  • web/src/features/dashboard/lib/filters.ts
  • web/src/features/dashboard/types.ts
  • web/src/features/system-settings/maintenance/log-settings-section.tsx
  • web/src/features/system-settings/operations/index.tsx
  • web/src/features/system-settings/operations/section-registry.tsx
  • web/src/features/system-settings/types.ts
  • web/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • 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 on lines +36 to +46
- name: Resolve tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${GITHUB_REF#refs/heads/}-$(date +'%Y%m%d')-${GITHUB_SHA::7}"
else
TAG=${GITHUB_REF#refs/tags/}
fi
echo "TAG=${TAG}" >> $GITHUB_ENV
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "${TAG}" > VERSION

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

workflow='.github/workflows/ghcr-publish.yml'

# This should produce no matches after replacing dynamic env-context reads
# with a shared job/step output.
if rg -n '\$\{\{[[:space:]]*env\.TAG[[:space:]]*\}\}' "$workflow"; then
  exit 1
fi

# Confirm TAG has one producer and consumers reference its output.
rg -n 'GITHUB_OUTPUT|needs\.[A-Za-z0-9_-]+\.outputs\.[A-Za-z0-9_-]+' "$workflow"

Repository: QuantumNous/new-api

Length of output: 368


🏁 Script executed:

#!/bin/bash
set -eu

workflow='.github/workflows/ghcr-publish.yml'

echo '--- file outline ---'
ast-grep outline "$workflow" --view expanded || true

echo '--- relevant sections ---'
sed -n '1,140p' "$workflow" | cat -n

echo '--- env.TAG references ---'
rg -n '\$\{\{[[:space:]]*env\.TAG[[:space:]]*\}\}|GITHUB_ENV|GITHUB_OUTPUT|needs\.' "$workflow"

Repository: QuantumNous/new-api

Length of output: 5899


🌐 Web query:

GitHub Actions GITHUB_ENV available in expression context env.TAG documentation

💡 Result:

The GITHUB_ENV environment file and the env context operate at different stages of the GitHub Actions workflow lifecycle, which is why a variable written to GITHUB_ENV is not immediately available in the env expression context within the same job [1][2]. 1. Understanding GITHUB_ENV: The GITHUB_ENV environment file is used to define environment variables that become available to subsequent steps within the same job [3][2]. When you write a value to this file (e.g., echo "TAG=v1.0" >> $GITHUB_ENV), the assignment is processed by the runner [1][2]. 2. Understanding the env Context: The env context is evaluated by GitHub Actions before the job is sent to the runner [1][2]. Because the env context is interpolated during this initial processing phase, it cannot see or access environment variables that are dynamically created on the runner by a step during execution [1][2]. 3. Accessing Values: - To use a value created via GITHUB_ENV in a later step within the same job, you should reference it as a standard environment variable (e.g., $TAG in a bash script) rather than via the ${{ env.TAG }} expression syntax [1][2]. - If you need to use a value in a conditional (like an if clause) or another part of the workflow processed before the runner, you cannot use variables generated via GITHUB_ENV [1][2]. In such cases, you must use outputs (via GITHUB_OUTPUT) or define the variable in the workflow file using the env key [3][2]. In summary, you cannot use ${{ env.TAG }} to access a variable created in a previous step via GITHUB_ENV because the env context does not track changes made to the environment at runtime [1][2].

Citations:


Generate the release tag once and reuse it everywhere. GITHUB_ENV does not feed ${{ env.TAG }}, so the build step and summary expand an empty tag here. The manifest job also recalculates the date-based tag independently, which can drift across midnight. Expose one job output and consume it in both jobs.

📍 Affects 1 file
  • .github/workflows/ghcr-publish.yml#L36-L46 (this comment)
  • .github/workflows/ghcr-publish.yml#L72-L74
  • .github/workflows/ghcr-publish.yml#L101-L108
🤖 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 @.github/workflows/ghcr-publish.yml around lines 36 - 46, The tag is
generated in multiple places and is unavailable through env-context expressions.
In .github/workflows/ghcr-publish.yml lines 36-46, expose the resolved tag as a
job output from the version step; update lines 72-74 to consume that output for
the build and summary, and update lines 101-108 so the manifest job consumes the
same output instead of recalculating the date-based tag. Ensure all jobs use one
consistently generated release tag.

Comment on lines +117 to +122
- name: Create & push latest manifest
run: |
docker buildx imagetools create \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-arm64

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not promote prereleases to latest.

This runs for every tag; publishing v1.0.0-rc.21-inspect.1 would overwrite latest with a release candidate. Gate this step to confirmed stable releases and skip it for dispatch builds.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 120-120: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 121-121: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 122-122: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 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 @.github/workflows/ghcr-publish.yml around lines 117 - 122, Gate the “Create
& push latest manifest” step so it runs only for confirmed stable release tags,
excluding prerelease tags and dispatch-triggered builds. Preserve the existing
manifest creation command for eligible stable releases.

Comment thread common/constants.go
Comment on lines +113 to +121
// StoreProviderRequestBodyEnabled captures the request body AFTER format conversion
// (i.e., the actual JSON sent to the upstream provider). Stored as "provider_request_body"
// in the consume log's Other JSON. Defaults to false.
var StoreProviderRequestBodyEnabled = false

// StoreProviderResponseBodyEnabled captures the raw response body FROM the upstream provider
// BEFORE format conversion back to user format. Only non-streaming. Stored as
// "provider_response_body" in Other JSON. Defaults to false.
var StoreProviderResponseBodyEnabled = false

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Wire provider-request capture through every relay path.

model/log.go persists this context key globally, but among the supplied handlers only relay/compatible_handler.go sets it. Claude, Gemini, embeddings, image, rerank, responses, and audio paths omit provider requests; TextHelper also omits pass-through requests. Enabling this setting therefore produces incomplete inspection logs.

🤖 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 `@common/constants.go` around lines 113 - 121, Wire provider-request capture
through every relay path using the existing context key and
StoreProviderRequestBodyEnabled behavior: update Claude, Gemini, embeddings,
image, rerank, responses, and audio handlers, plus TextHelper pass-through
requests, to store the exact request payload sent upstream before forwarding.
Match the existing implementation in relay/compatible_handler.go and preserve
the disabled-setting behavior.

Comment thread controller/usedata.go
Comment on lines +71 to +84
func GetQuotaDataGroupByChannel(c *gin.Context) {
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
// 判断时间跨度是否超过 1 个月
if endTimestamp-startTimestamp > 2592000 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "时间跨度不能超过 1 个月",
})
dates, err := model.GetQuotaDataGroupByChannel(startTimestamp, endTimestamp)
if err != nil {
common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": dates,
})
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

GetQuotaDataGroupByChannel ignores all_time, breaking the "All Time" channel view.

The dashboard client sends all_time: 'true' with start_timestamp=0/end_timestamp=0 when the All Time preset is active (web/src/features/dashboard/components/models/channel-charts.tsx Lines 107-111). This handler only parses the timestamps, so the downstream query runs created_at >= 0 and created_at <= 0 and returns no rows. GetAllQuotaDates and GetUserQuotaDates already special-case all_time; this handler must do the same.

🐛 Proposed fix
 func GetQuotaDataGroupByChannel(c *gin.Context) {
-	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
-	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
+	var startTimestamp int64
+	var endTimestamp int64
+	if c.Query("all_time") == "true" {
+		startTimestamp = 0
+		endTimestamp = time.Now().Unix()
+	} else {
+		startTimestamp, _ = strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
+		endTimestamp, _ = strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
+	}
 	dates, err := model.GetQuotaDataGroupByChannel(startTimestamp, endTimestamp)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func GetQuotaDataGroupByChannel(c *gin.Context) {
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
// 判断时间跨度是否超过 1 个月
if endTimestamp-startTimestamp > 2592000 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "时间跨度不能超过 1 个月",
})
dates, err := model.GetQuotaDataGroupByChannel(startTimestamp, endTimestamp)
if err != nil {
common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": dates,
})
}
func GetQuotaDataGroupByChannel(c *gin.Context) {
var startTimestamp int64
var endTimestamp int64
if c.Query("all_time") == "true" {
startTimestamp = 0
endTimestamp = time.Now().Unix()
} else {
startTimestamp, _ = strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTimestamp, _ = strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
}
dates, err := model.GetQuotaDataGroupByChannel(startTimestamp, endTimestamp)
if err != nil {
common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": dates,
})
}
🤖 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/usedata.go` around lines 71 - 84, Update
GetQuotaDataGroupByChannel to parse the all_time query parameter and, when it is
true, avoid applying the zero start/end timestamp range by using the established
all-time handling used by GetAllQuotaDates and GetUserQuotaDates. Preserve the
existing timestamp-based query behavior for non-all-time requests and continue
returning errors through common.ApiError.

Comment thread middleware/body_log.go
Comment on lines +11 to +16
// Sensitive request header names whose values are redacted in capture.
var sensitiveRequestHeaders = map[string]bool{
"authorization": true,
"proxy-authorization": true,
"x-api-key": true,
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact session credentials from both header maps.

Cookie currently falls through request capture, while response capture stores Set-Cookie unredacted. Enabling header capture can therefore persist session credentials in consume logs. Apply one case-insensitive redaction policy before storing either map.

Also applies to: 81-89

🤖 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 `@middleware/body_log.go` around lines 11 - 16, Extend the header-redaction
logic centered on sensitiveRequestHeaders to cover Cookie and Set-Cookie, and
apply it case-insensitively before storing either request or response header
map. Ensure both session-credential headers are replaced with the existing
redacted representation in capture logs.

Comment thread middleware/body_log.go
Comment on lines +69 to +76
// --- Response: wrap writer ---
var wrapper *responseBodyWriter
if common.StoreResponseBodyEnabled {
wrapper = &responseBodyWriter{
ResponseWriter: c.Writer,
buf: &bytes.Buffer{},
}
c.Writer = wrapper

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound capture memory before buffering response bodies. BodyFileThreshold is enforced only when RecordConsumeLog runs, after these paths have already read or accumulated the complete response in RAM. Large downloads or long-lived streams can therefore exhaust memory and delay forwarding.

  • middleware/body_log.go#L69-L76: spool or cap client-response capture instead of retaining an unbounded bytes.Buffer.
  • relay/audio_handler.go#L59-L64: avoid unrestricted full-body reads.
  • relay/claude_handler.go#L211-L225: use bounded/spooled capture for both branches.
  • relay/compatible_handler.go#L208-L225: use bounded/spooled capture for both branches.
  • relay/embedding_handler.go#L79-L85: avoid unrestricted full-body reads.
  • relay/gemini_handler.go#L190-L205: use bounded/spooled capture for both branches.
  • relay/gemini_handler.go#L308-L314: avoid unrestricted full-body reads.
  • relay/image_handler.go#L101-L107: avoid unrestricted full-body reads.
  • relay/rerank_handler.go#L91-L97: avoid unrestricted full-body reads.
  • relay/responses_handler.go#L137-L152: use bounded/spooled capture for both branches.
📍 Affects 9 files
  • middleware/body_log.go#L69-L76 (this comment)
  • relay/audio_handler.go#L59-L64
  • relay/claude_handler.go#L211-L225
  • relay/compatible_handler.go#L208-L225
  • relay/embedding_handler.go#L79-L85
  • relay/gemini_handler.go#L190-L205
  • relay/gemini_handler.go#L308-L314
  • relay/image_handler.go#L101-L107
  • relay/rerank_handler.go#L91-L97
  • relay/responses_handler.go#L137-L152
🤖 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 `@middleware/body_log.go` around lines 69 - 76, Bound or spool response-body
capture before data is accumulated, using BodyFileThreshold or the established
capture mechanism rather than unbounded memory buffering. Update
middleware/body_log.go:69-76 around responseBodyWriter,
relay/audio_handler.go:59-64, relay/embedding_handler.go:79-85,
relay/gemini_handler.go:308-314, relay/image_handler.go:101-107, and
relay/rerank_handler.go:91-97 to avoid unrestricted full-body reads; update both
capture branches in relay/claude_handler.go:211-225,
relay/compatible_handler.go:208-225, relay/gemini_handler.go:190-205, and
relay/responses_handler.go:137-152. Ensure forwarding remains timely while
RecordConsumeLog receives only bounded or spooled captured data.

Comment thread router/relay-router.go
playgroundRouter.Use(middleware.RouteTag("relay"))
playgroundRouter.Use(middleware.SystemPerformanceCheck())
playgroundRouter.Use(middleware.UserAuth(), middleware.Distribute())
playgroundRouter.Use(middleware.UserAuth(), middleware.Distribute(), middleware.HeaderCapture())

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact sensitive response headers before enabling capture.

middleware/body_log.go stores every response header, unlike request headers. This can persist Set-Cookie or provider-issued credentials in long-lived usage logs.

  • router/relay-router.go#L65-L65: use HeaderCapture only after response-header redaction covers sensitive headers.
  • router/relay-router.go#L77-L85: use HeaderCapture only after response-header redaction covers sensitive headers.
  • router/relay-router.go#L182-L182: use HeaderCapture only after response-header redaction covers sensitive headers.
  • router/relay-router.go#L194-L194: use HeaderCapture only after response-header redaction covers sensitive headers.
  • router/relay-router.go#L205-L205: use HeaderCapture only after response-header redaction covers sensitive headers.
📍 Affects 1 file
  • router/relay-router.go#L65-L65 (this comment)
  • router/relay-router.go#L77-L85
  • router/relay-router.go#L182-L182
  • router/relay-router.go#L194-L194
  • router/relay-router.go#L205-L205
🤖 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 `@router/relay-router.go` at line 65, Update each middleware chain in
router/relay-router.go at lines 65, 77-85, 182, 194, and 205 so response-header
redaction runs before HeaderCapture. Ensure the redaction covers sensitive
headers such as Set-Cookie and provider-issued credentials, then retain
HeaderCapture only after that protection is applied.

Comment on lines +64 to +81
const defaultDays = MAX_CHART_TREND_POINTS
const timeRange = useMemo(() => {
const { start, end } = getRollingDateRange(defaultDays)
return { start_timestamp: start, end_timestamp: end }
}, [defaultDays])

const chartDataQuery = useQuery({
queryKey: ['dashboard', 'overview', 'daily-overview-chart', timeRange],
queryFn: async () => {
const result = await getUserQuotaDates({
start_timestamp: timeRange.start_timestamp,
end_timestamp: timeRange.end_timestamp,
default_time: 'day',
})
return result.success ? (result.data ?? []) : []
},
staleTime: 60 * 1000,
})

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

start_timestamp/end_timestamp are passed as Date, but the API contract expects Unix seconds (number).

getRollingDateRange returns Date objects, so timeRange.start_timestamp/end_timestamp are Dates. getUserQuotaDates (web/src/features/dashboard/api.ts) types these params as number (seconds), matching how channel-charts.tsx (Math.floor(startTs.getTime() / 1000)) and log-stat-cards.tsx build them. This is a TypeScript type error (fails the required type check) and at runtime the Date serializes to an ISO string, so the backend receives malformed params and the panel silently renders nothing.

🐛 Convert to Unix seconds
   const timeRange = useMemo(() => {
     const { start, end } = getRollingDateRange(defaultDays)
-    return { start_timestamp: start, end_timestamp: end }
+    return {
+      start_timestamp: Math.floor(start.getTime() / 1000),
+      end_timestamp: Math.floor(end.getTime() / 1000),
+    }
   }, [defaultDays])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultDays = MAX_CHART_TREND_POINTS
const timeRange = useMemo(() => {
const { start, end } = getRollingDateRange(defaultDays)
return { start_timestamp: start, end_timestamp: end }
}, [defaultDays])
const chartDataQuery = useQuery({
queryKey: ['dashboard', 'overview', 'daily-overview-chart', timeRange],
queryFn: async () => {
const result = await getUserQuotaDates({
start_timestamp: timeRange.start_timestamp,
end_timestamp: timeRange.end_timestamp,
default_time: 'day',
})
return result.success ? (result.data ?? []) : []
},
staleTime: 60 * 1000,
})
const defaultDays = MAX_CHART_TREND_POINTS
const timeRange = useMemo(() => {
const { start, end } = getRollingDateRange(defaultDays)
return {
start_timestamp: Math.floor(start.getTime() / 1000),
end_timestamp: Math.floor(end.getTime() / 1000),
}
}, [defaultDays])
const chartDataQuery = useQuery({
queryKey: ['dashboard', 'overview', 'daily-overview-chart', timeRange],
queryFn: async () => {
const result = await getUserQuotaDates({
start_timestamp: timeRange.start_timestamp,
end_timestamp: timeRange.end_timestamp,
default_time: 'day',
})
return result.success ? (result.data ?? []) : []
},
staleTime: 60 * 1000,
})
🤖 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/daily-overview-panel.tsx`
around lines 64 - 81, Convert the Date values returned by getRollingDateRange in
the timeRange useMemo to Unix seconds numbers before passing them to
getUserQuotaDates. Update start_timestamp and end_timestamp using the
established getTime()/1000 conversion with flooring, while preserving the
existing query key and date-range behavior.

Source: Coding guidelines

Comment on lines +418 to +424
metricMode={chartPreferences.metricMode}
onMetricModeChange={(mode) =>
handleChartPreferencesChange({
...chartPreferences,
metricMode: mode,
})
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Switching metric mode resets the applied time-range filter.

onMetricModeChange routes through handleChartPreferencesChange, which calls setModelFilters(buildDefaultDashboardFilters(preferences)) (Lines 256-263). Since metricMode is a purely client-side display toggle over already-fetched data (ModelCharts selects count vs token specs from the same processChartData output), rebuilding filters discards any custom/quick time range the user applied and forces a data refetch on every count↔tokens switch. Consider updating/persisting metricMode without rebuilding modelFilters.

🤖 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/index.tsx` around lines 418 - 424, The metricMode
change handler must preserve the current modelFilters instead of routing through
handleChartPreferencesChange, which rebuilds default filters and triggers a
refetch. Update the onMetricModeChange path and its preference persistence so
only the client-side metricMode setting changes while the applied custom or
quick time range remains intact.

Comment on lines +279 to +280
export type MetricMode = 'count' | 'tokens'

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Duplicate MetricMode declaration — this fails type check.

MetricMode is already declared at Line 199 in this same module. Declaring it again here produces TS2300: Duplicate identifier 'MetricMode' and breaks the build. Remove this second definition.

🐛 Proposed fix
-export type MetricMode = 'count' | 'tokens'
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type MetricMode = 'count' | 'tokens'
🤖 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/types.ts` around lines 279 - 280, Remove the
duplicate MetricMode type declaration near the later section of the module,
keeping the existing declaration earlier in the file as the single source of
truth.

Source: Coding guidelines

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