🔄 feat(ratio-sync): introduce upstream ratio synchronisation feature #1220 - #1267
Conversation
… ratio sync Remove all custom channel functionality from the upstream ratio sync feature to simplify the codebase and focus on database-stored channels only. Changes: - Remove custom channel UI components and related state management - Remove custom channel testing and validation logic - Simplify ChannelSelectorModal by removing custom channel input fields - Update API payload to only include channel_ids, removing custom_channels - Remove custom channel processing logic from backend controller - Update import path for DEFAULT_ENDPOINT constant Files modified: - web/src/pages/Setting/Ratio/UpstreamRatioSync.js - web/src/components/settings/ChannelSelectorModal.js - controller/ratio_sync.go This change streamlines the ratio synchronization workflow by focusing solely on pre-configured database channels, reducing complexity and potential maintenance overhead.
…ovements Add visual status indicators and improve user experience for the upstream ratio sync channel selector modal. Features: - Add status-based avatar indicators for channels (enabled/disabled/auto-disabled) - Implement search functionality with text highlighting - Add endpoint configuration input for each channel - Optimize component structure with reusable ChannelInfo component UI Improvements: - Custom styling for transfer component items - Hide scrollbars for cleaner appearance in transfer lists - Responsive layout adjustments for channel information display - Color-coded avatars: green (enabled), red (disabled), amber (auto-disabled), grey (unknown) Code Quality: - Extract channel status configuration to constants - Create reusable ChannelInfo component to reduce code duplication - Implement proper search filtering for both channel names and URLs - Add consistent styling classes for transfer demo components Files modified: - web/src/components/settings/ChannelSelectorModal.js - web/src/pages/Setting/Ratio/UpstreamRatioSync.js - web/src/index.css This enhancement provides better visual feedback for channel status and improves the overall user experience when selecting channels for ratio synchronization.
…nliness Summary 1. Consider “both unset” as identical • When both localValue and upstreamValue are nil, mark upstreamValue as "same" to avoid showing “Not set”. 2. Exclude fully-synced upstream channels from result • Scan `differences` to detect channels that contain at least one divergent value. • Remove channels whose every ratio is either `"same"` or `nil`, so the frontend only receives actionable discrepancies. Why These changes reduce visual noise in the Upstream Ratio Sync table, making it easier for admins to focus on models requiring attention. No functional regressions or breaking API changes are introduced.
Summary
1. Add model name search box
• Introduce Semi UI `Input` with `IconSearch` prefix next to the “Apply Sync” button.
• Support case-insensitive fuzzy matching of model names.
• Real-time filtering, pagination and bulk-select logic now work on filtered data.
2. Improve empty state handling
• Add `hasSynced` flag to distinguish “not synced yet” from “synced with no differences”.
• Display messages:
– “Please select sync channels” when no sync has been performed.
– “No differences found” when a sync completed with zero discrepancies.
– “No matching model found” when search yields no results.
3. UI tweaks
• Replace lucide-react `Search` icon with Semi UI `IconSearch` for visual consistency.
• Keep responsive width and clearable input for better usability.
Why
These changes allow admins to quickly locate specific models and provide accurate feedback on the sync status, greatly improving the usability of the Upstream Ratio Sync page.
WalkthroughThis update introduces a comprehensive upstream ratio synchronization feature for a distributed system. It adds backend endpoints and logic for exposing, comparing, and synchronizing ratio configuration data across channels, including caching and concurrency-safe access. The frontend gains new UI components and workflows for selecting channels, viewing differences, and applying synchronization, along with supporting localization and styling. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Frontend
participant Backend
participant DB
User->>Frontend: Open Upstream Ratio Sync tab
Frontend->>Backend: GET /api/ratio_sync/channels
Backend->>DB: Query syncable channels
DB-->>Backend: Return channels
Backend-->>Frontend: Return channel list
User->>Frontend: Select channels and click "Sync"
Frontend->>Backend: POST /api/ratio_sync/fetch (with channel IDs)
Backend->>Backend: Fetch local ratio data
Backend->>Backend: Concurrently fetch ratio data from upstream channels
Backend->>Backend: Compare local and upstream data
Backend-->>Frontend: Return differences and test results
User->>Frontend: Select upstream values to resolve differences
User->>Frontend: Click "Apply Sync"
Frontend->>Backend: POST (update ratio config endpoints)
Backend->>DB: Update ratio config data
Backend-->>Frontend: Return status
Poem
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (5)
controller/ratio_config.go (1)
10-24: LGTM! Consider using internationalization for the error message.The controller logic is well-structured with proper HTTP status codes and feature flag validation. However, consider using the internationalization system instead of the hardcoded Chinese message for consistency with the system's i18n approach.
Consider this improvement:
- c.JSON(http.StatusForbidden, gin.H{ - "success": false, - "message": "倍率配置接口未启用", - }) + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "Ratio configuration interface is not enabled", + })setting/ratio_setting/exposed_cache.go (1)
27-33: Consider deep cloning for nested structures.The current shallow clone is appropriate for the current data types (
map[string]float64), but consider using deep cloning if the data structure becomes more complex in the future to prevent potential data races.dto/ratio_sync.go (1)
21-25: Consider adding validation for timeout bounds.While the timeout field is present, consider adding validation tags to ensure reasonable bounds (e.g.,
binding:"min=1,max=300") to prevent extremely low or high timeout values that could cause issues.type UpstreamRequest struct { ChannelIDs []int64 `json:"channel_ids"` CustomChannels []UpstreamDTO `json:"custom_channels"` - Timeout int `json:"timeout"` + Timeout int `json:"timeout" binding:"min=1,max=300"` }web/src/pages/Setting/Ratio/UpstreamRatioSync.js (1)
25-503: Consider component decomposition for better maintainability.This component handles multiple concerns (channel selection, data fetching, table rendering, sync application). Consider breaking it into smaller, focused components:
ChannelSelector- for channel selection logicDifferenceTable- for table rendering and paginationSyncControls- for sync application and bulk operationsThis would improve testability and maintainability.
web/src/components/settings/ChannelSelectorModal.js (1)
109-113: Improve search performance with memoization.The filter function is recreated on every render, which could impact performance with large channel lists.
Consider using
useCallbackto memoize the filter function:+import React, { useState, useCallback } from 'react'; +const channelFilter = useCallback((input, item) => { const searchLower = input.toLowerCase(); return item.label.toLowerCase().includes(searchLower) || (item._originalData?.base_url || '').toLowerCase().includes(searchLower); -}; +}, []);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
common/utils.go(2 hunks)controller/ratio_config.go(1 hunks)controller/ratio_sync.go(1 hunks)dto/ratio_sync.go(1 hunks)model/option.go(2 hunks)router/api-router.go(2 hunks)setting/ratio_setting/cache_ratio.go(2 hunks)setting/ratio_setting/expose_ratio.go(1 hunks)setting/ratio_setting/exposed_cache.go(1 hunks)setting/ratio_setting/model_ratio.go(4 hunks)web/src/components/settings/ChannelSelectorModal.js(1 hunks)web/src/components/settings/RatioSetting.js(4 hunks)web/src/constants/common.constant.js(1 hunks)web/src/i18n/locales/en.json(1 hunks)web/src/index.css(1 hunks)web/src/pages/Detail/index.js(0 hunks)web/src/pages/Setting/Ratio/ModelRatioSettings.js(2 hunks)web/src/pages/Setting/Ratio/UpstreamRatioSync.js(1 hunks)
💤 Files with no reviewable changes (1)
- web/src/pages/Detail/index.js
🧰 Additional context used
🪛 Biome (1.9.4)
web/src/pages/Setting/Ratio/UpstreamRatioSync.js
[error] 189-191: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🔇 Additional comments (25)
web/src/constants/common.constant.js (1)
3-3: LGTM! Clean constant addition.The new
DEFAULT_ENDPOINTconstant is well-named and properly placed. It correctly references the new ratio configuration API endpoint.common/utils.go (1)
289-304: LGTM! Well-implemented URL building utility.The
BuildURLfunction correctly handles URL parsing and resolution with appropriate fallback logic. The implementation properly handles edge cases like empty endpoints and parse errors.web/src/index.css (1)
437-503: LGTM! Well-structured CSS for transfer component.The new CSS rules provide comprehensive styling for the channel selector transfer component. The styling follows consistent patterns with the rest of the codebase, including proper flexbox layouts, scrollbar handling, and semantic color usage.
web/src/pages/Setting/Ratio/ModelRatioSettings.js (2)
28-28: LGTM! Proper state initialization.The new
ExposeRatioEnabledfield is properly initialized with a boolean default value, consistent with the existing state pattern.
210-220: LGTM! Well-integrated form switch.The new Form.Switch component for ratio exposure is properly implemented with:
- Correct field binding
- Proper change handler that updates state
- Consistent layout with existing form elements
router/api-router.go (2)
39-39: LGTM! Properly protected ratio config endpoint.The new
/ratio_configendpoint is correctly protected with critical rate limiting, which is appropriate for a configuration exposure endpoint.
87-92: LGTM! Well-structured ratio sync route group.The new ratio synchronization routes are properly organized with:
- Appropriate RootAuth middleware for admin-level operations
- Clear endpoint naming for channels listing and upstream fetching
- Consistent route group structure
Consider verifying that the controller handlers exist and match these route definitions.
#!/bin/bash # Description: Verify that the referenced controller methods exist # Expected: Find the controller functions referenced in the new routes # Search for GetRatioConfig controller method echo "=== Checking GetRatioConfig ===" ast-grep --pattern 'func GetRatioConfig($$$) { $$$ }' # Search for GetSyncableChannels controller method echo "=== Checking GetSyncableChannels ===" ast-grep --pattern 'func GetSyncableChannels($$$) { $$$ }' # Search for FetchUpstreamRatios controller method echo "=== Checking FetchUpstreamRatios ===" ast-grep --pattern 'func FetchUpstreamRatios($$$) { $$$ }'web/src/i18n/locales/en.json (1)
1668-1691: Excellent localization coverage for the new feature.The English localization strings are comprehensive and well-structured, covering all aspects of the upstream ratio synchronization feature including error handling, user prompts, and UI labels. The naming conventions are consistent with the existing pattern.
setting/ratio_setting/expose_ratio.go (1)
1-17: Excellent thread-safe feature flag implementation.This is a clean, concurrency-safe implementation using atomic operations. The API is simple and follows Go conventions well. The default initialization to
falseprovides a secure-by-default approach for the ratio exposure feature.setting/ratio_setting/cache_ratio.go (2)
88-92: Good cache invalidation pattern.The modification to capture the error and invalidate the cache only on successful JSON unmarshalling ensures data consistency. This follows the proper cache invalidation pattern and aligns with similar implementations in related ratio setting files.
114-122: Thread-safe copy function implemented correctly.The
GetCacheRatioCopyfunction provides a proper thread-safe shallow copy of the cache ratio map. The implementation correctly uses read locks and creates a fresh map to prevent external modifications from affecting the internal state.model/option.go (2)
129-129: Proper integration of the new feature flag.The addition of the ExposeRatioEnabled option to the initialization map correctly uses the atomic flag getter and follows the existing pattern for boolean options with
strconv.FormatBool().
270-271: Correct handling of option updates.The ExposeRatioEnabled case properly calls the atomic flag setter with the parsed boolean value, maintaining consistency with the atomic operations pattern used throughout the ratio setting system.
setting/ratio_setting/exposed_cache.go (2)
35-43: Excellent double-checked locking implementation.The pattern correctly checks the cache validity twice - once before acquiring the lock (fast path) and once after acquiring it (to handle race conditions). This ensures both performance and correctness in concurrent scenarios.
11-11: TTL duration is reasonable for the use case.The 30-second TTL strikes a good balance between data freshness and performance, preventing excessive backend calls while ensuring reasonable data currency for ratio synchronization.
web/src/components/settings/RatioSetting.js (2)
9-9: Clean integration of upstream sync feature.The changes properly integrate the new UpstreamRatioSync component with the existing tab structure and feature flag management. The ExposeRatioEnabled flag follows the same pattern as other boolean options.
Also applies to: 25-25, 53-53, 101-106
109-112: Improved layout organization.Moving the GroupRatioSettings card below the tabbed interface creates a cleaner separation between model ratio settings (in tabs) and group ratio settings (standalone card).
dto/ratio_sync.go (2)
15-19: Well-documented DTO with clear field requirements.The UpstreamDTO struct properly defines required and optional fields with appropriate validation tags. The extensive comments provide clear guidance on expected formats and usage patterns.
38-41: Flexible difference representation.The DifferenceItem struct cleverly uses
interface{}to handle different data types (concrete values, "same" string, or nil), providing flexibility for various ratio comparison scenarios.setting/ratio_setting/model_ratio.go (2)
320-325: Proper cache invalidation placement.Cache invalidation is correctly called only after successful JSON unmarshalling, ensuring the cache remains consistent with the actual data state. This prevents invalid cache states from persisting.
Also applies to: 352-357, 416-421
625-653: Thread-safe getter functions with proper copying.The new copy functions correctly acquire read locks and create new map instances to prevent data races and external mutations. The implementation follows the established concurrency patterns in the codebase.
web/src/pages/Setting/Ratio/UpstreamRatioSync.js (3)
180-196: Efficient state pruning after sync.The logic correctly removes resolved differences from the local state, providing immediate UI feedback without requiring a full refetch. The nested object cleanup prevents memory leaks from empty objects.
260-287: Well-optimized data processing with useMemo.The use of
useMemofor data transformation and filtering prevents unnecessary recalculations on each render, improving performance for large datasets.
367-390: Complex bulk selection logic handles edge cases well.The bulk selection implementation correctly handles partial selections, state cleanup, and nested object management. The logic properly maintains consistency between individual and bulk selections.
controller/ratio_sync.go (1)
15-37: Consider consolidating duplicate type definitions.The local types
upstreamResult,TestResult,DifferenceItem, andSyncableChannelappear to duplicate structures that might already exist in thedtopackage, creating potential inconsistencies.#!/bin/bash # Check if these types already exist in the dto package rg -A 5 "type.*TestResult" --type go rg -A 5 "type.*DifferenceItem" --type go rg -A 5 "type.*SyncableChannel" --type go
| Object.keys(ratios).forEach(ratioType => { | ||
| if (newDifferences[model] && newDifferences[model][ratioType]) { | ||
| delete newDifferences[model][ratioType]; | ||
|
|
||
| if (Object.keys(newDifferences[model]).length === 0) { | ||
| delete newDifferences[model]; | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Apply optional chaining for safer property access.
The static analysis correctly identifies an opportunity to use optional chaining for safer property access.
Object.entries(resolutions).forEach(([model, ratios]) => {
Object.keys(ratios).forEach(ratioType => {
- if (newDifferences[model] && newDifferences[model][ratioType]) {
+ if (newDifferences[model]?.[ratioType]) {
delete newDifferences[model][ratioType];
if (Object.keys(newDifferences[model]).length === 0) {
delete newDifferences[model];
}
}
});
});📝 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.
| Object.keys(ratios).forEach(ratioType => { | |
| if (newDifferences[model] && newDifferences[model][ratioType]) { | |
| delete newDifferences[model][ratioType]; | |
| if (Object.keys(newDifferences[model]).length === 0) { | |
| delete newDifferences[model]; | |
| } | |
| } | |
| }); | |
| Object.entries(resolutions).forEach(([model, ratios]) => { | |
| Object.keys(ratios).forEach(ratioType => { | |
| if (newDifferences[model]?.[ratioType]) { | |
| delete newDifferences[model][ratioType]; | |
| if (Object.keys(newDifferences[model]).length === 0) { | |
| delete newDifferences[model]; | |
| } | |
| } | |
| }); | |
| }); |
🧰 Tools
🪛 Biome (1.9.4)
[error] 189-191: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🤖 Prompt for AI Agents
In web/src/pages/Setting/Ratio/UpstreamRatioSync.js between lines 184 and 192,
the code accesses nested properties newDifferences[model] and
newDifferences[model][ratioType] without checking if newDifferences[model]
exists, which can cause runtime errors. Update the condition to use optional
chaining like newDifferences[model]?.[ratioType] to safely access these
properties and prevent errors if intermediate objects are undefined.
| visible={visible} | ||
| onCancel={onCancel} | ||
| onOk={onOk} | ||
| title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use translation function for modal title.
The modal title appears to be hardcoded Chinese text that should be localized.
The title is already using the t function correctly, so this is properly internationalized.
🤖 Prompt for AI Agents
In web/src/components/settings/ChannelSelectorModal.js at line 120, the modal
title is already using the translation function `t` correctly to localize the
text. No changes are needed as the title is properly internationalized.
| const ChannelInfo = ({ item, showEndpoint = false, isSelected = false }) => { | ||
| const channelId = item.key || item.value; | ||
| const currentEndpoint = channelEndpoints[channelId]; | ||
| const baseUrl = item._originalData?.base_url || ''; | ||
| const status = item._originalData?.status || 0; | ||
| const statusConfig = getChannelStatusConfig(status); | ||
|
|
||
| return ( | ||
| <> | ||
| <Avatar color={statusConfig.color} size="small"> | ||
| {statusConfig.text} | ||
| </Avatar> | ||
| <div className="info"> | ||
| <div className="name"> | ||
| {isSelected ? ( | ||
| item.label | ||
| ) : ( | ||
| <Highlight sourceString={item.label} searchWords={[searchText]} /> | ||
| )} | ||
| </div> | ||
| <div className="email" style={showEndpoint ? { display: 'flex', alignItems: 'center', gap: '4px' } : {}}> | ||
| <span className="text-xs text-gray-500 truncate max-w-[200px]" title={baseUrl}> | ||
| {isSelected ? ( | ||
| baseUrl | ||
| ) : ( | ||
| <Highlight sourceString={baseUrl} searchWords={[searchText]} /> | ||
| )} | ||
| </span> | ||
| {showEndpoint && ( | ||
| <Input | ||
| size="small" | ||
| value={currentEndpoint} | ||
| onChange={(value) => updateChannelEndpoint(channelId, value)} | ||
| placeholder="/api/ratio_config" | ||
| className="flex-1 text-xs" | ||
| style={{ fontSize: '12px' }} | ||
| /> | ||
| )} | ||
| {isSelected && !showEndpoint && ( | ||
| <span className="text-xs text-gray-700 font-mono bg-gray-100 px-2 py-1 rounded ml-2"> | ||
| {currentEndpoint} | ||
| </span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add prop validation and improve accessibility.
The ChannelInfo component lacks proper prop validation and has potential accessibility issues with the Avatar component not having proper alt text or aria-labels.
Consider adding PropTypes validation and improving accessibility:
const ChannelInfo = ({ item, showEndpoint = false, isSelected = false }) => {
const channelId = item.key || item.value;
const currentEndpoint = channelEndpoints[channelId];
const baseUrl = item._originalData?.base_url || '';
const status = item._originalData?.status || 0;
- const statusConfig = getChannelStatusConfig(status);
+ const statusConfig = getChannelStatusConfig(status, t);
return (
<>
- <Avatar color={statusConfig.color} size="small">
+ <Avatar
+ color={statusConfig.color}
+ size="small"
+ aria-label={`Channel status: ${statusConfig.text}`}
+ >
{statusConfig.text}
</Avatar>
{/* ... rest of the component */}
</>
);
};Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In web/src/components/settings/ChannelSelectorModal.js between lines 37 and 84,
the ChannelInfo component lacks PropTypes validation and accessibility features.
Add PropTypes definitions for the component's props to ensure type safety.
Improve accessibility by adding appropriate alt text or aria-label attributes to
the Avatar component to describe its content for screen readers.
| const CHANNEL_STATUS_CONFIG = { | ||
| 1: { color: 'green', text: '启用' }, | ||
| 2: { color: 'red', text: '禁用' }, | ||
| 3: { color: 'amber', text: '自禁' }, | ||
| default: { color: 'grey', text: '未知' } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Replace hardcoded Chinese text with internationalized strings.
The status configuration contains hardcoded Chinese text ('启用', '禁用', '自禁', '未知'), which breaks internationalization. These should use the t function for proper localization.
-const CHANNEL_STATUS_CONFIG = {
- 1: { color: 'green', text: '启用' },
- 2: { color: 'red', text: '禁用' },
- 3: { color: 'amber', text: '自禁' },
- default: { color: 'grey', text: '未知' }
-};
+const getChannelStatusConfig = (status, t) => {
+ const CONFIG = {
+ 1: { color: 'green', text: t('启用') },
+ 2: { color: 'red', text: t('禁用') },
+ 3: { color: 'amber', text: t('自禁') },
+ default: { color: 'grey', text: t('未知') }
+ };
+ return CONFIG[status] || CONFIG.default;
+};Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In web/src/components/settings/ChannelSelectorModal.js around lines 13 to 18,
the CHANNEL_STATUS_CONFIG object contains hardcoded Chinese text for status
labels. Replace these hardcoded strings with calls to the internationalization
function t(), passing appropriate keys for each status text to enable proper
localization support.
| <Input | ||
| size="small" | ||
| value={currentEndpoint} | ||
| onChange={(value) => updateChannelEndpoint(channelId, value)} | ||
| placeholder="/api/ratio_config" | ||
| className="flex-1 text-xs" | ||
| style={{ fontSize: '12px' }} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add input validation for endpoint configuration.
The endpoint input lacks validation and could accept malformed URLs or potentially unsafe values.
{showEndpoint && (
<Input
size="small"
value={currentEndpoint}
- onChange={(value) => updateChannelEndpoint(channelId, value)}
+ onChange={(value) => {
+ // Basic validation for endpoint format
+ if (value && !value.startsWith('/')) {
+ value = '/' + value;
+ }
+ updateChannelEndpoint(channelId, value);
+ }}
placeholder="/api/ratio_config"
className="flex-1 text-xs"
style={{ fontSize: '12px' }}
/>
)}📝 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.
| <Input | |
| size="small" | |
| value={currentEndpoint} | |
| onChange={(value) => updateChannelEndpoint(channelId, value)} | |
| placeholder="/api/ratio_config" | |
| className="flex-1 text-xs" | |
| style={{ fontSize: '12px' }} | |
| /> | |
| )} | |
| {showEndpoint && ( | |
| <Input | |
| size="small" | |
| value={currentEndpoint} | |
| onChange={(value) => { | |
| // Basic validation for endpoint format | |
| if (value && !value.startsWith('/')) { | |
| value = '/' + value; | |
| } | |
| updateChannelEndpoint(channelId, value); | |
| }} | |
| placeholder="/api/ratio_config" | |
| className="flex-1 text-xs" | |
| style={{ fontSize: '12px' }} | |
| /> | |
| )} |
🤖 Prompt for AI Agents
In web/src/components/settings/ChannelSelectorModal.js around lines 66 to 74,
the Input component for the endpoint configuration currently accepts any value
without validation. Add input validation logic to ensure the entered endpoint is
a well-formed URL or matches expected safe patterns before calling
updateChannelEndpoint. This can be done by validating the input value on change
and only updating the state if it passes the validation, or by showing an error
message for invalid inputs.
| func buildDifferences(localData map[string]any, successfulChannels []struct { | ||
| name string | ||
| data map[string]any | ||
| }) map[string]map[string]dto.DifferenceItem { | ||
| differences := make(map[string]map[string]dto.DifferenceItem) | ||
| ratioTypes := []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"} | ||
|
|
||
| allModels := make(map[string]struct{}) | ||
|
|
||
| for _, ratioType := range ratioTypes { | ||
| if localRatioAny, ok := localData[ratioType]; ok { | ||
| if localRatio, ok := localRatioAny.(map[string]float64); ok { | ||
| for modelName := range localRatio { | ||
| allModels[modelName] = struct{}{} | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for _, channel := range successfulChannels { | ||
| for _, ratioType := range ratioTypes { | ||
| if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok { | ||
| for modelName := range upstreamRatio { | ||
| allModels[modelName] = struct{}{} | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for modelName := range allModels { | ||
| for _, ratioType := range ratioTypes { | ||
| var localValue interface{} = nil | ||
| if localRatioAny, ok := localData[ratioType]; ok { | ||
| if localRatio, ok := localRatioAny.(map[string]float64); ok { | ||
| if val, exists := localRatio[modelName]; exists { | ||
| localValue = val | ||
| } | ||
| } | ||
| } | ||
|
|
||
| upstreamValues := make(map[string]interface{}) | ||
| hasUpstreamValue := false | ||
| hasDifference := false | ||
|
|
||
| for _, channel := range successfulChannels { | ||
| var upstreamValue interface{} = nil | ||
|
|
||
| if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok { | ||
| if val, exists := upstreamRatio[modelName]; exists { | ||
| upstreamValue = val | ||
| hasUpstreamValue = true | ||
|
|
||
| if localValue != nil && localValue != val { | ||
| hasDifference = true | ||
| } else if localValue == val { | ||
| upstreamValue = "same" | ||
| } | ||
| } | ||
| } | ||
| if upstreamValue == nil && localValue == nil { | ||
| upstreamValue = "same" | ||
| } | ||
|
|
||
| if localValue == nil && upstreamValue != nil && upstreamValue != "same" { | ||
| hasDifference = true | ||
| } | ||
|
|
||
| upstreamValues[channel.name] = upstreamValue | ||
| } | ||
|
|
||
| shouldInclude := false | ||
|
|
||
| if localValue != nil { | ||
| if hasDifference { | ||
| shouldInclude = true | ||
| } | ||
| } else { | ||
| if hasUpstreamValue { | ||
| shouldInclude = true | ||
| } | ||
| } | ||
|
|
||
| if shouldInclude { | ||
| if differences[modelName] == nil { | ||
| differences[modelName] = make(map[string]dto.DifferenceItem) | ||
| } | ||
| differences[modelName][ratioType] = dto.DifferenceItem{ | ||
| Current: localValue, | ||
| Upstreams: upstreamValues, | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| channelHasDiff := make(map[string]bool) | ||
| for _, ratioMap := range differences { | ||
| for _, item := range ratioMap { | ||
| for chName, val := range item.Upstreams { | ||
| if val != nil && val != "same" { | ||
| channelHasDiff[chName] = true | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for modelName, ratioMap := range differences { | ||
| for ratioType, item := range ratioMap { | ||
| for chName := range item.Upstreams { | ||
| if !channelHasDiff[chName] { | ||
| delete(item.Upstreams, chName) | ||
| } | ||
| } | ||
| differences[modelName][ratioType] = item | ||
| } | ||
| } | ||
|
|
||
| return differences | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Refactor complex buildDifferences function for maintainability.
The buildDifferences function is overly complex with deeply nested logic, making it difficult to understand, test, and maintain. Consider breaking it into smaller, focused functions.
+func collectAllModels(localData map[string]any, channels []struct {
+ name string
+ data map[string]any
+}, ratioTypes []string) map[string]struct{} {
+ allModels := make(map[string]struct{})
+
+ // Collect from local data
+ for _, ratioType := range ratioTypes {
+ if localRatioAny, ok := localData[ratioType]; ok {
+ if localRatio, ok := localRatioAny.(map[string]float64); ok {
+ for modelName := range localRatio {
+ allModels[modelName] = struct{}{}
+ }
+ }
+ }
+ }
+
+ // Collect from upstream data
+ for _, channel := range channels {
+ for _, ratioType := range ratioTypes {
+ if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
+ for modelName := range upstreamRatio {
+ allModels[modelName] = struct{}{}
+ }
+ }
+ }
+ }
+
+ return allModels
+}
+func compareModelRatio(modelName, ratioType string, localData map[string]any,
+ channels []struct {
+ name string
+ data map[string]any
+ }) (dto.DifferenceItem, bool) {
+ // Extract comparison logic here
+ // Return the difference item and whether it should be included
+}
func buildDifferences(localData map[string]any, successfulChannels []struct {
name string
data map[string]any
}) map[string]map[string]dto.DifferenceItem {
differences := make(map[string]map[string]dto.DifferenceItem)
ratioTypes := []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
- allModels := make(map[string]struct{})
- // ... existing complex logic
+ allModels := collectAllModels(localData, successfulChannels, ratioTypes)
for modelName := range allModels {
for _, ratioType := range ratioTypes {
- // ... existing complex comparison logic
+ if item, shouldInclude := compareModelRatio(modelName, ratioType, localData, successfulChannels); shouldInclude {
+ if differences[modelName] == nil {
+ differences[modelName] = make(map[string]dto.DifferenceItem)
+ }
+ differences[modelName][ratioType] = item
+ }
}
}
+ return filterEmptyChannels(differences)
+}
+func filterEmptyChannels(differences map[string]map[string]dto.DifferenceItem) map[string]map[string]dto.DifferenceItem {
+ // Extract the channel filtering logic hereCommittable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In controller/ratio_sync.go from lines 147 to 264, the buildDifferences function
is too complex and deeply nested. Refactor by splitting it into smaller helper
functions that each handle a specific task, such as extracting all model names,
comparing local and upstream values, and filtering differences. This will
improve readability, testability, and maintainability.
| endpoint = "/api/ratio_config" | ||
| } | ||
| url := chItem.BaseURL + endpoint | ||
| client := http.Client{Timeout: time.Duration(req.Timeout) * time.Second} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Set reasonable timeout limits.
The timeout is user-controlled without upper bounds, which could lead to resource exhaustion.
if req.Timeout <= 0 {
req.Timeout = 10
}
+if req.Timeout > 60 {
+ req.Timeout = 60 // Maximum 60 seconds
+}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In controller/ratio_sync.go at line 78, the HTTP client timeout is set directly
from a user-controlled value without any upper limit, which risks resource
exhaustion. Add a maximum timeout limit by checking if req.Timeout exceeds a
predefined safe threshold and, if so, cap it to that maximum value before
setting the client timeout.
| if localValue != nil && localValue != val { | ||
| hasDifference = true | ||
| } else if localValue == val { | ||
| upstreamValue = "same" | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix potential type comparison issue.
The comparison localValue != val may not work correctly for floating-point numbers due to precision issues, and the type assertion could fail silently.
+import "math"
+func compareFloatValues(a, b interface{}) bool {
+ aFloat, aOk := a.(float64)
+ bFloat, bOk := b.(float64)
+ if aOk && bOk {
+ return math.Abs(aFloat-bFloat) < 1e-9 // Use epsilon for float comparison
+ }
+ return a == b // Fallback to direct comparison
+}
-if localValue != nil && localValue != val {
+if localValue != nil && !compareFloatValues(localValue, val) {
hasDifference = true
-} else if localValue == val {
+} else if compareFloatValues(localValue, val) {
upstreamValue = "same"
}🤖 Prompt for AI Agents
In controller/ratio_sync.go around lines 199 to 203, the code compares
localValue and val directly, which can cause issues with floating-point
precision and silent type assertion failures. To fix this, ensure both values
are asserted to the correct numeric type safely, then compare them using a
tolerance threshold for floating-point numbers instead of direct equality. This
will prevent incorrect difference detection due to minor precision errors.
| func GetSyncableChannels(c *gin.Context) { | ||
| channels, err := model.GetAllChannels(0, 0, true, false) | ||
| if err != nil { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| var syncableChannels []dto.SyncableChannel | ||
| for _, channel := range channels { | ||
| if channel.GetBaseURL() != "" { | ||
| syncableChannels = append(syncableChannels, dto.SyncableChannel{ | ||
| ID: channel.Id, | ||
| Name: channel.Name, | ||
| BaseURL: channel.GetBaseURL(), | ||
| Status: channel.Status, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "message": "", | ||
| "data": syncableChannels, | ||
| }) | ||
| } No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add input validation and improve error handling.
The GetSyncableChannels function lacks input validation and returns success even when database errors occur.
func GetSyncableChannels(c *gin.Context) {
channels, err := model.GetAllChannels(0, 0, true, false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
+ c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": err.Error(),
})
return
}
var syncableChannels []dto.SyncableChannel
for _, channel := range channels {
- if channel.GetBaseURL() != "" {
+ baseURL := channel.GetBaseURL()
+ if baseURL != "" && isValidURL(baseURL) { // Reuse the validation function
syncableChannels = append(syncableChannels, dto.SyncableChannel{
ID: channel.Id,
Name: channel.Name,
- BaseURL: channel.GetBaseURL(),
+ BaseURL: baseURL,
Status: channel.Status,
})
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
- "message": "",
+ "message": "Successfully retrieved syncable channels",
"data": syncableChannels,
})
}🤖 Prompt for AI Agents
In controller/ratio_sync.go around lines 266 to 293, the GetSyncableChannels
function lacks input validation and improperly returns success status even when
database errors occur. Add validation for any input parameters received from the
gin.Context before processing. Modify the error handling to return an
appropriate HTTP error status code (e.g., 500) instead of http.StatusOK when a
database error occurs, and set "success" to false in the JSON response. Ensure
the function only returns success true when no errors happen and valid data is
returned.
| for _, chn := range upstreams { | ||
| wg.Add(1) | ||
| go func(chItem dto.UpstreamDTO) { | ||
| defer wg.Done() | ||
| endpoint := chItem.Endpoint | ||
| if endpoint == "" { | ||
| endpoint = "/api/ratio_config" | ||
| } | ||
| url := chItem.BaseURL + endpoint | ||
| client := http.Client{Timeout: time.Duration(req.Timeout) * time.Second} | ||
| resp, err := client.Get(url) | ||
| if err != nil { | ||
| ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} | ||
| return | ||
| } | ||
| defer resp.Body.Close() | ||
| if resp.StatusCode != http.StatusOK { | ||
| ch <- upstreamResult{Name: chItem.Name, Err: resp.Status} | ||
| return | ||
| } | ||
| var body struct { | ||
| Success bool `json:"success"` | ||
| Data map[string]any `json:"data"` | ||
| Message string `json:"message"` | ||
| } | ||
| if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { | ||
| ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} | ||
| return | ||
| } | ||
| if !body.Success { | ||
| ch <- upstreamResult{Name: chItem.Name, Err: body.Message} | ||
| return | ||
| } | ||
| ch <- upstreamResult{Name: chItem.Name, Data: body.Data} | ||
| }(chn) | ||
| } |
There was a problem hiding this comment.
Address security vulnerabilities in HTTP requests.
The code constructs URLs directly from user input and makes HTTP requests without proper validation, creating potential SSRF (Server-Side Request Forgery) vulnerabilities.
+import (
+ "net/url"
+ "strings"
+)
+func isValidURL(rawURL string) bool {
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return false
+ }
+ // Only allow http/https schemes
+ if u.Scheme != "http" && u.Scheme != "https" {
+ return false
+ }
+ // Prevent access to local/private networks
+ if strings.Contains(u.Host, "localhost") ||
+ strings.Contains(u.Host, "127.0.0.1") ||
+ strings.Contains(u.Host, "0.0.0.0") {
+ return false
+ }
+ return true
+}
go func(chItem dto.UpstreamDTO) {
defer wg.Done()
endpoint := chItem.Endpoint
if endpoint == "" {
endpoint = "/api/ratio_config"
}
- url := chItem.BaseURL + endpoint
+ fullURL := chItem.BaseURL + endpoint
+ if !isValidURL(fullURL) {
+ ch <- upstreamResult{Name: chItem.Name, Err: "invalid or unsafe URL"}
+ return
+ }
client := http.Client{Timeout: time.Duration(req.Timeout) * time.Second}
- resp, err := client.Get(url)
+ resp, err := client.Get(fullURL)📝 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.
| for _, chn := range upstreams { | |
| wg.Add(1) | |
| go func(chItem dto.UpstreamDTO) { | |
| defer wg.Done() | |
| endpoint := chItem.Endpoint | |
| if endpoint == "" { | |
| endpoint = "/api/ratio_config" | |
| } | |
| url := chItem.BaseURL + endpoint | |
| client := http.Client{Timeout: time.Duration(req.Timeout) * time.Second} | |
| resp, err := client.Get(url) | |
| if err != nil { | |
| ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} | |
| return | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != http.StatusOK { | |
| ch <- upstreamResult{Name: chItem.Name, Err: resp.Status} | |
| return | |
| } | |
| var body struct { | |
| Success bool `json:"success"` | |
| Data map[string]any `json:"data"` | |
| Message string `json:"message"` | |
| } | |
| if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { | |
| ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} | |
| return | |
| } | |
| if !body.Success { | |
| ch <- upstreamResult{Name: chItem.Name, Err: body.Message} | |
| return | |
| } | |
| ch <- upstreamResult{Name: chItem.Name, Data: body.Data} | |
| }(chn) | |
| } | |
| // At the top of controller/ratio_sync.go, add the new imports: | |
| import ( | |
| // … other imports … | |
| "net/url" | |
| "strings" | |
| ) | |
| // Add this helper right after your imports: | |
| func isValidURL(rawURL string) bool { | |
| u, err := url.Parse(rawURL) | |
| if err != nil { | |
| return false | |
| } | |
| // Only allow http/https schemes | |
| if u.Scheme != "http" && u.Scheme != "https" { | |
| return false | |
| } | |
| // Prevent access to local/private networks | |
| if strings.Contains(u.Host, "localhost") || | |
| strings.Contains(u.Host, "127.0.0.1") || | |
| strings.Contains(u.Host, "0.0.0.0") { | |
| return false | |
| } | |
| return true | |
| } | |
| // Then, in your existing loop (lines 69–104), update to: | |
| for _, chn := range upstreams { | |
| wg.Add(1) | |
| go func(chItem dto.UpstreamDTO) { | |
| defer wg.Done() | |
| endpoint := chItem.Endpoint | |
| if endpoint == "" { | |
| endpoint = "/api/ratio_config" | |
| } | |
| fullURL := chItem.BaseURL + endpoint | |
| if !isValidURL(fullURL) { | |
| ch <- upstreamResult{Name: chItem.Name, Err: "invalid or unsafe URL"} | |
| return | |
| } | |
| client := http.Client{Timeout: time.Duration(req.Timeout) * time.Second} | |
| resp, err := client.Get(fullURL) | |
| if err != nil { | |
| ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} | |
| return | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != http.StatusOK { | |
| ch <- upstreamResult{Name: chItem.Name, Err: resp.Status} | |
| return | |
| } | |
| var body struct { | |
| Success bool `json:"success"` | |
| Data map[string]any `json:"data"` | |
| Message string `json:"message"` | |
| } | |
| if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { | |
| ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} | |
| return | |
| } | |
| if !body.Success { | |
| ch <- upstreamResult{Name: chItem.Name, Err: body.Message} | |
| return | |
| } | |
| ch <- upstreamResult{Name: chItem.Name, Data: body.Data} | |
| }(chn) | |
| } |
🤖 Prompt for AI Agents
In controller/ratio_sync.go around lines 69 to 104, the code constructs URLs
directly from user input and makes HTTP requests without validating the URLs,
which can lead to SSRF vulnerabilities. To fix this, validate and sanitize the
BaseURL and endpoint before making the HTTP request. Use URL parsing to ensure
the URL is well-formed, restrict requests to allowed domains or IP ranges, and
avoid concatenating strings directly to form URLs. Implement checks to reject or
handle unsafe URLs before proceeding with the HTTP client call.
…adiness WHAT’S NEW • controller/ratio_sync.go – Deleted unused local structs (TestResult, DifferenceItem, SyncableChannel). – Centralised config with constants: defaultTimeoutSeconds, defaultEndpoint, maxConcurrentFetches, ratioTypes. – Replaced magic numbers; added semaphore-based concurrency limit and shared http.Client (with TLS & Expect-Continue timeouts). – Added comprehensive error handling and context-aware logging via common.Log* helpers. – Checked DB errors from GetChannelsByIds; early-return on failures or empty upstream list. – Removed custom-channel support; logic now relies solely on ChannelIDs. – Minor clean-ups: import grouping, string trimming, endpoint normalisation. • dto/ratio_sync.go – Simplified UpstreamRequest: dropped unused CustomChannels field. WHY These improvements harden the ratio-sync endpoint for production use by preventing silent failures, controlling resource usage, and making behaviour configurable and observable. HOW No business logic change—only structural refactor, logging, and safeguards—so existing API contracts (aside from removed custom_channels) remain intact.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
controller/ratio_sync.go (5)
40-42: Set reasonable timeout limits.The timeout is user-controlled without upper bounds, which could lead to resource exhaustion.
The previous suggestion to add a maximum timeout limit (e.g., 60 seconds) still applies:
if req.Timeout <= 0 { req.Timeout = defaultTimeoutSeconds } +if req.Timeout > 60 { + req.Timeout = 60 // Maximum 60 seconds +}
88-104: Address security vulnerabilities in HTTP requests.The code constructs URLs directly from user input and makes HTTP requests without proper validation, creating potential SSRF (Server-Side Request Forgery) vulnerabilities.
The previous suggestion to validate URLs still applies. Add URL validation before making HTTP requests to prevent SSRF attacks.
177-293: Refactor complex buildDifferences function for maintainability.The
buildDifferencesfunction is overly complex with deeply nested logic, making it difficult to understand, test, and maintain.The previous suggestion to break this function into smaller, focused helper functions still applies. Consider extracting:
- Model collection logic (lines 183-203)
- Value comparison logic (lines 205-268)
- Channel filtering logic (lines 270-290)
228-232: Fix potential type comparison issue.The comparison
localValue != valmay not work correctly for floating-point numbers due to precision issues, and the type assertion could fail silently.The previous suggestion to use epsilon-based comparison for floating-point numbers still applies to prevent incorrect difference detection due to precision errors.
295-322: Add input validation and improve error handling.The
GetSyncableChannelsfunction lacks input validation and returns success even when database errors occur.The previous suggestion to fix error handling still applies:
- Return HTTP 500 status code for database errors instead of 200
- Add URL validation for base URLs
- Only return success: true when no errors occur
🧹 Nitpick comments (2)
controller/ratio_sync.go (2)
78-78: Optimize HTTP client creation for better performance.Creating a new HTTP client instance on each request is inefficient. Consider making this a package-level variable or singleton to reuse connections.
Move the HTTP client to package level:
+var httpClient = &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, +} - client := &http.Client{Transport: &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second}} + // Use the package-level httpClient
106-106: Use the reusable HTTP client.This line should use the reusable HTTP client suggested above instead of the local
clientvariable.- resp, err := client.Do(httpReq) + resp, err := httpClient.Do(httpReq)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
controller/ratio_sync.go(1 hunks)dto/ratio_sync.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- dto/ratio_sync.go
🔇 Additional comments (2)
controller/ratio_sync.go (2)
19-25: LGTM - Well-defined constants and configuration.The constants provide reasonable defaults and the ratioTypes array clearly defines the data types to be synchronized.
139-139: Add error handling for GetExposedData.The call to
ratio_setting.GetExposedData()lacks error handling, which could lead to issues if the local data retrieval fails.- localData := ratio_setting.GetExposedData() + localData, err := ratio_setting.GetExposedData() + if err != nil { + common.LogError(c.Request.Context(), "failed to get local ratio data: "+err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "获取本地配置失败"}) + return + }Likely an incorrect or invalid review comment.
…o-sync 🔄 feat(ratio-sync): introduce upstream ratio synchronisation feature QuantumNous#1220
Refs: #1220
This commit adds the first iteration of the upstream ratio synchronisation
functionality, enabling administrators to fetch, compare and align local
ratio settings with upstream channels.
Key changes:
dto/ratio_sync.go
• Add UpstreamDTO, UpstreamRequest, TestResult, DifferenceItem and
SyncableChannel data structures.
setting/ratio_setting/expose_ratio.go
• Introduce atomic flag to toggle ratio–config exposure.
setting/ratio_setting/exposed_cache.go
• Implement TTL-based cache (30 s) for exposed ratio data with safe cloning
and manual invalidation support.
controller/ratio_sync.go
• Implement FetchUpstreamRatios and GetSyncableChannels handlers.
• Build detailed diff logic across model_ratio, completion_ratio,
cache_ratio and model_price with concurrency and graceful error handling.
controller/ratio_config.go
• Provide GetRatioConfig endpoint protected by exposeRatio flag.
router/api-router.go
• Register new /api/ratio_config and /api/ratio_sync routes with proper
middleware (rate-limit, auth).
Outcome:
Administrators can now test connectivity to upstream channels, review
differences in ratio configurations, and apply selected changes to maintain
consistency while minimising manual effort.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes