Skip to content

Teammate/quota runtime faq fixes - #4573

Closed
jkjk02 wants to merge 18 commits into
QuantumNous:mainfrom
Micah-Zheng:teammate/quota-runtime-faq-fixes
Closed

Teammate/quota runtime faq fixes#4573
jkjk02 wants to merge 18 commits into
QuantumNous:mainfrom
Micah-Zheng:teammate/quota-runtime-faq-fixes

Conversation

@jkjk02

@jkjk02 jkjk02 commented May 1, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

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

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

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

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

Summary by CodeRabbit

  • New Features

    • Added Model Square for browsing and filtering pricing details
    • Added Status Monitor page for system monitoring
    • Enhanced dashboard with uptime tracking and status display
    • Added API request URL card for easier API key management
    • Expanded System Settings access from super-admin to admin users and above
    • Added recharge pricing display with discount tier information
  • Improvements

    • Enhanced role-based access control for admin operations
    • Improved API key form validation and grouping
    • Better navigation link handling with search parameters
  • Localization

    • Added translations for new features across multiple languages
  • Documentation

    • Added private deployment procedures guide

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR centralizes role permission handling by introducing HasRootPermission and EffectiveRole helper functions in the backend, updates authorization checks across controllers to use these utilities, and adjusts the RootAuth middleware threshold. It also adds extensive frontend enhancements including new routes for model-square and status-monitor pages, custom navigation system, dashboard uptime tracking, API request URL card, recharge pricing features, and supporting internationalization entries.

Changes

Cohort / File(s) Summary
Backend Role Permission System
common/constants.go, middleware/auth.go, controller/custom_oauth.go, controller/twofa.go, controller/user.go, model/user.go
Introduces HasRootPermission() and EffectiveRole() helpers in constants, updates authorization logic across controllers to replace direct role comparisons with HasRootPermission(), reduces RootAuth middleware threshold from RoleRootUser to RoleAdminUser, and consolidates sidebar permission computation to treat all root-permission roles uniformly.
Configuration & Documentation
.gitignore, docs/private-deploy-sop.md
Adds three additional ignore paths (.omx/, .playwright-mcp/, web/default/.omc/) and creates deployment SOP documentation for private customization workflow.
Navigation & Routing Infrastructure
web/default/src/components/layout/types.ts, web/default/src/components/layout/components/nav-group.tsx, web/default/src/components/layout/components/top-nav.tsx, web/default/src/components/layout/components/workspace-switcher.tsx, web/default/src/hooks/use-sidebar-data.ts, web/default/src/hooks/use-top-nav-links.ts, web/default/src/routeTree.gen.ts
Adds URL parsing helpers (splitUrl, splitHref) to support pathname/search separation for TanStack Router, integrates custom sidebar/top-nav links, updates workspace switcher role gating to use ROLE.ADMIN instead of ROLE.SUPER_ADMIN, and generates three new authenticated routes (/model-square/, /model-square/$modelId/, /status-monitor).
API Keys Features
web/default/src/features/keys/constants.ts, web/default/src/features/keys/lib/api-key-form.ts, web/default/src/features/keys/lib/index.ts, web/default/src/features/keys/components/api-key-group-combobox.tsx, web/default/src/features/keys/components/api-keys-mutate-drawer.tsx, web/default/src/features/keys/components/api-request-url-card.tsx, web/default/src/features/keys/index.tsx
Changes DEFAULT_GROUP from 'auto' to empty string, adds getApiKeyFormDefaultValues() helper for conditional auto-group initialization, introduces ApiRequestUrlCard component for displaying normalized server endpoint, updates ApiKeyGroupCombobox to expose error state, and refines form validation to require non-empty group field.
Pricing & Model Pages
web/default/src/features/pricing/hooks/use-filters.ts, web/default/src/features/pricing/components/pricing-table.tsx, web/default/src/features/pricing/components/model-details.tsx, web/default/src/features/pricing/index.tsx, web/default/src/routes/_authenticated/model-square/index.tsx, web/default/src/routes/_authenticated/model-square/$modelId/index.tsx
Refactors pricing page to support dynamic routing (routeTo parameter), adds onModelClick callback to PricingTable for flexible navigation, makes ModelDetails embeddable with custom route context, implements new /model-square/ landing and detail routes with Zod-validated search parameters, and normalizes filter search-param handling with defaults.
Wallet & Recharge Features
web/default/src/features/wallet/lib/format.ts, web/default/src/features/wallet/components/affiliate-rewards-card.tsx, web/default/src/features/wallet/components/recharge-form-card.tsx, web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx, web/default/src/features/wallet/index.tsx
Adds formatCnyAmount() utility for Chinese Yuan formatting, repurposes AffiliateRewardsCard to display recharge discount tiers instead of referral stats, switches payment UI currency display to formatCnyAmount, removes affiliate transfer dialog and useAffiliate() hook integration, and updates parent Wallet component to derive pricing/ratio data from status.
System & Settings Routes
web/default/src/routes/_authenticated/status-monitor.tsx, web/default/src/routes/_authenticated/system-settings/route.tsx, web/default/src/components/profile-dropdown.tsx, web/default/src/lib/roles.ts
Introduces /status-monitor route with iframe to external monitoring service, updates system-settings access guard to allow ROLE.ADMIN and above instead of only SUPER_ADMIN, revises getRoleLabelKey() to return SUPER_ADMIN label for any role >= ROLE.ADMIN, and changes profile dropdown to gate system-settings visibility to ROLE.ADMIN threshold.
Dashboard & Uptime Tracking
web/default/src/features/dashboard/components/overview/summary-cards.tsx, web/default/src/features/dashboard/hooks/use-dashboard-config.tsx
Adds periodic uptime refresh (60s interval), introduces formatUptimeDuration() to display elapsed time, extends summary card totals with uptimeDisplay/uptimeSinceDisplay, augments dashboard config with uptime card, and adjusts large-screen grid from 3 to 4 columns.
Custom Site Configuration
web/default/src/custom/site.ts
Adds typed custom navigation links (customSidebarLinks, customTopNavLinks) with icon support, defines customHeaderNavModuleDefaults with status monitor flag, and exports formatCustomPaymentAmount() utility for yen formatting.
Branding & Constants
web/default/index.html, web/default/src/lib/constants.ts
Updates favicon href from /logo.png to /logo-custom.png and changes DEFAULT_LOGO constant to reference /logo-custom.png.
Internationalization
web/default/src/i18n/locales/en.json, web/default/src/i18n/locales/fr.json, web/default/src/i18n/locales/ja.json, web/default/src/i18n/locales/ru.json, web/default/src/i18n/locales/vi.json, web/default/src/i18n/locales/zh.json
Adds translation keys for new UI features: "Model Square", "Status Monitor", "API Request URL" actions, recharge pricing information (original price, discount tiers, recharge rates), and localized variants across six languages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • 新增"顶栏"、"侧边栏"管理功能 #1701: Both PRs modify role/permission authorization logic and update sidebar/header module configuration by adding or normalizing how root/admin permission checks are enforced and how sidebar/header defaults are computed across controllers, models, and frontend hooks.

Suggested reviewers

  • Calcium-Ion
  • creamlike1024

Poem

🐰 Roles now dance with graceful new names,
Permission helpers tame the admin games,
From root to admin, cleaner flows take flight,
Model Square awaits, uptime burns bright!
Custom colors shine, the system feels right.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Teammate/quota runtime faq fixes' is vague and does not clearly summarize the main changes in the changeset. The title uses non-descriptive terms that don't convey meaningful information about what was actually changed. Consider revising the title to be more specific and descriptive, such as 'Add admin role customizations, pricing UI improvements, and API key enhancements' or similar to better reflect the actual scope of changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch teammate/quota-runtime-faq-fixes

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

@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: 17

Caution

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

⚠️ Outside diff range comments (4)
model/user.go (1)

130-145: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the repo JSON wrapper in this touched path.

generateDefaultSidebarConfigForRole() still marshals with json.Marshal, but this codebase requires Go business code to go through common/json.go. Please switch this function to common.Marshal while touching it.

♻️ Suggested fix
-	configBytes, err := json.Marshal(defaultConfig)
+	configBytes, err := common.Marshal(defaultConfig)

As per coding guidelines, "All JSON marshal/unmarshal operations MUST use wrapper functions in common/json.go ... Do NOT directly import or call encoding/json in business code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/user.go` around lines 130 - 145, In generateDefaultSidebarConfigForRole
replace the direct call to json.Marshal with the repo wrapper by calling
common.Marshal(defaultConfig) (assigning to configBytes, err as before), and
ensure the file imports the common package instead of using encoding/json
directly; preserve the existing error handling using the returned err and any
subsequent logic that follows configBytes.
controller/user.go (1)

433-443: ⚠️ Potential issue | 🟠 Major

Remove hardcoded admin access and respect SidebarModulesAdmin configuration.

The generateDefaultSidebarConfig function grants full admin section access (setting: true) to all users with root permission without consulting the SidebarModulesAdmin configuration. This bypasses the sidebar management system designed to provide granular admin permission control. Additionally, json.Marshal on line ~495 should use common.Marshal() from common/json.go per coding guidelines.

Refactor to:

  1. Read SidebarModulesAdmin from common.OptionMap and parse it to determine which admin modules are actually enabled
  2. Only include admin sections that are configured as accessible
  3. Use common.Marshal() instead of direct json.Marshal()

This also applies to the permissions assignment at line 440.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/user.go` around lines 433 - 443, The current code in
generateDefaultSidebarConfig grants full admin access to root users and
hardcodes admin module settings; change it to read the "SidebarModulesAdmin"
value from common.OptionMap, parse it into the admin modules map, and only
include admin sections present/enabled in that parsed configuration when
building sidebar_modules for both root and non-root branches (do not
unconditionally set setting:true or admin:true). Replace any direct json.Marshal
calls in this function with common.Marshal() to produce JSON per project
conventions. Update the permissions assignment where sidebar_modules is set so
it uses the parsed admin modules map (or an empty map if the option is
missing/invalid) instead of hardcoded values.
web/default/src/features/keys/components/api-keys-mutate-drawer.tsx (2)

156-168: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid resetting the create form when useStatus() finishes loading.

defaultUseAutoGroup is async, so this effect reruns when the status query resolves or refetches. If the drawer is already open in create mode, form.reset(...) will wipe whatever the user has typed so far. Limit this reset to the initial open, or guard it behind a dirty-state check.

Suggested guard
-    } else if (open && !isUpdate) {
+    } else if (open && !isUpdate && !form.formState.isDirty) {
       // For create, reset to defaults
       form.reset(getApiKeyFormDefaultValues(defaultUseAutoGroup))
     }
-  }, [open, isUpdate, currentRow, form, defaultUseAutoGroup])
+  }, [open, isUpdate, currentRow, form, defaultUseAutoGroup, form.formState.isDirty])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/features/keys/components/api-keys-mutate-drawer.tsx` around
lines 156 - 168, The effect in useEffect that calls form.reset (via
getApiKey(...).then(...) and getApiKeyFormDefaultValues(defaultUseAutoGroup)) is
rerunning when the async defaultUseAutoGroup resolves and unintentionally wipes
user input; change the guard so reset only happens on the initial open
transition or when the form is pristine: detect the open transition
(previousOpen false -> open true) or check form.isDirty() before calling
form.reset, and keep the existing branches for isUpdate/currentRow and create
mode (getApiKey, form.reset(transformApiKeyToFormDefaults(...)), and
getApiKeyFormDefaultValues) but skip resetting if the drawer was already open or
the form is dirty.

128-147: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t drive group availability off a localized desc string.

Filtering out entries whose desc is '用户分组' can leave the combobox with no valid options, and group is now a required field. It also makes the behavior depend on backend copy text, so any wording change silently changes which groups users are allowed to select. Use a stable flag/key from the API instead, or at least preserve the current value in the options list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/features/keys/components/api-keys-mutate-drawer.tsx` around
lines 128 - 147, The code filters group options by the localized info.desc ===
'用户分组', which is unstable; change the predicate on groupsRaw to use a stable API
flag/key (e.g., info.hidden === true or info.isUserGroup === true) instead of
info.desc, and avoid dropping the currently selected group: after building
groups from Object.entries(groupsRaw), ensure the form's current group value
(e.g., the component prop or form initial value named group/currentGroup) is
preserved by adding it back if missing (use groups.some(g => g.value ===
currentGroup) and push/unshift the missing option from groupsRaw). Also keep the
existing defaultUseAutoGroup logic that injects the 'auto' option into groups.
🧹 Nitpick comments (4)
docs/private-deploy-sop.md (1)

270-271: ⚡ Quick win

Validate compose syntax before restart.

Add docker compose config -q before up -d to fail fast on malformed docker-compose.yml edits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/private-deploy-sop.md` around lines 270 - 271, Update the restart
command so it validates the compose file first: run "docker compose config -q"
(or "sudo docker compose config -q" if sudo is required) and only proceed to
"sudo docker compose up -d new-api" when the validation succeeds; in practice,
add the validation step immediately before the existing "sudo docker compose up
-d new-api" command to fail fast on malformed docker-compose.yml edits.
web/default/src/i18n/locales/ja.json (1)

2069-2069: 💤 Low value

Consider alternative translation for "Model Square"

The translation "モデル広場" (model hiroba/square) is a literal translation that may not convey the intended meaning clearly in a technical context. Consider these alternatives:

  • "モデルスクエア" (katakana for "Model Square") - More common for feature names in Japanese UX
  • "モデル一覧" (model list) - If this refers to a model listing/catalog
  • "モデルマーケット" (model market) - If this is a marketplace concept

The current translation is grammatically correct but may confuse users expecting a more standard UI term.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/i18n/locales/ja.json` at line 2069, The current Japanese
translation for the key "Model Square" uses a literal phrase "モデル広場" which may
be unclear in a UX/technical context; update the value for the "Model Square"
key in the ja.json locale to a more standard UI term such as "モデルスクエア"
(katakana), "モデル一覧" (if it represents a listing), or "モデルマーケット" (if it's a
marketplace) depending on the intended meaning—replace the string "モデル広場" with
the chosen alternative while keeping the JSON key "Model Square" unchanged.
web/default/src/features/pricing/components/model-details.tsx (1)

466-470: ⚡ Quick win

Couple routeFrom and backPath in a single typed contract.

Line 466-Line 470 currently allows invalid pairings (e.g. pricing route + model-square back path), which can leak incompatible search state into Line 500. A discriminated union will prevent accidental misuse at compile time.

♻️ Proposed typing refinement
-type ModelDetailsProps = {
-  embedded?: boolean
-  routeFrom?: '/pricing/$modelId/' | '/_authenticated/model-square/$modelId/'
-  backPath?: '/pricing' | '/model-square'
-}
+type ModelDetailsProps =
+  | {
+      embedded?: boolean
+      routeFrom?: '/pricing/$modelId/'
+      backPath?: '/pricing'
+    }
+  | {
+      embedded?: boolean
+      routeFrom: '/_authenticated/model-square/$modelId/'
+      backPath?: '/model-square'
+    }

Also applies to: 474-477, 500-501

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/features/pricing/components/model-details.tsx` around lines
466 - 470, The props type ModelDetailsProps allows invalid routeFrom/backPath
combinations; replace it with a discriminated union that couples routeFrom and
backPath into matching pairs (e.g. one variant where routeFrom:
'/pricing/$modelId/' and backPath: '/pricing', and another variant where
routeFrom: '/_authenticated/model-square/$modelId/' and backPath:
'/model-square'), then update any references to ModelDetailsProps (including the
other occurrences mentioned around the component and where search state is
derived) so the compiler enforces valid pairings and prevents leaking
incompatible search state into the logic that reads these props.
web/default/src/custom/site.ts (1)

17-35: ⚡ Quick win

Use scoped i18n keys here instead of raw labels.

titleKey is acting as a translation key, so adding values like 'Model Square' and 'Status Monitor' keeps pushing the locale files toward inconsistent ad-hoc keys. Prefer semantically scoped keys such as nav.modelSquare / nav.statusMonitor here and in the locale JSON.

Based on learnings: Applies to web/default/**/*.{ts,tsx}: Use hierarchical, semantically clear translation keys with consistent naming (e.g., dashboard.overview.title).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/custom/site.ts` around lines 17 - 35, Replace literal label
strings used as translation keys in customSidebarLinks and customTopNavLinks
with scoped, semantic i18n keys (e.g., change titleKey: 'Model Square' to
titleKey: 'nav.modelSquare' and 'Status Monitor' to 'nav.statusMonitor'); update
the corresponding locale JSON entries under those scoped keys (and follow the
project convention like dashboard.overview.title when appropriate) so lookups
remain consistent across web/default/**/*.{ts,tsx}; ensure each object in
customSidebarLinks and customTopNavLinks uses the new hierarchical keys for
titleKey and verify consumers call the i18n lookup with those keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@common/constants.go`:
- Around line 175-184: Change HasRootPermission to only return true for the
actual root role (role == RoleRootUser) and add a new HasAdminPermission(role
int) bool that returns role >= RoleAdminUser; update EffectiveRole to rely on
the (new) HasRootPermission so only true root maps to RoleRootUser. After this
change, replace uses that intend “admin-area” access with HasAdminPermission
(e.g., admin menus/routes), but keep same-or-higher protections and root-only
checks using HasRootPermission (so callers like controller/twofa.go and
controller/custom_oauth.go that must remain root-only will not treat admins as
root).

In `@docs/private-deploy-sop.md`:
- Around line 329-344: The docs currently instruct to run git push origin
private/custom-ui after syncing upstream, which contradicts the PR-only rule for
the private/custom-ui branch; update the Upstream-sync instructions for the
private/custom-ui flow to remove the direct push and instead instruct the user
to push their local changes to a feature/sync branch (or the same branch if your
policy allows remote branches) and open a pull request for merging into
private/custom-ui—reference the branch name private/custom-ui in the text and
replace the final "git push origin private/custom-ui" step with a short line
telling readers to push their sync branch and create a PR for merging into
private/custom-ui per repo governance.
- Around line 255-267: The current loop replaces the first image: line
containing "new-api" which can hit the wrong service; instead locate the new-api
service block first by finding the service header line (match r'^\s*new-api:')
and note its indentation, then scan subsequent lines within that block (stop
when encountering a line with indentation less than or equal to the service
header or a new top-level service) and replace the image: line inside that block
only; if no new-api header or no image: line in the block is found, raise a
clear error.

In `@middleware/auth.go`:
- Around line 182-186: RootAuth() was relaxed from RoleRootUser to RoleAdminUser
causing sensitive endpoints to be exposed; restore/controller-level enforcement
by adding explicit role checks in the two handlers: in GetChannelKey and
FetchModels call authHelper or directly verify c.GetInt("role") (or equivalent)
against RoleRootUser (not RoleAdminUser) and abort with 403 when below
RoleRootUser, ensuring no other code path returns secrets for non-root users.
Also fix the misleading HasRootPermission()—either change its implementation to
return role >= RoleRootUser or rename it to HasAdminOrRootPermission() and
update all call sites to use the correct semantic; document any intentional
deviation if you opt to keep admin access.

In `@web/default/src/components/layout/components/nav-group.tsx`:
- Around line 142-146: The code currently converts query params with
Object.fromEntries(new URLSearchParams(search)) which forces all values to
strings and breaks Zod-based route validateSearch schemas; replace those
conversions in nav-group.tsx (the spots using Object.fromEntries(new
URLSearchParams(search))) by passing the raw search string into the route's
validateSearch (or calling route.validateSearch?.(search) and using its result)
so the route-level validator can coerce types correctly (e.g.,
booleans/enums/numbers) and only fall back to undefined when validation fails.

In `@web/default/src/components/layout/components/top-nav.tsx`:
- Around line 18-25: splitHref currently uses href.split('?') which loses parts
after the first '?' in query values; change splitHref to find the first '?' with
href.indexOf('?') and use slice to set pathname = href.slice(0, idx) (or the
whole href if idx === -1) and search = idx === -1 ? '' : href.slice(idx + 1) so
the entire query string (including any additional '?' characters in parameter
values) is preserved when passed to the Link components referenced in the file
(splitHref).

In `@web/default/src/features/dashboard/components/overview/summary-cards.tsx`:
- Around line 30-38: The translation keys for singular vs plural are
inconsistently cased in the duration formatter (variables days, hours, minutes
and the parts.push calls using t(...)), which can break i18n lookups; update the
three calls to t(...) to use a consistent key casing (e.g., use 'day'/'days',
'hour'/'hours', 'minute'/'minutes' or 'Day'/'Days'/'Hour'/'Hours' consistently)
for both singular and plural branches and ensure your translation files define
the chosen keys.

In `@web/default/src/features/keys/components/api-key-group-combobox.tsx`:
- Line 140: The ChevronsUpDown icon (and the other decorative icon rendered on
the same component around line 165) are purely visual and should be hidden from
assistive tech; update the JSX where ChevronsUpDown and the other decorative
icon are rendered in the ApiKeyGroupCombobox component to include
aria-hidden="true" (and ensure they are not focusable) so screen readers ignore
them while preserving visual appearance.

In `@web/default/src/features/pricing/components/model-details.tsx`:
- Line 554: The ArrowLeft icon in the Back button is decorative and causing
redundant screen-reader output; update the JSX where ArrowLeft is rendered (in
model-details.tsx, inside the Back button with visible text "Back") to add
aria-hidden="true" to the ArrowLeft element so assistive tech ignores the icon
while the button text remains the accessible label.

In `@web/default/src/features/pricing/components/pricing-table.tsx`:
- Around line 69-73: The handleRowClick callback currently calls
onModelClick(model.model_name || '') which can pass an empty string; change
handleRowClick to first check the PricingModel's model_name and no-op if it's
falsy (undefined/empty) so onModelClick is only invoked with a valid id. Update
the logic inside handleRowClick (referencing handleRowClick, PricingModel, and
onModelClick) and keep the useCallback dependency array unchanged.

In `@web/default/src/features/wallet/components/affiliate-rewards-card.tsx`:
- Around line 25-26: The savedAmount calculation is inverted: instead of
computing the discounted price, set savedAmount to originalPrice *
numericDiscount so it represents the amount saved (use the existing variables
originalPrice and numericDiscount); update the expression in
affiliate-rewards-card.tsx where savedAmount is defined (currently using
originalPrice * (1 - numericDiscount)) to use originalPrice * numericDiscount,
and ensure numericDiscount is the expected fraction (0-1) before the calculation
if needed.

In `@web/default/src/hooks/use-top-nav-links.ts`:
- Around line 76-80: The Pricing nav item currently uses href
'/model-square?view=table' regardless of auth and only sets disabled from
pricing.requireAuth; update the logic in useTopNavLinks (where links.push is
called for the Pricing item) to prevent guests from navigating to an auth-gated
route by either (a) marking the item disabled when pricing.requireAuth is true
and the user is not authenticated (use your auth flag/isAuthenticated), or (b)
when the user is unauthenticated, change the href to a safe entry such as the
login/authorize flow with a next/redirect param to '/model-square?view=table' so
clicks route to authentication first; ensure you reference pricing.requireAuth
and the auth state used elsewhere in use-top-nav-links when implementing this
change.

In `@web/default/src/i18n/locales/ja.json`:
- Around line 2069-2071: Add the missing dynamic title keys "Model Square" and
"Status Monitor" to the STATIC_I18N_KEYS array in src/i18n/static-keys.ts so the
extractor picks them up; specifically update the STATIC_I18N_KEYS constant to
include the exact key strings "Model Square" and "Status Monitor" (translations
already exist in ja.json) to align with their use as dynamic titleKey values in
site.ts.

In `@web/default/src/lib/roles.ts`:
- Around line 21-24: getRoleLabelKey currently normalizes ROLE.ADMIN to show the
SUPER_ADMIN label; restore direct label mapping by removing the special-case (do
not map ROLE.ADMIN to ROLE.SUPER_ADMIN) and return ROLE_LABEL_KEYS[role as
RoleValue] ?? ROLE_LABEL_KEYS[DEFAULT_ROLE] in getRoleLabelKey. Add a separate
helper function canAccessSystemSettings(role?: number): boolean that performs
the permission normalization (e.g., (role ?? DEFAULT_ROLE) >= ROLE.ADMIN) and
update route/menu gating call sites to use canAccessSystemSettings(...) instead
of relying on getRoleLabelKey for access checks.

In `@web/default/src/routes/_authenticated/model-square/`$modelId/index.tsx:
- Around line 6-16: The search schema modelSquareDetailsSearchSchema is missing
the caller's "view" param so opening ModelDetails from the table view loses
state; update modelSquareDetailsSearchSchema to include view:
z.string().optional() (or a z.enum of allowed views if you want stricter typing)
so the parsed route/search preserves and returns the original ?view value when
ModelDetails (and its navigation code that reads the search params) navigates
back.

In `@web/default/src/routes/_authenticated/status-monitor.tsx`:
- Around line 1-3: Replace the hardcoded iframe title with a localized string:
import and call useTranslation() in the component that defines the route (the
module using createFileRoute) and change title='Status Monitor' to
title={t('statusMonitor.title')} (or a suitable key), and similarly wrap any
other user-facing strings in that component (lines ~10-19) with t('...'); ensure
you add the import { useTranslation } from 'react-i18next' and use the t
function from const { t } = useTranslation() so AppHeader/Main/iframe use
localized text.
- Around line 16-20: The iframe embedding STATUS_MONITOR_URL lacks
isolation/privacy attributes; update the JSX <iframe> element to include a
restrictive sandbox attribute (e.g., sandbox with only the minimal needed flags
such as "allow-scripts" only if the monitor requires scripts, otherwise an empty
sandbox) and add referrerPolicy="no-referrer" (or "same-origin" if required by
the monitor) to prevent leaking the parent referrer; modify the iframe that uses
STATUS_MONITOR_URL and keep the existing title and className while choosing the
minimal sandbox flags required by the external monitor.

---

Outside diff comments:
In `@controller/user.go`:
- Around line 433-443: The current code in generateDefaultSidebarConfig grants
full admin access to root users and hardcodes admin module settings; change it
to read the "SidebarModulesAdmin" value from common.OptionMap, parse it into the
admin modules map, and only include admin sections present/enabled in that
parsed configuration when building sidebar_modules for both root and non-root
branches (do not unconditionally set setting:true or admin:true). Replace any
direct json.Marshal calls in this function with common.Marshal() to produce JSON
per project conventions. Update the permissions assignment where sidebar_modules
is set so it uses the parsed admin modules map (or an empty map if the option is
missing/invalid) instead of hardcoded values.

In `@model/user.go`:
- Around line 130-145: In generateDefaultSidebarConfigForRole replace the direct
call to json.Marshal with the repo wrapper by calling
common.Marshal(defaultConfig) (assigning to configBytes, err as before), and
ensure the file imports the common package instead of using encoding/json
directly; preserve the existing error handling using the returned err and any
subsequent logic that follows configBytes.

In `@web/default/src/features/keys/components/api-keys-mutate-drawer.tsx`:
- Around line 156-168: The effect in useEffect that calls form.reset (via
getApiKey(...).then(...) and getApiKeyFormDefaultValues(defaultUseAutoGroup)) is
rerunning when the async defaultUseAutoGroup resolves and unintentionally wipes
user input; change the guard so reset only happens on the initial open
transition or when the form is pristine: detect the open transition
(previousOpen false -> open true) or check form.isDirty() before calling
form.reset, and keep the existing branches for isUpdate/currentRow and create
mode (getApiKey, form.reset(transformApiKeyToFormDefaults(...)), and
getApiKeyFormDefaultValues) but skip resetting if the drawer was already open or
the form is dirty.
- Around line 128-147: The code filters group options by the localized info.desc
=== '用户分组', which is unstable; change the predicate on groupsRaw to use a stable
API flag/key (e.g., info.hidden === true or info.isUserGroup === true) instead
of info.desc, and avoid dropping the currently selected group: after building
groups from Object.entries(groupsRaw), ensure the form's current group value
(e.g., the component prop or form initial value named group/currentGroup) is
preserved by adding it back if missing (use groups.some(g => g.value ===
currentGroup) and push/unshift the missing option from groupsRaw). Also keep the
existing defaultUseAutoGroup logic that injects the 'auto' option into groups.

---

Nitpick comments:
In `@docs/private-deploy-sop.md`:
- Around line 270-271: Update the restart command so it validates the compose
file first: run "docker compose config -q" (or "sudo docker compose config -q"
if sudo is required) and only proceed to "sudo docker compose up -d new-api"
when the validation succeeds; in practice, add the validation step immediately
before the existing "sudo docker compose up -d new-api" command to fail fast on
malformed docker-compose.yml edits.

In `@web/default/src/custom/site.ts`:
- Around line 17-35: Replace literal label strings used as translation keys in
customSidebarLinks and customTopNavLinks with scoped, semantic i18n keys (e.g.,
change titleKey: 'Model Square' to titleKey: 'nav.modelSquare' and 'Status
Monitor' to 'nav.statusMonitor'); update the corresponding locale JSON entries
under those scoped keys (and follow the project convention like
dashboard.overview.title when appropriate) so lookups remain consistent across
web/default/**/*.{ts,tsx}; ensure each object in customSidebarLinks and
customTopNavLinks uses the new hierarchical keys for titleKey and verify
consumers call the i18n lookup with those keys.

In `@web/default/src/features/pricing/components/model-details.tsx`:
- Around line 466-470: The props type ModelDetailsProps allows invalid
routeFrom/backPath combinations; replace it with a discriminated union that
couples routeFrom and backPath into matching pairs (e.g. one variant where
routeFrom: '/pricing/$modelId/' and backPath: '/pricing', and another variant
where routeFrom: '/_authenticated/model-square/$modelId/' and backPath:
'/model-square'), then update any references to ModelDetailsProps (including the
other occurrences mentioned around the component and where search state is
derived) so the compiler enforces valid pairings and prevents leaking
incompatible search state into the logic that reads these props.

In `@web/default/src/i18n/locales/ja.json`:
- Line 2069: The current Japanese translation for the key "Model Square" uses a
literal phrase "モデル広場" which may be unclear in a UX/technical context; update
the value for the "Model Square" key in the ja.json locale to a more standard UI
term such as "モデルスクエア" (katakana), "モデル一覧" (if it represents a listing), or
"モデルマーケット" (if it's a marketplace) depending on the intended meaning—replace the
string "モデル広場" with the chosen alternative while keeping the JSON key "Model
Square" unchanged.
🪄 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: 5c230e2c-ef8d-4173-b9b9-399d790b8ecf

📥 Commits

Reviewing files that changed from the base of the PR and between dac55f0 and a70c455.

⛔ Files ignored due to path filters (4)
  • web/default/public/favicon-custom.ico is excluded by !**/*.ico
  • web/default/public/favicon.ico is excluded by !**/*.ico
  • web/default/public/logo-custom.png is excluded by !**/*.png
  • web/default/public/logo.png is excluded by !**/*.png
📒 Files selected for processing (48)
  • .gitignore
  • common/constants.go
  • controller/custom_oauth.go
  • controller/twofa.go
  • controller/user.go
  • docs/private-deploy-sop.md
  • middleware/auth.go
  • model/user.go
  • web/default/index.html
  • web/default/src/components/layout/components/nav-group.tsx
  • web/default/src/components/layout/components/top-nav.tsx
  • web/default/src/components/layout/components/workspace-switcher.tsx
  • web/default/src/components/layout/types.ts
  • web/default/src/components/profile-dropdown.tsx
  • web/default/src/custom/site.ts
  • web/default/src/features/dashboard/components/overview/summary-cards.tsx
  • web/default/src/features/dashboard/hooks/use-dashboard-config.tsx
  • web/default/src/features/keys/components/api-key-group-combobox.tsx
  • web/default/src/features/keys/components/api-keys-mutate-drawer.tsx
  • web/default/src/features/keys/components/api-request-url-card.tsx
  • web/default/src/features/keys/constants.ts
  • web/default/src/features/keys/index.tsx
  • web/default/src/features/keys/lib/api-key-form.ts
  • web/default/src/features/keys/lib/index.ts
  • web/default/src/features/pricing/components/model-details.tsx
  • web/default/src/features/pricing/components/pricing-table.tsx
  • web/default/src/features/pricing/hooks/use-filters.ts
  • web/default/src/features/pricing/index.tsx
  • web/default/src/features/wallet/components/affiliate-rewards-card.tsx
  • web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx
  • web/default/src/features/wallet/components/recharge-form-card.tsx
  • web/default/src/features/wallet/index.tsx
  • web/default/src/features/wallet/lib/format.ts
  • web/default/src/hooks/use-sidebar-data.ts
  • web/default/src/hooks/use-top-nav-links.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
  • web/default/src/lib/constants.ts
  • web/default/src/lib/roles.ts
  • web/default/src/routeTree.gen.ts
  • web/default/src/routes/_authenticated/model-square/$modelId/index.tsx
  • web/default/src/routes/_authenticated/model-square/index.tsx
  • web/default/src/routes/_authenticated/status-monitor.tsx
  • web/default/src/routes/_authenticated/system-settings/route.tsx

Comment thread common/constants.go
Comment on lines +175 to +184
func HasRootPermission(role int) bool {
return role >= RoleAdminUser
}

func EffectiveRole(role int) int {
if HasRootPermission(role) {
return RoleRootUser
}
return role
}

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.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Split admin-access from true root overrides before this leaks more privileges.

HasRootPermission() now returns true for RoleAdminUser, so any caller that used to special-case only RoleRootUser now treats admins as equivalent to root. In this PR that already widens hierarchy bypasses in controller/twofa.go Line 523 and controller/custom_oauth.go Lines 504 and 563, which means an admin can operate on peer/root accounts. Keep a separate “admin-area access” helper and reserve HasRootPermission() for the actual root role.

🔒 Suggested direction
+func HasAdminPermission(role int) bool {
+	return role >= RoleAdminUser
+}
+
 func HasRootPermission(role int) bool {
-	return role >= RoleAdminUser
+	return role == RoleRootUser
 }

Then update only menu/route/admin-area gates to use HasAdminPermission, and keep same-or-higher-user protections on HasRootPermission.

📝 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 HasRootPermission(role int) bool {
return role >= RoleAdminUser
}
func EffectiveRole(role int) int {
if HasRootPermission(role) {
return RoleRootUser
}
return role
}
func HasAdminPermission(role int) bool {
return role >= RoleAdminUser
}
func HasRootPermission(role int) bool {
return role == RoleRootUser
}
func EffectiveRole(role int) int {
if HasRootPermission(role) {
return RoleRootUser
}
return role
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/constants.go` around lines 175 - 184, Change HasRootPermission to only
return true for the actual root role (role == RoleRootUser) and add a new
HasAdminPermission(role int) bool that returns role >= RoleAdminUser; update
EffectiveRole to rely on the (new) HasRootPermission so only true root maps to
RoleRootUser. After this change, replace uses that intend “admin-area” access
with HasAdminPermission (e.g., admin menus/routes), but keep same-or-higher
protections and root-only checks using HasRootPermission (so callers like
controller/twofa.go and controller/custom_oauth.go that must remain root-only
will not treat admins as root).

Comment on lines +255 to +267
sudo python3 - <<PY
from pathlib import Path
image = "$IMAGE"
path = Path("docker-compose.yml")
text = path.read_text()
lines = text.splitlines()
for idx, line in enumerate(lines):
if line.strip().startswith("image:") and "new-api" in line:
lines[idx] = f" image: {image}"
break
else:
raise SystemExit("new-api image line not found")
path.write_text("\n".join(lines) + "\n")

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Compose image replacement logic can target the wrong service.

This loop updates the first image: line containing "new-api", which is ambiguous and can modify a non-target service when multiple images match. Please scope replacement to the new-api service block explicitly.

Suggested safer replacement logic
 sudo python3 - <<PY
 from pathlib import Path
 image = "$IMAGE"
 path = Path("docker-compose.yml")
 text = path.read_text()
 lines = text.splitlines()
-for idx, line in enumerate(lines):
-    if line.strip().startswith("image:") and "new-api" in line:
-        lines[idx] = f"    image: {image}"
-        break
+in_new_api = False
+for idx, line in enumerate(lines):
+    stripped = line.strip()
+    if stripped.startswith("new-api:"):
+        in_new_api = True
+        continue
+    if in_new_api and stripped and not line.startswith(" "):
+        in_new_api = False
+    if in_new_api and stripped.startswith("image:"):
+        indent = line[: len(line) - len(line.lstrip(" "))]
+        lines[idx] = f"{indent}image: {image}"
+        break
 else:
     raise SystemExit("new-api image line not found")
 path.write_text("\\n".join(lines) + "\\n")
 PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/private-deploy-sop.md` around lines 255 - 267, The current loop replaces
the first image: line containing "new-api" which can hit the wrong service;
instead locate the new-api service block first by finding the service header
line (match r'^\s*new-api:') and note its indentation, then scan subsequent
lines within that block (stop when encountering a line with indentation less
than or equal to the service header or a new top-level service) and replace the
image: line inside that block only; if no new-api header or no image: line in
the block is found, raise a clear error.

Comment on lines +329 to +344
git switch private/custom-ui
git pull --ff-only origin private/custom-ui
git merge upstream/main
```

如果有冲突:

1. 只解决和私人定制相关的冲突。
2. 不要顺手改无关文件。
3. 解决后运行检查。
4. 提交 merge commit。
5. 推送到私人仓库:

```bash
git push origin private/custom-ui
```

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Upstream-sync section conflicts with the PR-only rule for private/custom-ui.

This section ends with git push origin private/custom-ui, which contradicts Line 15–17 (“don’t direct-push; use PR”). Keep governance consistent here to avoid bypassing review during sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/private-deploy-sop.md` around lines 329 - 344, The docs currently
instruct to run git push origin private/custom-ui after syncing upstream, which
contradicts the PR-only rule for the private/custom-ui branch; update the
Upstream-sync instructions for the private/custom-ui flow to remove the direct
push and instead instruct the user to push their local changes to a feature/sync
branch (or the same branch if your policy allows remote branches) and open a
pull request for merging into private/custom-ui—reference the branch name
private/custom-ui in the text and replace the final "git push origin
private/custom-ui" step with a short line telling readers to push their sync
branch and create a PR for merging into private/custom-ui per repo governance.

Comment thread middleware/auth.go
Comment on lines 182 to 186
func RootAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authHelper(c, common.RoleRootUser)
authHelper(c, common.RoleAdminUser)
}
}

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all routes using RootAuth() have controller-level HasRootPermission checks
echo "=== Routes using RootAuth ==="
rg -n 'RootAuth\(\)' --type go

echo ""
echo "=== Controllers with HasRootPermission checks ==="
rg -n 'HasRootPermission' --type go -A 2 -B 2

Repository: QuantumNous/new-api

Length of output: 5003


🏁 Script executed:

#!/bin/bash
# 1. Get RoleAdminUser and RoleRootUser definitions
echo "=== Role definitions ==="
rg -n 'RoleAdminUser|RoleRootUser' common/constants.go -A 1 -B 1

echo ""
echo "=== Handler functions for affected routes ==="
# Check GetChannelKey handler
echo "GetChannelKey handler:"
rg -n 'func.*GetChannelKey' controller/ -A 10 | head -20

echo ""
echo "FetchModels handler:"
rg -n 'func.*FetchModels' controller/ -A 10 | head -20

Repository: QuantumNous/new-api

Length of output: 2182


🏁 Script executed:

#!/bin/bash
# Get full GetChannelKey implementation
echo "=== GetChannelKey full implementation ==="
sed -n '385,430p' controller/channel.go

echo ""
echo "=== FetchModels full implementation ==="
sed -n '973,1050p' controller/channel.go

echo ""
echo "=== Check for handlers using optionRoute, customOAuthRoute, performanceRoute, ratioSyncRoute ==="
# These are likely controller functions without explicit names, search for them
rg -n 'func.*\(c \*gin\.Context\)' controller/ -A 5 | grep -A 5 -i 'option\|performance\|ratio' | head -30

Repository: QuantumNous/new-api

Length of output: 4272


RootAuth() change creates a security regression without adequate controller-level protection.

This change lowers the privilege requirement from RoleRootUser (100) to RoleAdminUser (10)—a 10x reduction. Two sensitive endpoints now have inadequate protection:

  1. GetChannelKey handler (controller/channel.go:385): Exposes all channel API keys to any admin user, with no internal permission checks.
  2. FetchModels handler (controller/channel.go:973): Allows any admin user to test external API endpoints with arbitrary credentials, with no internal permission checks.

Additionally, HasRootPermission() (common/constants.go:175) is dangerously misleading—it checks role >= RoleAdminUser, not actual root user status. This invites future bugs where developers assume the function enforces root-level access.

Required actions:

  • Add explicit permission checks within GetChannelKey and FetchModels handlers to restrict sensitive operations to RoleRootUser only.
  • Either rename HasRootPermission() to HasAdminOrRootPermission() or fix its implementation to match its name.
  • If the intent is genuinely to grant admin-level access to these endpoints, document the security implications and risk acceptance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/auth.go` around lines 182 - 186, RootAuth() was relaxed from
RoleRootUser to RoleAdminUser causing sensitive endpoints to be exposed;
restore/controller-level enforcement by adding explicit role checks in the two
handlers: in GetChannelKey and FetchModels call authHelper or directly verify
c.GetInt("role") (or equivalent) against RoleRootUser (not RoleAdminUser) and
abort with 403 when below RoleRootUser, ensuring no other code path returns
secrets for non-root users. Also fix the misleading HasRootPermission()—either
change its implementation to return role >= RoleRootUser or rename it to
HasAdminOrRootPermission() and update all call sites to use the correct
semantic; document any intentional deviation if you opt to keep admin access.

Comment on lines +142 to +146
search={
search
? Object.fromEntries(new URLSearchParams(search))
: undefined
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Routes with typed/non-string validateSearch fields =="
rg -n "validateSearch|z\\.boolean\\(|z\\.number\\(|z\\.enum\\(" web/default/src/routes -A2 -B2

echo
echo "== Navigation URL definitions that include query strings =="
rg -n "url:\\s*['\"][^'\"]*\\?" web/default/src

echo
echo "== Current nav query conversion call sites =="
rg -n "Object\\.fromEntries\\(new URLSearchParams\\(search\\)\\)" web/default/src/components/layout/components/nav-group.tsx

Repository: QuantumNous/new-api

Length of output: 13438


Search param coercion to string breaks typed route validateSearch schemas.

At lines 142 and 215, Object.fromEntries(new URLSearchParams(search)) forces all values to strings. Multiple routes expect non-string types: z.boolean() in wallet, model-square, and pricing routes; z.enum() in model-square and pricing routes; z.number() in keys, usage-logs, models, channels, and users routes. URLSearchParams coercion causes Zod validation to fail silently, triggering .catch() defaults instead of preserving the intended filter state.

Suggested fix
 function splitUrl(url: string) {
   const [pathname, search = ''] = url.split('?')

   return {
     pathname,
     search,
   }
 }
+
+function parseSearchObject(search: string): Record<string, unknown> | undefined {
+  if (!search) return undefined
+  return Object.fromEntries(
+    Array.from(new URLSearchParams(search).entries()).map(([key, value]) => {
+      try {
+        return [key, JSON.parse(value)]
+      } catch {
+        return [key, value]
+      }
+    })
+  )
+}
@@
               <Link
                 to={pathname}
-                search={
-                  search
-                    ? Object.fromEntries(new URLSearchParams(search))
-                    : undefined
-                }
+                search={parseSearchObject(search)}
                 onClick={() => setOpenMobile(false)}
               >
@@
                       <Link
                         to={pathname}
-                        search={
-                          search
-                            ? Object.fromEntries(new URLSearchParams(search))
-                            : undefined
-                        }
+                        search={parseSearchObject(search)}
                         onClick={() => setOpenMobile(false)}
                       >

Also applies to: 215

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/components/layout/components/nav-group.tsx` around lines 142
- 146, The code currently converts query params with Object.fromEntries(new
URLSearchParams(search)) which forces all values to strings and breaks Zod-based
route validateSearch schemas; replace those conversions in nav-group.tsx (the
spots using Object.fromEntries(new URLSearchParams(search))) by passing the raw
search string into the route's validateSearch (or calling
route.validateSearch?.(search) and using its result) so the route-level
validator can coerce types correctly (e.g., booleans/enums/numbers) and only
fall back to undefined when validation fails.

Comment on lines +2069 to 2071
"Model Square": "モデル広場",
"Status Monitor": "ステータス監視",
"Model Pricing": "モデル料金",

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if new i18n keys are registered in static-keys.ts or used as string literals

# Search for the new keys in static-keys.ts
echo "=== Checking static-keys.ts ==="
rg -n "Model Square|Status Monitor|Original price|Pricing Information|Recharge rate and discount tiers|Recharge Rate|Recharge Discounts|No recharge discounts configured" web/default/src/i18n/static-keys.ts

# Search for usage as t() string literals in the codebase
echo -e "\n=== Checking t() usage in components ==="
rg -n -C2 "t\(['\"](?:Model Square|Status Monitor|Original price|Pricing Information|Recharge rate and discount tiers|Recharge Rate|Recharge Discounts|No recharge discounts configured)['\"]" web/default/src/

Repository: QuantumNous/new-api

Length of output: 3580


🏁 Script executed:

# Search for "Model Square" and "Status Monitor" usage anywhere in the codebase
echo "=== Searching for Model Square usage ==="
rg -n "Model Square" web/default/src/

echo -e "\n=== Searching for Status Monitor usage ==="
rg -n "Status Monitor" web/default/src/

echo -e "\n=== Examining static-keys.ts file ==="
head -50 web/default/src/i18n/static-keys.ts

echo -e "\n=== Checking ja.json context around lines 2069-2071 ==="
sed -n '2065,2075p' web/default/src/i18n/locales/ja.json

echo -e "\n=== Checking ja.json context around lines 3866-3871 ==="
sed -n '3862,3875p' web/default/src/i18n/locales/ja.json

Repository: QuantumNous/new-api

Length of output: 3559


Add "Model Square" and "Status Monitor" to STATIC_I18N_KEYS

These keys are used as dynamic titleKey values in web/default/src/custom/site.ts and won't be picked up by the t('...') extraction scanner. They must be registered in src/i18n/static-keys.ts per the design pattern for dynamic configuration labels.

The other keys ("Original price", "Pricing Information", "Recharge rate and discount tiers", "Recharge Rate", "Recharge Discounts", "No recharge discounts configured") are already properly used with t('...') string literals in components and are correctly extracted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/i18n/locales/ja.json` around lines 2069 - 2071, Add the
missing dynamic title keys "Model Square" and "Status Monitor" to the
STATIC_I18N_KEYS array in src/i18n/static-keys.ts so the extractor picks them
up; specifically update the STATIC_I18N_KEYS constant to include the exact key
strings "Model Square" and "Status Monitor" (translations already exist in
ja.json) to align with their use as dynamic titleKey values in site.ts.

Comment on lines 21 to 24
export function getRoleLabelKey(role?: number): string {
if ((role ?? DEFAULT_ROLE) >= ROLE.ADMIN) return ROLE_LABEL_KEYS[ROLE.SUPER_ADMIN]

return ROLE_LABEL_KEYS[role as RoleValue] ?? ROLE_LABEL_KEYS[DEFAULT_ROLE]

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep role labeling separate from permission normalization.

This makes every ROLE.ADMIN user render as “Super Admin”. getRoleLabel() is used in the profile header and mobile user display, so admins now get a higher privilege label than they actually have. Preserve the direct label map here and add a separate helper for ROLE.ADMIN+ access checks.

🪪 Suggested fix
+export function canAccessSystemSettings(role?: number): boolean {
+  return (role ?? DEFAULT_ROLE) >= ROLE.ADMIN
+}
+
 export function getRoleLabelKey(role?: number): string {
-  if ((role ?? DEFAULT_ROLE) >= ROLE.ADMIN) return ROLE_LABEL_KEYS[ROLE.SUPER_ADMIN]
-
-  return ROLE_LABEL_KEYS[role as RoleValue] ?? ROLE_LABEL_KEYS[DEFAULT_ROLE]
+  const resolvedRole = role ?? DEFAULT_ROLE
+  return ROLE_LABEL_KEYS[resolvedRole as RoleValue] ?? ROLE_LABEL_KEYS[DEFAULT_ROLE]
 }

Then switch the route/menu gating call sites to canAccessSystemSettings(...).

📝 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 function getRoleLabelKey(role?: number): string {
if ((role ?? DEFAULT_ROLE) >= ROLE.ADMIN) return ROLE_LABEL_KEYS[ROLE.SUPER_ADMIN]
return ROLE_LABEL_KEYS[role as RoleValue] ?? ROLE_LABEL_KEYS[DEFAULT_ROLE]
export function canAccessSystemSettings(role?: number): boolean {
return (role ?? DEFAULT_ROLE) >= ROLE.ADMIN
}
export function getRoleLabelKey(role?: number): string {
const resolvedRole = role ?? DEFAULT_ROLE
return ROLE_LABEL_KEYS[resolvedRole as RoleValue] ?? ROLE_LABEL_KEYS[DEFAULT_ROLE]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/lib/roles.ts` around lines 21 - 24, getRoleLabelKey currently
normalizes ROLE.ADMIN to show the SUPER_ADMIN label; restore direct label
mapping by removing the special-case (do not map ROLE.ADMIN to ROLE.SUPER_ADMIN)
and return ROLE_LABEL_KEYS[role as RoleValue] ?? ROLE_LABEL_KEYS[DEFAULT_ROLE]
in getRoleLabelKey. Add a separate helper function
canAccessSystemSettings(role?: number): boolean that performs the permission
normalization (e.g., (role ?? DEFAULT_ROLE) >= ROLE.ADMIN) and update route/menu
gating call sites to use canAccessSystemSettings(...) instead of relying on
getRoleLabelKey for access checks.

Comment on lines +6 to +16
const modelSquareDetailsSearchSchema = z.object({
search: z.string().optional(),
sort: z.string().optional(),
vendor: z.string().optional(),
group: z.string().optional(),
quotaType: z.string().optional(),
endpointType: z.string().optional(),
tag: z.string().optional(),
tokenUnit: z.enum(['M', 'K']).optional(),
rechargePrice: z.boolean().optional(),
})

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve the caller’s view search param in the detail route.

This schema drops view, but ModelDetails navigates back with the current route search and the model-square entry link already uses ?view=table. Opening a model from table view will therefore lose that state and send the user back to the default view instead of the one they came from.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/routes/_authenticated/model-square/`$modelId/index.tsx around
lines 6 - 16, The search schema modelSquareDetailsSearchSchema is missing the
caller's "view" param so opening ModelDetails from the table view loses state;
update modelSquareDetailsSearchSchema to include view: z.string().optional() (or
a z.enum of allowed views if you want stricter typing) so the parsed
route/search preserves and returns the original ?view value when ModelDetails
(and its navigation code that reads the search params) navigates back.

Comment on lines +1 to +3
import { createFileRoute } from '@tanstack/react-router'
import { AppHeader, Main } from '@/components/layout'

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the iframe title instead of hardcoding English.

title='Status Monitor' should come from t(...) so it updates with active language.

Suggested fix
 import { createFileRoute } from '@tanstack/react-router'
 import { AppHeader, Main } from '@/components/layout'
+import { useTranslation } from 'react-i18next'
@@
 function StatusMonitor() {
+  const { t } = useTranslation()
+
   return (
@@
           <iframe
             src={STATUS_MONITOR_URL}
-            title='Status Monitor'
+            title={t('Status Monitor')}
             className='h-full w-full border-0'
           />
As per coding guidelines: `web/default/**/*.tsx`: “All user-facing text must support i18n using `useTranslation()` and the `t()` function in React components”.

Also applies to: 10-19

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/routes/_authenticated/status-monitor.tsx` around lines 1 - 3,
Replace the hardcoded iframe title with a localized string: import and call
useTranslation() in the component that defines the route (the module using
createFileRoute) and change title='Status Monitor' to
title={t('statusMonitor.title')} (or a suitable key), and similarly wrap any
other user-facing strings in that component (lines ~10-19) with t('...'); ensure
you add the import { useTranslation } from 'react-i18next' and use the t
function from const { t } = useTranslation() so AppHeader/Main/iframe use
localized text.

Comment on lines +16 to +20
<iframe
src={STATUS_MONITOR_URL}
title='Status Monitor'
className='h-full w-full border-0'
/>

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden the external iframe embedding.

The iframe loads third-party content without sandbox and referrerPolicy, which weakens isolation/privacy guarantees.

Suggested hardening
           <iframe
             src={STATUS_MONITOR_URL}
             title='Status Monitor'
             className='h-full w-full border-0'
+            loading='lazy'
+            referrerPolicy='no-referrer'
+            sandbox='allow-scripts allow-same-origin'
           />
📝 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
<iframe
src={STATUS_MONITOR_URL}
title='Status Monitor'
className='h-full w-full border-0'
/>
<iframe
src={STATUS_MONITOR_URL}
title='Status Monitor'
className='h-full w-full border-0'
loading='lazy'
referrerPolicy='no-referrer'
sandbox='allow-scripts allow-same-origin'
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/routes/_authenticated/status-monitor.tsx` around lines 16 -
20, The iframe embedding STATUS_MONITOR_URL lacks isolation/privacy attributes;
update the JSX <iframe> element to include a restrictive sandbox attribute
(e.g., sandbox with only the minimal needed flags such as "allow-scripts" only
if the monitor requires scripts, otherwise an empty sandbox) and add
referrerPolicy="no-referrer" (or "same-origin" if required by the monitor) to
prevent leaking the parent referrer; modify the iframe that uses
STATUS_MONITOR_URL and keep the existing title and className while choosing the
minimal sandbox flags required by the external monitor.

@seefs001 seefs001 closed this May 1, 2026
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.

3 participants