Commit 0f9f094a by CaIon

feat(default): reorganize system settings pricing UI

Refine the default system settings structure and model pricing editor so pricing configuration is easier to scan and edit.
parent 9acf5fec
...@@ -119,7 +119,7 @@ export function WorkspaceSwitcher({ ...@@ -119,7 +119,7 @@ export function WorkspaceSwitcher({
// Only navigate, let useEffect synchronize workspace state based on new pathname // Only navigate, let useEffect synchronize workspace state based on new pathname
// This avoids race conditions and context loss issues // This avoids race conditions and context loss issues
if (workspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS) { if (workspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS) {
navigate({ to: '/system-settings/general' }) navigate({ to: '/system-settings/site' })
} else { } else {
navigate({ to: '/dashboard' }) navigate({ to: '/dashboard' })
} }
......
import { type TFunction } from 'i18next' import { type TFunction } from 'i18next'
import { import {
Box,
CreditCard,
Layout,
Settings, Settings,
Shield, Shield,
ShieldAlert, ShieldAlert,
Layout,
Plug,
Box,
Wrench, Wrench,
} from 'lucide-react' } from 'lucide-react'
import { getAuthSectionNavItems } from '@/features/system-settings/auth/section-registry.tsx' import { getAuthSectionNavItems } from '@/features/system-settings/auth/section-registry.tsx'
import { getBillingSectionNavItems } from '@/features/system-settings/billing/section-registry.tsx'
import { getContentSectionNavItems } from '@/features/system-settings/content/section-registry.tsx' import { getContentSectionNavItems } from '@/features/system-settings/content/section-registry.tsx'
import { getGeneralSectionNavItems } from '@/features/system-settings/general/section-registry.tsx'
import { getIntegrationsSectionNavItems } from '@/features/system-settings/integrations/section-registry.tsx'
import { getMaintenanceSectionNavItems } from '@/features/system-settings/maintenance/section-registry.tsx'
import { getModelsSectionNavItems } from '@/features/system-settings/models/section-registry.tsx' import { getModelsSectionNavItems } from '@/features/system-settings/models/section-registry.tsx'
import { getRequestLimitsSectionNavItems } from '@/features/system-settings/request-limits/section-registry.tsx' import { getOperationsSectionNavItems } from '@/features/system-settings/operations/section-registry.tsx'
import { getSecuritySectionNavItems } from '@/features/system-settings/security/section-registry.tsx'
import { getSiteSectionNavItems } from '@/features/system-settings/site/section-registry.tsx'
import { type NavGroup } from '../types' import { type NavGroup } from '../types'
/** /**
...@@ -30,9 +30,9 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] { ...@@ -30,9 +30,9 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] {
title: t('System Administration'), title: t('System Administration'),
items: [ items: [
{ {
title: t('General'), title: t('Site & Branding'),
icon: Settings, icon: Settings,
items: getGeneralSectionNavItems(t), items: getSiteSectionNavItems(t),
}, },
{ {
title: t('Authentication'), title: t('Authentication'),
...@@ -40,29 +40,29 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] { ...@@ -40,29 +40,29 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] {
items: getAuthSectionNavItems(t), items: getAuthSectionNavItems(t),
}, },
{ {
title: t('Request Limits'), title: t('Billing & Payment'),
icon: ShieldAlert, icon: CreditCard,
items: getRequestLimitsSectionNavItems(t), items: getBillingSectionNavItems(t),
}, },
{ {
title: t('Content'), title: t('Models & Routing'),
icon: Layout, icon: Box,
items: getContentSectionNavItems(t), items: getModelsSectionNavItems(t),
}, },
{ {
title: t('Integrations'), title: t('Security & Limits'),
icon: Plug, icon: ShieldAlert,
items: getIntegrationsSectionNavItems(t), items: getSecuritySectionNavItems(t),
}, },
{ {
title: t('Models'), title: t('Console Content'),
icon: Box, icon: Layout,
items: getModelsSectionNavItems(t), items: getContentSectionNavItems(t),
}, },
{ {
title: t('Maintenance'), title: t('Operations'),
icon: Wrench, icon: Wrench,
items: getMaintenanceSectionNavItems(t), items: getOperationsSectionNavItems(t),
}, },
], ],
}, },
......
...@@ -100,8 +100,8 @@ export function ProfileDropdown() { ...@@ -100,8 +100,8 @@ export function ProfileDropdown() {
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
navigate({ navigate({
to: '/system-settings/general', to: '/system-settings/site/$section',
search: { section: 'system-info' }, params: { section: 'system-info' },
}) })
} }
> >
......
...@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next' ...@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & { type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
minimal?: boolean minimal?: boolean
} }
...@@ -28,10 +30,27 @@ export function GeneralError({ ...@@ -28,10 +30,27 @@ export function GeneralError({
{t('Please try again later.')} {t('Please try again later.')}
</p> </p>
{!minimal && ( {!minimal && (
<div className='mt-6 flex gap-4'> <p className='text-muted-foreground text-center text-sm'>
{t('If this keeps happening, please report it on GitHub Issues.')}
</p>
)}
{!minimal && (
<div className='mt-6 flex flex-wrap justify-center gap-4'>
<Button variant='outline' onClick={() => history.go(-1)}> <Button variant='outline' onClick={() => history.go(-1)}>
{t('Go Back')} {t('Go Back')}
</Button> </Button>
<Button
variant='outline'
render={
<a
href={FEEDBACK_URL}
target='_blank'
rel='noopener noreferrer'
/>
}
>
{t('Report an issue')}
</Button>
<Button onClick={() => navigate({ to: '/' })}> <Button onClick={() => navigate({ to: '/' })}>
{t('Back to Home')} {t('Back to Home')}
</Button> </Button>
......
...@@ -87,7 +87,10 @@ export function DeploymentAccessGuard({ ...@@ -87,7 +87,10 @@ export function DeploymentAccessGuard({
const navigate = useNavigate() const navigate = useNavigate()
const handleGoToSettings = () => { const handleGoToSettings = () => {
navigate({ to: '/system-settings/integrations' }) navigate({
to: '/system-settings/models/$section',
params: { section: 'model-deployment' },
})
} }
// Combined loading state with step indicator // Combined loading state with step indicator
......
...@@ -168,6 +168,13 @@ export function ModelMutateDrawer({ ...@@ -168,6 +168,13 @@ export function ModelMutateDrawer({
'group_ratio_setting.group_special_usable_group': '{}', 'group_ratio_setting.group_special_usable_group': '{}',
'grok.violation_deduction_enabled': false, 'grok.violation_deduction_enabled': false,
'grok.violation_deduction_amount': 0, 'grok.violation_deduction_amount': 0,
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
'model_deployment.ionet.api_key': '',
'model_deployment.ionet.enabled': false,
} }
return getOptionValue(systemOptionsData.data, defaultModelSettings) return getOptionValue(systemOptionsData.data, defaultModelSettings)
}, [systemOptionsData]) }, [systemOptionsData])
......
import { SettingsPage } from '../components/settings-page' import { SettingsPage } from '../components/settings-page'
import type { IntegrationSettings as IntegrationSettingsType } from '../types' import type { BillingSettings } from '../types'
import { import {
INTEGRATIONS_DEFAULT_SECTION, BILLING_DEFAULT_SECTION,
getIntegrationsSectionContent, getBillingSectionContent,
} from './section-registry.tsx' } from './section-registry.tsx'
const defaultIntegrationSettings: IntegrationSettingsType = { const defaultBillingSettings: BillingSettings = {
SMTPServer: '', QuotaForNewUser: 0,
SMTPPort: '', PreConsumedQuota: 0,
SMTPAccount: '', QuotaForInviter: 0,
SMTPFrom: '', QuotaForInvitee: 0,
SMTPToken: '', TopUpLink: '',
SMTPSSLEnabled: false, 'general_setting.docs_link': '',
SMTPForceAuthLogin: false, 'quota_setting.enable_free_model_pre_consume': true,
WorkerUrl: '', QuotaPerUnit: 500000,
WorkerValidKey: '', USDExchangeRate: 7,
WorkerAllowHttpImageRequestEnabled: false, 'general_setting.quota_display_type': 'USD',
ChannelDisableThreshold: '', 'general_setting.custom_currency_symbol': '¤',
QuotaRemindThreshold: '', 'general_setting.custom_currency_exchange_rate': 1,
AutomaticDisableChannelEnabled: false, DisplayInCurrencyEnabled: true,
AutomaticEnableChannelEnabled: false, DisplayTokenStatEnabled: true,
AutomaticDisableKeywords: '', ModelPrice: '',
AutomaticDisableStatusCodes: '401', ModelRatio: '',
AutomaticRetryStatusCodes: CacheRatio: '',
'100-199,300-399,401-407,409-499,500-503,505-523,525-599', CreateCacheRatio: '',
'monitor_setting.auto_test_channel_enabled': false, CompletionRatio: '',
'monitor_setting.auto_test_channel_minutes': 10, ImageRatio: '',
'model_deployment.ionet.api_key': '', AudioRatio: '',
'model_deployment.ionet.enabled': false, AudioCompletionRatio: '',
ExposeRatioEnabled: false,
'billing_setting.billing_mode': '{}',
'billing_setting.billing_expr': '{}',
'tool_price_setting.prices': '{}',
TopupGroupRatio: '',
GroupRatio: '',
UserUsableGroups: '',
GroupGroupRatio: '',
AutoGroups: '',
DefaultUseAutoGroup: false,
'group_ratio_setting.group_special_usable_group': '{}',
PayAddress: '', PayAddress: '',
EpayId: '', EpayId: '',
EpayKey: '', EpayKey: '',
...@@ -74,15 +85,18 @@ const defaultIntegrationSettings: IntegrationSettingsType = { ...@@ -74,15 +85,18 @@ const defaultIntegrationSettings: IntegrationSettingsType = {
WaffoPancakeCurrency: 'USD', WaffoPancakeCurrency: 'USD',
WaffoPancakeUnitPrice: 1, WaffoPancakeUnitPrice: 1,
WaffoPancakeMinTopUp: 1, WaffoPancakeMinTopUp: 1,
'checkin_setting.enabled': false,
'checkin_setting.min_quota': 1000,
'checkin_setting.max_quota': 10000,
} }
export function IntegrationSettings() { export function BillingSettings() {
return ( return (
<SettingsPage <SettingsPage
routePath='/_authenticated/system-settings/integrations/$section' routePath='/_authenticated/system-settings/billing/$section'
defaultSettings={defaultIntegrationSettings} defaultSettings={defaultBillingSettings}
defaultSection={INTEGRATIONS_DEFAULT_SECTION} defaultSection={BILLING_DEFAULT_SECTION}
getSectionContent={getIntegrationsSectionContent} getSectionContent={getBillingSectionContent}
/> />
) )
} }
import { parseCurrencyDisplayType } from '@/lib/currency'
import type { BillingSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { CheckinSettingsSection } from '../general/checkin-settings-section'
import { PricingSection } from '../general/pricing-section'
import { QuotaSettingsSection } from '../general/quota-settings-section'
import { PaymentSettingsSection } from '../integrations/payment-settings-section'
import { RatioSettingsCard } from '../models/ratio-settings-card'
const getModelDefaults = (settings: BillingSettings) => ({
ModelPrice: settings.ModelPrice,
ModelRatio: settings.ModelRatio,
CacheRatio: settings.CacheRatio,
CreateCacheRatio: settings.CreateCacheRatio,
CompletionRatio: settings.CompletionRatio,
ImageRatio: settings.ImageRatio,
AudioRatio: settings.AudioRatio,
AudioCompletionRatio: settings.AudioCompletionRatio,
ExposeRatioEnabled: settings.ExposeRatioEnabled,
BillingMode: settings['billing_setting.billing_mode'],
BillingExpr: settings['billing_setting.billing_expr'],
})
const getGroupDefaults = (settings: BillingSettings) => ({
TopupGroupRatio: settings.TopupGroupRatio,
GroupRatio: settings.GroupRatio,
UserUsableGroups: settings.UserUsableGroups,
GroupGroupRatio: settings.GroupGroupRatio,
AutoGroups: settings.AutoGroups,
DefaultUseAutoGroup: settings.DefaultUseAutoGroup,
GroupSpecialUsableGroup:
settings['group_ratio_setting.group_special_usable_group'],
})
const BILLING_SECTIONS = [
{
id: 'quota',
titleKey: 'Quota Settings',
descriptionKey: 'Configure user quota allocation and rewards',
build: (settings: BillingSettings) => (
<QuotaSettingsSection
defaultValues={{
QuotaForNewUser: settings.QuotaForNewUser,
PreConsumedQuota: settings.PreConsumedQuota,
QuotaForInviter: settings.QuotaForInviter,
QuotaForInvitee: settings.QuotaForInvitee,
TopUpLink: settings.TopUpLink,
'general_setting.docs_link': settings['general_setting.docs_link'],
'quota_setting.enable_free_model_pre_consume':
settings['quota_setting.enable_free_model_pre_consume'],
}}
/>
),
},
{
id: 'currency',
titleKey: 'Currency & Display',
descriptionKey: 'Configure currency conversion and quota display options',
build: (settings: BillingSettings) => (
<PricingSection
defaultValues={{
QuotaPerUnit: settings.QuotaPerUnit,
USDExchangeRate: settings.USDExchangeRate,
DisplayInCurrencyEnabled: settings.DisplayInCurrencyEnabled,
DisplayTokenStatEnabled: settings.DisplayTokenStatEnabled,
general_setting: {
quota_display_type: parseCurrencyDisplayType(
settings['general_setting.quota_display_type']
),
custom_currency_symbol:
settings['general_setting.custom_currency_symbol'] ?? '¤',
custom_currency_exchange_rate:
settings['general_setting.custom_currency_exchange_rate'] ?? 1,
},
}}
/>
),
},
{
id: 'model-pricing',
titleKey: 'Model Pricing',
descriptionKey: 'Configure model pricing ratios and tool prices',
build: (settings: BillingSettings) => (
<RatioSettingsCard
titleKey='Model Pricing'
descriptionKey='Configure model pricing ratios and tool prices'
modelDefaults={getModelDefaults(settings)}
groupDefaults={getGroupDefaults(settings)}
toolPricesDefault={settings['tool_price_setting.prices']}
visibleTabs={['models', 'tool-prices', 'upstream-sync']}
/>
),
},
{
id: 'group-pricing',
titleKey: 'Group Pricing',
descriptionKey: 'Configure group ratios and group-specific pricing rules',
build: (settings: BillingSettings) => (
<RatioSettingsCard
titleKey='Group Pricing'
descriptionKey='Configure group ratios and group-specific pricing rules'
modelDefaults={getModelDefaults(settings)}
groupDefaults={getGroupDefaults(settings)}
toolPricesDefault={settings['tool_price_setting.prices']}
visibleTabs={['groups']}
/>
),
},
{
id: 'payment',
titleKey: 'Payment Gateway',
descriptionKey: 'Configure payment gateway integrations',
build: (settings: BillingSettings) => (
<PaymentSettingsSection
defaultValues={{
PayAddress: settings.PayAddress,
EpayId: settings.EpayId,
EpayKey: settings.EpayKey,
Price: settings.Price,
MinTopUp: settings.MinTopUp,
CustomCallbackAddress: settings.CustomCallbackAddress,
PayMethods: settings.PayMethods,
AmountOptions: settings['payment_setting.amount_options'],
AmountDiscount: settings['payment_setting.amount_discount'],
StripeApiSecret: settings.StripeApiSecret,
StripeWebhookSecret: settings.StripeWebhookSecret,
StripePriceId: settings.StripePriceId,
StripeUnitPrice: settings.StripeUnitPrice,
StripeMinTopUp: settings.StripeMinTopUp,
StripePromotionCodesEnabled: settings.StripePromotionCodesEnabled,
CreemApiKey: settings.CreemApiKey,
CreemWebhookSecret: settings.CreemWebhookSecret,
CreemTestMode: settings.CreemTestMode,
CreemProducts: settings.CreemProducts,
}}
waffoDefaultValues={{
WaffoEnabled: settings.WaffoEnabled ?? false,
WaffoApiKey: settings.WaffoApiKey ?? '',
WaffoPrivateKey: settings.WaffoPrivateKey ?? '',
WaffoPublicCert: settings.WaffoPublicCert ?? '',
WaffoSandboxPublicCert: settings.WaffoSandboxPublicCert ?? '',
WaffoSandboxApiKey: settings.WaffoSandboxApiKey ?? '',
WaffoSandboxPrivateKey: settings.WaffoSandboxPrivateKey ?? '',
WaffoSandbox: settings.WaffoSandbox ?? false,
WaffoMerchantId: settings.WaffoMerchantId ?? '',
WaffoCurrency: settings.WaffoCurrency ?? 'USD',
WaffoUnitPrice: settings.WaffoUnitPrice ?? 1,
WaffoMinTopUp: settings.WaffoMinTopUp ?? 1,
WaffoNotifyUrl: settings.WaffoNotifyUrl ?? '',
WaffoReturnUrl: settings.WaffoReturnUrl ?? '',
WaffoPayMethods: settings.WaffoPayMethods ?? '[]',
}}
waffoPancakeDefaultValues={{
WaffoPancakeEnabled: settings.WaffoPancakeEnabled ?? false,
WaffoPancakeSandbox: settings.WaffoPancakeSandbox ?? false,
WaffoPancakeMerchantID: settings.WaffoPancakeMerchantID ?? '',
WaffoPancakePrivateKey: settings.WaffoPancakePrivateKey ?? '',
WaffoPancakeWebhookPublicKey:
settings.WaffoPancakeWebhookPublicKey ?? '',
WaffoPancakeWebhookTestKey:
settings.WaffoPancakeWebhookTestKey ?? '',
WaffoPancakeStoreID: settings.WaffoPancakeStoreID ?? '',
WaffoPancakeProductID: settings.WaffoPancakeProductID ?? '',
WaffoPancakeReturnURL: settings.WaffoPancakeReturnURL ?? '',
WaffoPancakeCurrency: settings.WaffoPancakeCurrency ?? 'USD',
WaffoPancakeUnitPrice: settings.WaffoPancakeUnitPrice ?? 1,
WaffoPancakeMinTopUp: settings.WaffoPancakeMinTopUp ?? 1,
}}
/>
),
},
{
id: 'checkin',
titleKey: 'Check-in Rewards',
descriptionKey: 'Configure daily check-in rewards for users',
build: (settings: BillingSettings) => (
<CheckinSettingsSection
defaultValues={{
enabled: settings['checkin_setting.enabled'],
minQuota: settings['checkin_setting.min_quota'],
maxQuota: settings['checkin_setting.max_quota'],
}}
/>
),
},
] as const
export type BillingSectionId = (typeof BILLING_SECTIONS)[number]['id']
const billingRegistry = createSectionRegistry<BillingSectionId, BillingSettings>(
{
sections: BILLING_SECTIONS,
defaultSection: 'quota',
basePath: '/system-settings/billing',
urlStyle: 'path',
}
)
export const BILLING_SECTION_IDS = billingRegistry.sectionIds
export const BILLING_DEFAULT_SECTION = billingRegistry.defaultSection
export const getBillingSectionNavItems = billingRegistry.getSectionNavItems
export const getBillingSectionContent = billingRegistry.getSectionContent
import { useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { parseCurrencyDisplayType } from '@/lib/currency'
import { useSystemOptions, getOptionValue } from '../hooks/use-system-options'
import type { GeneralSettings } from '../types'
import {
GENERAL_DEFAULT_SECTION,
getGeneralSectionContent,
} from './section-registry.tsx'
const defaultGeneralSettings: GeneralSettings = {
'theme.frontend': 'default',
Notice: '',
SystemName: 'New API',
Logo: '',
Footer: '',
About: '',
HomePageContent: '',
ServerAddress: '',
'legal.user_agreement': '',
'legal.privacy_policy': '',
QuotaForNewUser: 0,
PreConsumedQuota: 0,
QuotaForInviter: 0,
QuotaForInvitee: 0,
TopUpLink: '',
'general_setting.docs_link': '',
'quota_setting.enable_free_model_pre_consume': true,
QuotaPerUnit: 500000,
USDExchangeRate: 7,
'general_setting.quota_display_type': 'USD',
'general_setting.custom_currency_symbol': '¤',
'general_setting.custom_currency_exchange_rate': 1,
RetryTimes: 0,
DisplayInCurrencyEnabled: true,
DisplayTokenStatEnabled: true,
DefaultCollapseSidebar: false,
DemoSiteEnabled: false,
SelfUseModeEnabled: false,
'checkin_setting.enabled': false,
'checkin_setting.min_quota': 1000,
'checkin_setting.max_quota': 10000,
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
}
export function GeneralSettings() {
const { t } = useTranslation()
const { data, isLoading } = useSystemOptions()
const params = useParams({
from: '/_authenticated/system-settings/general/$section',
})
if (isLoading) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-muted-foreground'>{t('Loading settings...')}</div>
</div>
)
}
const settings = getOptionValue(data?.data, defaultGeneralSettings)
const quotaDisplayType = parseCurrencyDisplayType(
settings['general_setting.quota_display_type']
)
const activeSection = (params?.section ?? GENERAL_DEFAULT_SECTION) as
| 'system-info'
| 'quota'
| 'pricing'
| 'checkin'
| 'behavior'
| 'channel-affinity'
const sectionContent = getGeneralSectionContent(
activeSection,
settings,
quotaDisplayType
)
return (
<div className='flex h-full w-full flex-1 flex-col'>
<div className='faded-bottom h-full w-full overflow-y-auto scroll-smooth pe-4 pb-12'>
<div className='space-y-4'>{sectionContent}</div>
</div>
</div>
)
}
import type { GeneralSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { ChannelAffinitySection } from './channel-affinity'
import { CheckinSettingsSection } from './checkin-settings-section'
import { PricingSection } from './pricing-section'
import { QuotaSettingsSection } from './quota-settings-section'
import { SystemBehaviorSection } from './system-behavior-section'
import { SystemInfoSection } from './system-info-section'
const GENERAL_SECTIONS = [
{
id: 'system-info',
titleKey: 'System Information',
descriptionKey: 'Configure basic system information and branding',
build: (settings: GeneralSettings) => (
<SystemInfoSection
defaultValues={{
theme: {
frontend: settings['theme.frontend'] as 'default' | 'classic',
},
Notice: settings.Notice,
SystemName: settings.SystemName,
Logo: settings.Logo,
Footer: settings.Footer,
About: settings.About,
HomePageContent: settings.HomePageContent,
ServerAddress: settings.ServerAddress,
legal: {
user_agreement: settings['legal.user_agreement'],
privacy_policy: settings['legal.privacy_policy'],
},
}}
/>
),
},
{
id: 'quota',
titleKey: 'Quota Settings',
descriptionKey: 'Configure user quota allocation and rewards',
build: (settings: GeneralSettings) => (
<QuotaSettingsSection
defaultValues={{
QuotaForNewUser: settings.QuotaForNewUser,
PreConsumedQuota: settings.PreConsumedQuota,
QuotaForInviter: settings.QuotaForInviter,
QuotaForInvitee: settings.QuotaForInvitee,
TopUpLink: settings.TopUpLink,
'general_setting.docs_link': settings['general_setting.docs_link'],
'quota_setting.enable_free_model_pre_consume':
settings['quota_setting.enable_free_model_pre_consume'],
}}
/>
),
},
{
id: 'pricing',
titleKey: 'Pricing & Display',
descriptionKey: 'Configure pricing model and display options',
build: (
settings: GeneralSettings,
quotaDisplayType: 'USD' | 'CNY' | 'TOKENS' | 'CUSTOM'
) => (
<PricingSection
defaultValues={{
QuotaPerUnit: settings.QuotaPerUnit,
USDExchangeRate: settings.USDExchangeRate,
DisplayInCurrencyEnabled: settings.DisplayInCurrencyEnabled,
DisplayTokenStatEnabled: settings.DisplayTokenStatEnabled,
general_setting: {
quota_display_type: quotaDisplayType,
custom_currency_symbol:
settings['general_setting.custom_currency_symbol'] ?? '¤',
custom_currency_exchange_rate:
settings['general_setting.custom_currency_exchange_rate'] ?? 1,
},
}}
/>
),
},
{
id: 'checkin',
titleKey: 'Check-in Settings',
descriptionKey: 'Configure daily check-in rewards for users',
build: (settings: GeneralSettings) => (
<CheckinSettingsSection
defaultValues={{
enabled: settings['checkin_setting.enabled'],
minQuota: settings['checkin_setting.min_quota'],
maxQuota: settings['checkin_setting.max_quota'],
}}
/>
),
},
{
id: 'behavior',
titleKey: 'System Behavior',
descriptionKey: 'Configure system-wide behavior and defaults',
build: (settings: GeneralSettings) => (
<SystemBehaviorSection
defaultValues={{
RetryTimes: settings.RetryTimes,
DefaultCollapseSidebar: settings.DefaultCollapseSidebar,
DemoSiteEnabled: settings.DemoSiteEnabled,
SelfUseModeEnabled: settings.SelfUseModeEnabled,
}}
/>
),
},
{
id: 'channel-affinity',
titleKey: 'Channel Affinity',
descriptionKey: 'Configure channel affinity (sticky routing) rules',
build: (settings: GeneralSettings) => (
<ChannelAffinitySection
defaultValues={{
'channel_affinity_setting.enabled':
settings['channel_affinity_setting.enabled'],
'channel_affinity_setting.switch_on_success':
settings['channel_affinity_setting.switch_on_success'],
'channel_affinity_setting.max_entries':
settings['channel_affinity_setting.max_entries'],
'channel_affinity_setting.default_ttl_seconds':
settings['channel_affinity_setting.default_ttl_seconds'],
'channel_affinity_setting.rules':
settings['channel_affinity_setting.rules'],
}}
/>
),
},
] as const
export type GeneralSectionId = (typeof GENERAL_SECTIONS)[number]['id']
const generalRegistry = createSectionRegistry<
GeneralSectionId,
GeneralSettings,
['USD' | 'CNY' | 'TOKENS' | 'CUSTOM']
>({
sections: GENERAL_SECTIONS,
defaultSection: 'system-info',
basePath: '/system-settings/general',
urlStyle: 'path',
})
export const GENERAL_SECTION_IDS = generalRegistry.sectionIds
export const GENERAL_DEFAULT_SECTION = generalRegistry.defaultSection
export const getGeneralSectionNavItems = generalRegistry.getSectionNavItems
export const getGeneralSectionContent = generalRegistry.getSectionContent
...@@ -32,7 +32,6 @@ const _systemInfoSchema = z.object({ ...@@ -32,7 +32,6 @@ const _systemInfoSchema = z.object({
theme: z.object({ theme: z.object({
frontend: z.enum(['default', 'classic']), frontend: z.enum(['default', 'classic']),
}), }),
Notice: z.string().optional(),
SystemName: z.string().min(1), SystemName: z.string().min(1),
ServerAddress: z.string().optional(), ServerAddress: z.string().optional(),
Logo: z.string().url().optional().or(z.literal('')), Logo: z.string().url().optional().or(z.literal('')),
...@@ -65,7 +64,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) { ...@@ -65,7 +64,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
frontend: frontend:
defaultValues.theme?.frontend === 'classic' ? 'classic' : 'default', defaultValues.theme?.frontend === 'classic' ? 'classic' : 'default',
}, },
Notice: normalizeValue(defaultValues.Notice),
SystemName: normalizeValue(defaultValues.SystemName), SystemName: normalizeValue(defaultValues.SystemName),
ServerAddress: normalizeValue(defaultValues.ServerAddress), ServerAddress: normalizeValue(defaultValues.ServerAddress),
Logo: normalizeValue(defaultValues.Logo), Logo: normalizeValue(defaultValues.Logo),
...@@ -82,7 +80,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) { ...@@ -82,7 +80,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
theme: z.object({ theme: z.object({
frontend: z.enum(['default', 'classic']), frontend: z.enum(['default', 'classic']),
}), }),
Notice: z.string().optional(),
SystemName: z.string().min(1, { SystemName: z.string().min(1, {
error: () => t('System name is required'), error: () => t('System name is required'),
}), }),
...@@ -163,31 +160,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) { ...@@ -163,31 +160,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
<FormField <FormField
control={form.control} control={form.control}
name='Notice'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Notice')}</FormLabel>
<FormControl>
<Textarea
placeholder={t(
'Enter announcement content (supports Markdown & HTML)'
)}
rows={6}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Announcement displayed to users (supports Markdown & HTML)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='SystemName' name='SystemName'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
......
import type { IntegrationSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { EmailSettingsSection } from './email-settings-section'
import { IoNetDeploymentSettingsSection } from './ionet-deployment-settings-section'
import { MonitoringSettingsSection } from './monitoring-settings-section'
import { PaymentSettingsSection } from './payment-settings-section'
import { WorkerSettingsSection } from './worker-settings-section'
const INTEGRATIONS_SECTIONS = [
{
id: 'payment',
titleKey: 'Payment Gateway',
descriptionKey: 'Configure payment gateway integrations',
build: (settings: IntegrationSettings) => (
<PaymentSettingsSection
defaultValues={{
PayAddress: settings.PayAddress,
EpayId: settings.EpayId,
EpayKey: settings.EpayKey,
Price: settings.Price,
MinTopUp: settings.MinTopUp,
CustomCallbackAddress: settings.CustomCallbackAddress,
PayMethods: settings.PayMethods,
AmountOptions: settings['payment_setting.amount_options'],
AmountDiscount: settings['payment_setting.amount_discount'],
StripeApiSecret: settings.StripeApiSecret,
StripeWebhookSecret: settings.StripeWebhookSecret,
StripePriceId: settings.StripePriceId,
StripeUnitPrice: settings.StripeUnitPrice,
StripeMinTopUp: settings.StripeMinTopUp,
StripePromotionCodesEnabled: settings.StripePromotionCodesEnabled,
CreemApiKey: settings.CreemApiKey,
CreemWebhookSecret: settings.CreemWebhookSecret,
CreemTestMode: settings.CreemTestMode,
CreemProducts: settings.CreemProducts,
}}
waffoDefaultValues={{
WaffoEnabled: settings.WaffoEnabled ?? false,
WaffoApiKey: settings.WaffoApiKey ?? '',
WaffoPrivateKey: settings.WaffoPrivateKey ?? '',
WaffoPublicCert: settings.WaffoPublicCert ?? '',
WaffoSandboxPublicCert: settings.WaffoSandboxPublicCert ?? '',
WaffoSandboxApiKey: settings.WaffoSandboxApiKey ?? '',
WaffoSandboxPrivateKey: settings.WaffoSandboxPrivateKey ?? '',
WaffoSandbox: settings.WaffoSandbox ?? false,
WaffoMerchantId: settings.WaffoMerchantId ?? '',
WaffoCurrency: settings.WaffoCurrency ?? 'USD',
WaffoUnitPrice: settings.WaffoUnitPrice ?? 1,
WaffoMinTopUp: settings.WaffoMinTopUp ?? 1,
WaffoNotifyUrl: settings.WaffoNotifyUrl ?? '',
WaffoReturnUrl: settings.WaffoReturnUrl ?? '',
WaffoPayMethods: settings.WaffoPayMethods ?? '[]',
}}
waffoPancakeDefaultValues={{
WaffoPancakeEnabled: settings.WaffoPancakeEnabled ?? false,
WaffoPancakeSandbox: settings.WaffoPancakeSandbox ?? false,
WaffoPancakeMerchantID: settings.WaffoPancakeMerchantID ?? '',
WaffoPancakePrivateKey: settings.WaffoPancakePrivateKey ?? '',
WaffoPancakeWebhookPublicKey:
settings.WaffoPancakeWebhookPublicKey ?? '',
WaffoPancakeWebhookTestKey: settings.WaffoPancakeWebhookTestKey ?? '',
WaffoPancakeStoreID: settings.WaffoPancakeStoreID ?? '',
WaffoPancakeProductID: settings.WaffoPancakeProductID ?? '',
WaffoPancakeReturnURL: settings.WaffoPancakeReturnURL ?? '',
WaffoPancakeCurrency: settings.WaffoPancakeCurrency ?? 'USD',
WaffoPancakeUnitPrice: settings.WaffoPancakeUnitPrice ?? 1,
WaffoPancakeMinTopUp: settings.WaffoPancakeMinTopUp ?? 1,
}}
/>
),
},
{
id: 'email',
titleKey: 'SMTP Email',
descriptionKey: 'Configure SMTP email settings',
build: (settings: IntegrationSettings) => (
<EmailSettingsSection
defaultValues={{
SMTPServer: settings.SMTPServer,
SMTPPort: settings.SMTPPort,
SMTPAccount: settings.SMTPAccount,
SMTPFrom: settings.SMTPFrom,
SMTPToken: settings.SMTPToken,
SMTPSSLEnabled: settings.SMTPSSLEnabled,
SMTPForceAuthLogin: settings.SMTPForceAuthLogin,
}}
/>
),
},
{
id: 'worker',
titleKey: 'Worker Proxy',
descriptionKey: 'Configure worker service settings',
build: (settings: IntegrationSettings) => (
<WorkerSettingsSection
defaultValues={{
WorkerUrl: settings.WorkerUrl,
WorkerValidKey: settings.WorkerValidKey,
WorkerAllowHttpImageRequestEnabled:
settings.WorkerAllowHttpImageRequestEnabled,
}}
/>
),
},
{
id: 'ionet',
titleKey: 'io.net Deployments',
descriptionKey: 'Configure IoNet model deployment settings',
build: (settings: IntegrationSettings) => (
<IoNetDeploymentSettingsSection
defaultValues={{
enabled: settings['model_deployment.ionet.enabled'],
apiKey: settings['model_deployment.ionet.api_key'],
}}
/>
),
},
{
id: 'monitoring',
titleKey: 'Monitoring & Alerts',
descriptionKey: 'Configure channel monitoring and automation',
build: (settings: IntegrationSettings) => (
<MonitoringSettingsSection
defaultValues={{
ChannelDisableThreshold: settings.ChannelDisableThreshold,
QuotaRemindThreshold: settings.QuotaRemindThreshold,
AutomaticDisableChannelEnabled:
settings.AutomaticDisableChannelEnabled,
AutomaticEnableChannelEnabled: settings.AutomaticEnableChannelEnabled,
AutomaticDisableKeywords: settings.AutomaticDisableKeywords,
AutomaticDisableStatusCodes: settings.AutomaticDisableStatusCodes,
AutomaticRetryStatusCodes: settings.AutomaticRetryStatusCodes,
'monitor_setting.auto_test_channel_enabled':
settings['monitor_setting.auto_test_channel_enabled'],
'monitor_setting.auto_test_channel_minutes':
settings['monitor_setting.auto_test_channel_minutes'],
}}
/>
),
},
] as const
export type IntegrationSectionId = (typeof INTEGRATIONS_SECTIONS)[number]['id']
const integrationsRegistry = createSectionRegistry<
IntegrationSectionId,
IntegrationSettings
>({
sections: INTEGRATIONS_SECTIONS,
defaultSection: 'payment',
basePath: '/system-settings/integrations',
urlStyle: 'path',
})
export const INTEGRATIONS_SECTION_IDS = integrationsRegistry.sectionIds
export const INTEGRATIONS_DEFAULT_SECTION = integrationsRegistry.defaultSection
export const getIntegrationsSectionNavItems =
integrationsRegistry.getSectionNavItems
export const getIntegrationsSectionContent =
integrationsRegistry.getSectionContent
import type { MaintenanceSettings } from '../types'
export type HeaderNavPricingConfig = { export type HeaderNavPricingConfig = {
enabled: boolean enabled: boolean
requireAuth: boolean requireAuth: boolean
...@@ -62,25 +60,6 @@ export const SIDEBAR_MODULES_DEFAULT: SidebarModulesAdminConfig = { ...@@ -62,25 +60,6 @@ export const SIDEBAR_MODULES_DEFAULT: SidebarModulesAdminConfig = {
}, },
} }
export const DEFAULT_MAINTENANCE_SETTINGS: MaintenanceSettings = {
Notice: '',
LogConsumeEnabled: false,
HeaderNavModules: JSON.stringify(HEADER_NAV_DEFAULT),
SidebarModulesAdmin: JSON.stringify(SIDEBAR_MODULES_DEFAULT),
'performance_setting.disk_cache_enabled': false,
'performance_setting.disk_cache_threshold_mb': 10,
'performance_setting.disk_cache_max_size_mb': 1024,
'performance_setting.disk_cache_path': '',
'performance_setting.monitor_enabled': false,
'performance_setting.monitor_cpu_threshold': 90,
'performance_setting.monitor_memory_threshold': 90,
'performance_setting.monitor_disk_threshold': 95,
'perf_metrics_setting.enabled': true,
'perf_metrics_setting.flush_interval': 5,
'perf_metrics_setting.bucket_time': 'hour',
'perf_metrics_setting.retention_days': 0,
}
const toBoolean = (value: unknown, fallback: boolean): boolean => { const toBoolean = (value: unknown, fallback: boolean): boolean => {
if (typeof value === 'boolean') return value if (typeof value === 'boolean') return value
if (typeof value === 'number') return value === 1 if (typeof value === 'number') return value === 1
......
import type { MaintenanceSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import {
parseHeaderNavModules,
parseSidebarModulesAdmin,
serializeHeaderNavModules,
serializeSidebarModulesAdmin,
} from './config'
import { HeaderNavigationSection } from './header-navigation-section'
import { LogSettingsSection } from './log-settings-section'
import { NoticeSection } from './notice-section'
import { PerformanceSection } from './performance-section'
import { SidebarModulesSection } from './sidebar-modules-section'
import { UpdateCheckerSection } from './update-checker-section'
const MAINTENANCE_SECTIONS = [
{
id: 'update-checker',
titleKey: 'System maintenance',
descriptionKey: 'Check for system updates',
build: (
_settings: MaintenanceSettings,
currentVersion?: string | null,
startTime?: number | null
) => (
<UpdateCheckerSection
currentVersion={currentVersion}
startTime={startTime}
/>
),
},
{
id: 'notice',
titleKey: 'System Notice',
descriptionKey: 'Configure system maintenance notice',
build: (settings: MaintenanceSettings) => (
<NoticeSection defaultValue={settings.Notice ?? ''} />
),
},
{
id: 'logs',
titleKey: 'Log Maintenance',
descriptionKey: 'Configure log consumption settings',
build: (settings: MaintenanceSettings) => (
<LogSettingsSection
defaultEnabled={Boolean(settings.LogConsumeEnabled)}
/>
),
},
{
id: 'header-navigation',
titleKey: 'Header navigation',
descriptionKey: 'Configure header navigation modules',
build: (settings: MaintenanceSettings) => {
const headerNavConfig = parseHeaderNavModules(settings.HeaderNavModules)
const headerNavSerialized = serializeHeaderNavModules(headerNavConfig)
return (
<HeaderNavigationSection
config={headerNavConfig}
initialSerialized={headerNavSerialized}
/>
)
},
},
{
id: 'sidebar-modules',
titleKey: 'Sidebar modules',
descriptionKey: 'Configure sidebar modules for admin',
build: (settings: MaintenanceSettings) => {
const sidebarConfig = parseSidebarModulesAdmin(
settings.SidebarModulesAdmin
)
const sidebarSerialized = serializeSidebarModulesAdmin(sidebarConfig)
return (
<SidebarModulesSection
config={sidebarConfig}
initialSerialized={sidebarSerialized}
/>
)
},
},
{
id: 'performance',
titleKey: 'Performance',
descriptionKey: 'Disk cache, system monitoring and performance stats',
build: (settings: MaintenanceSettings) => (
<PerformanceSection
defaultValues={{
'performance_setting.disk_cache_enabled':
settings['performance_setting.disk_cache_enabled'] ?? false,
'performance_setting.disk_cache_threshold_mb':
settings['performance_setting.disk_cache_threshold_mb'] ?? 10,
'performance_setting.disk_cache_max_size_mb':
settings['performance_setting.disk_cache_max_size_mb'] ?? 1024,
'performance_setting.disk_cache_path':
settings['performance_setting.disk_cache_path'] ?? '',
'performance_setting.monitor_enabled':
settings['performance_setting.monitor_enabled'] ?? false,
'performance_setting.monitor_cpu_threshold':
settings['performance_setting.monitor_cpu_threshold'] ?? 90,
'performance_setting.monitor_memory_threshold':
settings['performance_setting.monitor_memory_threshold'] ?? 90,
'performance_setting.monitor_disk_threshold':
settings['performance_setting.monitor_disk_threshold'] ?? 95,
'perf_metrics_setting.enabled':
settings['perf_metrics_setting.enabled'] ?? true,
'perf_metrics_setting.flush_interval':
settings['perf_metrics_setting.flush_interval'] ?? 5,
'perf_metrics_setting.bucket_time':
settings['perf_metrics_setting.bucket_time'] ?? 'hour',
'perf_metrics_setting.retention_days':
settings['perf_metrics_setting.retention_days'] ?? 0,
}}
/>
),
},
] as const
export type MaintenanceSectionId = (typeof MAINTENANCE_SECTIONS)[number]['id']
const maintenanceRegistry = createSectionRegistry<
MaintenanceSectionId,
MaintenanceSettings,
[string | null | undefined, number | null | undefined]
>({
sections: MAINTENANCE_SECTIONS,
defaultSection: 'update-checker',
basePath: '/system-settings/maintenance',
urlStyle: 'path',
})
export const MAINTENANCE_SECTION_IDS = maintenanceRegistry.sectionIds
export const MAINTENANCE_DEFAULT_SECTION = maintenanceRegistry.defaultSection
export const getMaintenanceSectionNavItems =
maintenanceRegistry.getSectionNavItems
export const getMaintenanceSectionContent =
maintenanceRegistry.getSectionContent
...@@ -43,6 +43,13 @@ const defaultModelSettings: ModelSettings = { ...@@ -43,6 +43,13 @@ const defaultModelSettings: ModelSettings = {
AutoGroups: '', AutoGroups: '',
DefaultUseAutoGroup: false, DefaultUseAutoGroup: false,
'group_ratio_setting.group_special_usable_group': '{}', 'group_ratio_setting.group_special_usable_group': '{}',
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
'model_deployment.ionet.api_key': '',
'model_deployment.ionet.enabled': false,
} }
export function ModelSettings() { export function ModelSettings() {
......
...@@ -179,17 +179,24 @@ const groupSchema = z.object({ ...@@ -179,17 +179,24 @@ const groupSchema = z.object({
type ModelFormValues = z.infer<typeof modelSchema> type ModelFormValues = z.infer<typeof modelSchema>
type GroupFormValues = z.infer<typeof groupSchema> type GroupFormValues = z.infer<typeof groupSchema>
type RatioTabId = 'models' | 'groups' | 'tool-prices' | 'upstream-sync'
type RatioSettingsCardProps = { type RatioSettingsCardProps = {
modelDefaults: ModelFormValues modelDefaults: ModelFormValues
groupDefaults: GroupFormValues groupDefaults: GroupFormValues
toolPricesDefault: string toolPricesDefault: string
titleKey?: string
descriptionKey?: string
visibleTabs?: RatioTabId[]
} }
export function RatioSettingsCard({ export function RatioSettingsCard({
modelDefaults, modelDefaults,
groupDefaults, groupDefaults,
toolPricesDefault, toolPricesDefault,
titleKey = 'Pricing Ratios',
descriptionKey = 'Configure model, caching, and group ratios used for billing',
visibleTabs = ['models', 'groups', 'tool-prices', 'upstream-sync'],
}: RatioSettingsCardProps) { }: RatioSettingsCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
const updateOption = useUpdateOption() const updateOption = useUpdateOption()
...@@ -414,61 +421,79 @@ export function RatioSettingsCard({ ...@@ -414,61 +421,79 @@ export function RatioSettingsCard({
resetMutate() resetMutate()
}, [resetMutate]) }, [resetMutate])
return ( const tabLabels: Record<RatioTabId, string> = {
<SettingsSection models: 'Model ratios',
title={t('Pricing Ratios')} groups: 'Group ratios',
description={t( 'tool-prices': 'Tool prices',
'Configure model, caching, and group ratios used for billing' 'upstream-sync': 'Upstream price sync',
)} }
> const tabsGridClass =
<Tabs defaultValue='models' className='space-y-6'> {
<TabsList className='grid w-full grid-cols-4'> 1: 'grid-cols-1',
<TabsTrigger value='models'>{t('Model ratios')}</TabsTrigger> 2: 'grid-cols-2',
<TabsTrigger value='groups'>{t('Group ratios')}</TabsTrigger> 3: 'grid-cols-3',
<TabsTrigger value='tool-prices'>{t('Tool prices')}</TabsTrigger> 4: 'grid-cols-4',
<TabsTrigger value='upstream-sync'> }[visibleTabs.length] ?? 'grid-cols-4'
{t('Upstream price sync')} const defaultTab = visibleTabs[0] ?? 'models'
</TabsTrigger>
</TabsList>
<TabsContent value='models'>
<ModelRatioForm
form={modelForm}
onSave={saveModelRatios}
onReset={handleResetRatios}
isSaving={updateOption.isPending}
isResetting={resetMutation.isPending}
/>
</TabsContent>
<TabsContent value='groups'> const renderTabContent = (tab: RatioTabId) => {
<GroupRatioForm if (tab === 'models') {
form={groupForm} return (
onSave={saveGroupRatios} <ModelRatioForm
isSaving={updateOption.isPending} form={modelForm}
/> onSave={saveModelRatios}
</TabsContent> onReset={handleResetRatios}
isSaving={updateOption.isPending}
isResetting={resetMutation.isPending}
/>
)
}
if (tab === 'groups') {
return (
<GroupRatioForm
form={groupForm}
onSave={saveGroupRatios}
isSaving={updateOption.isPending}
/>
)
}
if (tab === 'tool-prices') {
return <ToolPriceSettings defaultValue={toolPricesDefault} />
}
return (
<UpstreamRatioSync
modelRatios={{
ModelPrice: modelDefaults.ModelPrice,
ModelRatio: modelDefaults.ModelRatio,
CompletionRatio: modelDefaults.CompletionRatio,
CacheRatio: modelDefaults.CacheRatio,
CreateCacheRatio: modelDefaults.CreateCacheRatio,
ImageRatio: modelDefaults.ImageRatio,
AudioRatio: modelDefaults.AudioRatio,
AudioCompletionRatio: modelDefaults.AudioCompletionRatio,
'billing_setting.billing_mode': modelDefaults.BillingMode,
'billing_setting.billing_expr': modelDefaults.BillingExpr,
}}
/>
)
}
<TabsContent value='tool-prices'> return (
<ToolPriceSettings defaultValue={toolPricesDefault} /> <SettingsSection title={t(titleKey)} description={t(descriptionKey)}>
</TabsContent> <Tabs defaultValue={defaultTab} className='space-y-6'>
<TabsList className={`grid w-full ${tabsGridClass}`}>
{visibleTabs.map((tab) => (
<TabsTrigger key={tab} value={tab}>
{t(tabLabels[tab])}
</TabsTrigger>
))}
</TabsList>
<TabsContent value='upstream-sync'> {visibleTabs.map((tab) => (
<UpstreamRatioSync <TabsContent key={tab} value={tab}>
modelRatios={{ {renderTabContent(tab)}
ModelPrice: modelDefaults.ModelPrice, </TabsContent>
ModelRatio: modelDefaults.ModelRatio, ))}
CompletionRatio: modelDefaults.CompletionRatio,
CacheRatio: modelDefaults.CacheRatio,
CreateCacheRatio: modelDefaults.CreateCacheRatio,
ImageRatio: modelDefaults.ImageRatio,
AudioRatio: modelDefaults.AudioRatio,
AudioCompletionRatio: modelDefaults.AudioCompletionRatio,
'billing_setting.billing_mode': modelDefaults.BillingMode,
'billing_setting.billing_expr': modelDefaults.BillingExpr,
}}
/>
</TabsContent>
</Tabs> </Tabs>
<ConfirmDialog <ConfirmDialog
......
...@@ -4,7 +4,8 @@ import { ClaudeSettingsCard } from './claude-settings-card' ...@@ -4,7 +4,8 @@ import { ClaudeSettingsCard } from './claude-settings-card'
import { GeminiSettingsCard } from './gemini-settings-card' import { GeminiSettingsCard } from './gemini-settings-card'
import { GlobalSettingsCard } from './global-settings-card' import { GlobalSettingsCard } from './global-settings-card'
import { GrokSettingsCard } from './grok-settings-card' import { GrokSettingsCard } from './grok-settings-card'
import { RatioSettingsCard } from './ratio-settings-card' import { ChannelAffinitySection } from '../general/channel-affinity'
import { IoNetDeploymentSettingsSection } from '../integrations/ionet-deployment-settings-section'
function formatJsonForEditor(value: string, fallback: string) { function formatJsonForEditor(value: string, fallback: string) {
const raw = (value ?? '').toString().trim() const raw = (value ?? '').toString().trim()
...@@ -106,34 +107,35 @@ const MODELS_SECTIONS = [ ...@@ -106,34 +107,35 @@ const MODELS_SECTIONS = [
), ),
}, },
{ {
id: 'ratio', id: 'channel-affinity',
titleKey: 'Pricing Ratios', titleKey: 'Channel Affinity',
descriptionKey: 'Configure model pricing and ratio settings', descriptionKey: 'Configure channel affinity (sticky routing) rules',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
<RatioSettingsCard <ChannelAffinitySection
modelDefaults={{ defaultValues={{
ModelPrice: settings.ModelPrice, 'channel_affinity_setting.enabled':
ModelRatio: settings.ModelRatio, settings['channel_affinity_setting.enabled'],
CacheRatio: settings.CacheRatio, 'channel_affinity_setting.switch_on_success':
CreateCacheRatio: settings.CreateCacheRatio, settings['channel_affinity_setting.switch_on_success'],
CompletionRatio: settings.CompletionRatio, 'channel_affinity_setting.max_entries':
ImageRatio: settings.ImageRatio, settings['channel_affinity_setting.max_entries'],
AudioRatio: settings.AudioRatio, 'channel_affinity_setting.default_ttl_seconds':
AudioCompletionRatio: settings.AudioCompletionRatio, settings['channel_affinity_setting.default_ttl_seconds'],
ExposeRatioEnabled: settings.ExposeRatioEnabled, 'channel_affinity_setting.rules':
BillingMode: settings['billing_setting.billing_mode'], settings['channel_affinity_setting.rules'],
BillingExpr: settings['billing_setting.billing_expr'],
}} }}
toolPricesDefault={settings['tool_price_setting.prices']} />
groupDefaults={{ ),
TopupGroupRatio: settings.TopupGroupRatio, },
GroupRatio: settings.GroupRatio, {
UserUsableGroups: settings.UserUsableGroups, id: 'model-deployment',
GroupGroupRatio: settings.GroupGroupRatio, titleKey: 'Model Deployment',
AutoGroups: settings.AutoGroups, descriptionKey: 'Configure model deployment provider settings',
DefaultUseAutoGroup: settings.DefaultUseAutoGroup, build: (settings: ModelSettings) => (
GroupSpecialUsableGroup: <IoNetDeploymentSettingsSection
settings['group_ratio_setting.group_special_usable_group'], defaultValues={{
enabled: settings['model_deployment.ionet.enabled'],
apiKey: settings['model_deployment.ionet.api_key'],
}} }}
/> />
), ),
......
...@@ -3,22 +3,62 @@ import { useParams } from '@tanstack/react-router' ...@@ -3,22 +3,62 @@ import { useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useStatus } from '@/hooks/use-status' import { useStatus } from '@/hooks/use-status'
import { getOptionValue, useSystemOptions } from '../hooks/use-system-options' import { getOptionValue, useSystemOptions } from '../hooks/use-system-options'
import { DEFAULT_MAINTENANCE_SETTINGS } from './config' import type { OperationsSettings } from '../types'
import { import {
MAINTENANCE_DEFAULT_SECTION, OPERATIONS_DEFAULT_SECTION,
getMaintenanceSectionContent, getOperationsSectionContent,
} from './section-registry.tsx' } from './section-registry.tsx'
export function MaintenanceSettings() { const defaultOperationsSettings: OperationsSettings = {
RetryTimes: 0,
DefaultCollapseSidebar: false,
DemoSiteEnabled: false,
SelfUseModeEnabled: false,
ChannelDisableThreshold: '',
QuotaRemindThreshold: '',
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDisableKeywords: '',
AutomaticDisableStatusCodes: '401',
AutomaticRetryStatusCodes:
'100-199,300-399,401-407,409-499,500-503,505-523,525-599',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10,
SMTPServer: '',
SMTPPort: '',
SMTPAccount: '',
SMTPFrom: '',
SMTPToken: '',
SMTPSSLEnabled: false,
SMTPForceAuthLogin: false,
WorkerUrl: '',
WorkerValidKey: '',
WorkerAllowHttpImageRequestEnabled: false,
LogConsumeEnabled: false,
'performance_setting.disk_cache_enabled': false,
'performance_setting.disk_cache_threshold_mb': 10,
'performance_setting.disk_cache_max_size_mb': 1024,
'performance_setting.disk_cache_path': '',
'performance_setting.monitor_enabled': false,
'performance_setting.monitor_cpu_threshold': 90,
'performance_setting.monitor_memory_threshold': 90,
'performance_setting.monitor_disk_threshold': 95,
'perf_metrics_setting.enabled': true,
'perf_metrics_setting.flush_interval': 5,
'perf_metrics_setting.bucket_time': 'hour',
'perf_metrics_setting.retention_days': 0,
}
export function OperationsSettings() {
const { t } = useTranslation() const { t } = useTranslation()
const { data, isLoading } = useSystemOptions() const { data, isLoading } = useSystemOptions()
const { status } = useStatus() const { status } = useStatus()
const params = useParams({ const params = useParams({
from: '/_authenticated/system-settings/maintenance/$section', from: '/_authenticated/system-settings/operations/$section',
}) })
const settings = useMemo( const settings = useMemo(
() => getOptionValue(data?.data, DEFAULT_MAINTENANCE_SETTINGS), () => getOptionValue(data?.data, defaultOperationsSettings),
[data?.data] [data?.data]
) )
...@@ -30,14 +70,15 @@ export function MaintenanceSettings() { ...@@ -30,14 +70,15 @@ export function MaintenanceSettings() {
) )
} }
const activeSection = (params?.section ?? MAINTENANCE_DEFAULT_SECTION) as const activeSection = (params?.section ?? OPERATIONS_DEFAULT_SECTION) as
| 'update-checker' | 'behavior'
| 'notice' | 'monitoring'
| 'email'
| 'worker'
| 'logs' | 'logs'
| 'header-navigation'
| 'sidebar-modules'
| 'performance' | 'performance'
const sectionContent = getMaintenanceSectionContent( | 'update-checker'
const sectionContent = getOperationsSectionContent(
activeSection, activeSection,
settings, settings,
status?.version as string | undefined, status?.version as string | undefined,
......
import type { OperationsSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { SystemBehaviorSection } from '../general/system-behavior-section'
import { EmailSettingsSection } from '../integrations/email-settings-section'
import { MonitoringSettingsSection } from '../integrations/monitoring-settings-section'
import { WorkerSettingsSection } from '../integrations/worker-settings-section'
import { LogSettingsSection } from '../maintenance/log-settings-section'
import { PerformanceSection } from '../maintenance/performance-section'
import { UpdateCheckerSection } from '../maintenance/update-checker-section'
const OPERATIONS_SECTIONS = [
{
id: 'behavior',
titleKey: 'System Behavior',
descriptionKey: 'Configure system-wide behavior and defaults',
build: (settings: OperationsSettings) => (
<SystemBehaviorSection
defaultValues={{
RetryTimes: settings.RetryTimes,
DefaultCollapseSidebar: settings.DefaultCollapseSidebar,
DemoSiteEnabled: settings.DemoSiteEnabled,
SelfUseModeEnabled: settings.SelfUseModeEnabled,
}}
/>
),
},
{
id: 'monitoring',
titleKey: 'Monitoring & Alerts',
descriptionKey: 'Configure channel monitoring and automation',
build: (settings: OperationsSettings) => (
<MonitoringSettingsSection
defaultValues={{
ChannelDisableThreshold: settings.ChannelDisableThreshold,
QuotaRemindThreshold: settings.QuotaRemindThreshold,
AutomaticDisableChannelEnabled:
settings.AutomaticDisableChannelEnabled,
AutomaticEnableChannelEnabled: settings.AutomaticEnableChannelEnabled,
AutomaticDisableKeywords: settings.AutomaticDisableKeywords,
AutomaticDisableStatusCodes: settings.AutomaticDisableStatusCodes,
AutomaticRetryStatusCodes: settings.AutomaticRetryStatusCodes,
'monitor_setting.auto_test_channel_enabled':
settings['monitor_setting.auto_test_channel_enabled'],
'monitor_setting.auto_test_channel_minutes':
settings['monitor_setting.auto_test_channel_minutes'],
}}
/>
),
},
{
id: 'email',
titleKey: 'SMTP Email',
descriptionKey: 'Configure SMTP email settings',
build: (settings: OperationsSettings) => (
<EmailSettingsSection
defaultValues={{
SMTPServer: settings.SMTPServer,
SMTPPort: settings.SMTPPort,
SMTPAccount: settings.SMTPAccount,
SMTPFrom: settings.SMTPFrom,
SMTPToken: settings.SMTPToken,
SMTPSSLEnabled: settings.SMTPSSLEnabled,
SMTPForceAuthLogin: settings.SMTPForceAuthLogin,
}}
/>
),
},
{
id: 'worker',
titleKey: 'Worker Proxy',
descriptionKey: 'Configure worker service settings',
build: (settings: OperationsSettings) => (
<WorkerSettingsSection
defaultValues={{
WorkerUrl: settings.WorkerUrl,
WorkerValidKey: settings.WorkerValidKey,
WorkerAllowHttpImageRequestEnabled:
settings.WorkerAllowHttpImageRequestEnabled,
}}
/>
),
},
{
id: 'logs',
titleKey: 'Log Maintenance',
descriptionKey: 'Configure log consumption settings',
build: (settings: OperationsSettings) => (
<LogSettingsSection
defaultEnabled={Boolean(settings.LogConsumeEnabled)}
/>
),
},
{
id: 'performance',
titleKey: 'Performance',
descriptionKey: 'Disk cache, system monitoring and performance stats',
build: (settings: OperationsSettings) => (
<PerformanceSection
defaultValues={{
'performance_setting.disk_cache_enabled':
settings['performance_setting.disk_cache_enabled'] ?? false,
'performance_setting.disk_cache_threshold_mb':
settings['performance_setting.disk_cache_threshold_mb'] ?? 10,
'performance_setting.disk_cache_max_size_mb':
settings['performance_setting.disk_cache_max_size_mb'] ?? 1024,
'performance_setting.disk_cache_path':
settings['performance_setting.disk_cache_path'] ?? '',
'performance_setting.monitor_enabled':
settings['performance_setting.monitor_enabled'] ?? false,
'performance_setting.monitor_cpu_threshold':
settings['performance_setting.monitor_cpu_threshold'] ?? 90,
'performance_setting.monitor_memory_threshold':
settings['performance_setting.monitor_memory_threshold'] ?? 90,
'performance_setting.monitor_disk_threshold':
settings['performance_setting.monitor_disk_threshold'] ?? 95,
'perf_metrics_setting.enabled':
settings['perf_metrics_setting.enabled'] ?? true,
'perf_metrics_setting.flush_interval':
settings['perf_metrics_setting.flush_interval'] ?? 5,
'perf_metrics_setting.bucket_time':
settings['perf_metrics_setting.bucket_time'] ?? 'hour',
'perf_metrics_setting.retention_days':
settings['perf_metrics_setting.retention_days'] ?? 0,
}}
/>
),
},
{
id: 'update-checker',
titleKey: 'System maintenance',
descriptionKey: 'Check for system updates',
build: (
_settings: OperationsSettings,
currentVersion?: string | null,
startTime?: number | null
) => (
<UpdateCheckerSection
currentVersion={currentVersion}
startTime={startTime}
/>
),
},
] as const
export type OperationsSectionId = (typeof OPERATIONS_SECTIONS)[number]['id']
const operationsRegistry = createSectionRegistry<
OperationsSectionId,
OperationsSettings,
[string | null | undefined, number | null | undefined]
>({
sections: OPERATIONS_SECTIONS,
defaultSection: 'behavior',
basePath: '/system-settings/operations',
urlStyle: 'path',
})
export const OPERATIONS_SECTION_IDS = operationsRegistry.sectionIds
export const OPERATIONS_DEFAULT_SECTION = operationsRegistry.defaultSection
export const getOperationsSectionNavItems =
operationsRegistry.getSectionNavItems
export const getOperationsSectionContent =
operationsRegistry.getSectionContent
import { SettingsPage } from '../components/settings-page' import { SettingsPage } from '../components/settings-page'
import type { RequestLimitsSettings } from '../types' import type { SecuritySettings } from '../types'
import { import {
REQUEST_LIMITS_DEFAULT_SECTION, SECURITY_DEFAULT_SECTION,
getRequestLimitsSectionContent, getSecuritySectionContent,
} from './section-registry.tsx' } from './section-registry.tsx'
const defaultRequestLimitsSettings: RequestLimitsSettings = { const defaultSecuritySettings: SecuritySettings = {
ModelRequestRateLimitEnabled: false, ModelRequestRateLimitEnabled: false,
ModelRequestRateLimitCount: 0, ModelRequestRateLimitCount: 0,
ModelRequestRateLimitSuccessCount: 1000, ModelRequestRateLimitSuccessCount: 1000,
...@@ -24,13 +24,13 @@ const defaultRequestLimitsSettings: RequestLimitsSettings = { ...@@ -24,13 +24,13 @@ const defaultRequestLimitsSettings: RequestLimitsSettings = {
'fetch_setting.apply_ip_filter_for_domain': false, 'fetch_setting.apply_ip_filter_for_domain': false,
} }
export function RequestLimitsSettings() { export function SecuritySettings() {
return ( return (
<SettingsPage <SettingsPage
routePath='/_authenticated/system-settings/request-limits/$section' routePath='/_authenticated/system-settings/security/$section'
defaultSettings={defaultRequestLimitsSettings} defaultSettings={defaultSecuritySettings}
defaultSection={REQUEST_LIMITS_DEFAULT_SECTION} defaultSection={SECURITY_DEFAULT_SECTION}
getSectionContent={getRequestLimitsSectionContent} getSectionContent={getSecuritySectionContent}
/> />
) )
} }
import type { RequestLimitsSettings } from '../types' import type { SecuritySettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry' import { createSectionRegistry } from '../utils/section-registry'
import { RateLimitSection } from './rate-limit-section' import { RateLimitSection } from '../request-limits/rate-limit-section'
import { SensitiveWordsSection } from './sensitive-words-section' import { SensitiveWordsSection } from '../request-limits/sensitive-words-section'
import { SSRFSection } from './ssrf-section' import { SSRFSection } from '../request-limits/ssrf-section'
const REQUEST_LIMITS_SECTIONS = [ const SECURITY_SECTIONS = [
{ {
id: 'rate-limit', id: 'rate-limit',
titleKey: 'Rate Limiting', titleKey: 'Rate Limiting',
descriptionKey: 'Configure model request rate limiting', descriptionKey: 'Configure model request rate limiting',
build: (settings: RequestLimitsSettings) => ( build: (settings: SecuritySettings) => (
<RateLimitSection <RateLimitSection
defaultValues={{ defaultValues={{
ModelRequestRateLimitEnabled: settings.ModelRequestRateLimitEnabled, ModelRequestRateLimitEnabled: settings.ModelRequestRateLimitEnabled,
...@@ -27,7 +27,7 @@ const REQUEST_LIMITS_SECTIONS = [ ...@@ -27,7 +27,7 @@ const REQUEST_LIMITS_SECTIONS = [
id: 'sensitive-words', id: 'sensitive-words',
titleKey: 'Sensitive Words', titleKey: 'Sensitive Words',
descriptionKey: 'Configure sensitive word filtering', descriptionKey: 'Configure sensitive word filtering',
build: (settings: RequestLimitsSettings) => ( build: (settings: SecuritySettings) => (
<SensitiveWordsSection <SensitiveWordsSection
defaultValues={{ defaultValues={{
CheckSensitiveEnabled: settings.CheckSensitiveEnabled, CheckSensitiveEnabled: settings.CheckSensitiveEnabled,
...@@ -41,7 +41,7 @@ const REQUEST_LIMITS_SECTIONS = [ ...@@ -41,7 +41,7 @@ const REQUEST_LIMITS_SECTIONS = [
id: 'ssrf', id: 'ssrf',
titleKey: 'SSRF Protection', titleKey: 'SSRF Protection',
descriptionKey: 'Configure SSRF (Server-Side Request Forgery) protection', descriptionKey: 'Configure SSRF (Server-Side Request Forgery) protection',
build: (settings: RequestLimitsSettings) => ( build: (settings: SecuritySettings) => (
<SSRFSection <SSRFSection
defaultValues={{ defaultValues={{
'fetch_setting.enable_ssrf_protection': 'fetch_setting.enable_ssrf_protection':
...@@ -64,23 +64,19 @@ const REQUEST_LIMITS_SECTIONS = [ ...@@ -64,23 +64,19 @@ const REQUEST_LIMITS_SECTIONS = [
}, },
] as const ] as const
export type RequestLimitsSectionId = export type SecuritySectionId = (typeof SECURITY_SECTIONS)[number]['id']
(typeof REQUEST_LIMITS_SECTIONS)[number]['id']
const requestLimitsRegistry = createSectionRegistry< const securityRegistry = createSectionRegistry<
RequestLimitsSectionId, SecuritySectionId,
RequestLimitsSettings SecuritySettings
>({ >({
sections: REQUEST_LIMITS_SECTIONS, sections: SECURITY_SECTIONS,
defaultSection: 'rate-limit', defaultSection: 'rate-limit',
basePath: '/system-settings/request-limits', basePath: '/system-settings/security',
urlStyle: 'path', urlStyle: 'path',
}) })
export const REQUEST_LIMITS_SECTION_IDS = requestLimitsRegistry.sectionIds export const SECURITY_SECTION_IDS = securityRegistry.sectionIds
export const REQUEST_LIMITS_DEFAULT_SECTION = export const SECURITY_DEFAULT_SECTION = securityRegistry.defaultSection
requestLimitsRegistry.defaultSection export const getSecuritySectionNavItems = securityRegistry.getSectionNavItems
export const getRequestLimitsSectionNavItems = export const getSecuritySectionContent = securityRegistry.getSectionContent
requestLimitsRegistry.getSectionNavItems
export const getRequestLimitsSectionContent =
requestLimitsRegistry.getSectionContent
import { SettingsPage } from '../components/settings-page'
import type { SiteSettings } from '../types'
import {
SITE_DEFAULT_SECTION,
getSiteSectionContent,
} from './section-registry.tsx'
const defaultSiteSettings: SiteSettings = {
'theme.frontend': 'default',
Notice: '',
SystemName: 'New API',
Logo: '',
Footer: '',
About: '',
HomePageContent: '',
ServerAddress: '',
'legal.user_agreement': '',
'legal.privacy_policy': '',
HeaderNavModules: '',
SidebarModulesAdmin: '',
}
export function SiteSettings() {
return (
<SettingsPage
routePath='/_authenticated/system-settings/site/$section'
defaultSettings={defaultSiteSettings}
defaultSection={SITE_DEFAULT_SECTION}
getSectionContent={getSiteSectionContent}
/>
)
}
import type { SiteSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import {
parseHeaderNavModules,
parseSidebarModulesAdmin,
serializeHeaderNavModules,
serializeSidebarModulesAdmin,
} from '../maintenance/config'
import { HeaderNavigationSection } from '../maintenance/header-navigation-section'
import { NoticeSection } from '../maintenance/notice-section'
import { SidebarModulesSection } from '../maintenance/sidebar-modules-section'
import { SystemInfoSection } from '../general/system-info-section'
const SITE_SECTIONS = [
{
id: 'system-info',
titleKey: 'System Information',
descriptionKey: 'Configure basic system information and branding',
build: (settings: SiteSettings) => (
<SystemInfoSection
defaultValues={{
theme: {
frontend: settings['theme.frontend'] as 'default' | 'classic',
},
SystemName: settings.SystemName,
Logo: settings.Logo,
Footer: settings.Footer,
About: settings.About,
HomePageContent: settings.HomePageContent,
ServerAddress: settings.ServerAddress,
legal: {
user_agreement: settings['legal.user_agreement'],
privacy_policy: settings['legal.privacy_policy'],
},
}}
/>
),
},
{
id: 'notice',
titleKey: 'System Notice',
descriptionKey: 'Configure system maintenance notice',
build: (settings: SiteSettings) => (
<NoticeSection defaultValue={settings.Notice ?? ''} />
),
},
{
id: 'header-navigation',
titleKey: 'Header navigation',
descriptionKey: 'Configure header navigation modules',
build: (settings: SiteSettings) => {
const headerNavConfig = parseHeaderNavModules(settings.HeaderNavModules)
const headerNavSerialized = serializeHeaderNavModules(headerNavConfig)
return (
<HeaderNavigationSection
config={headerNavConfig}
initialSerialized={headerNavSerialized}
/>
)
},
},
{
id: 'sidebar-modules',
titleKey: 'Sidebar modules',
descriptionKey: 'Configure sidebar modules for admin',
build: (settings: SiteSettings) => {
const sidebarConfig = parseSidebarModulesAdmin(
settings.SidebarModulesAdmin
)
const sidebarSerialized = serializeSidebarModulesAdmin(sidebarConfig)
return (
<SidebarModulesSection
config={sidebarConfig}
initialSerialized={sidebarSerialized}
/>
)
},
},
] as const
export type SiteSectionId = (typeof SITE_SECTIONS)[number]['id']
const siteRegistry = createSectionRegistry<SiteSectionId, SiteSettings>({
sections: SITE_SECTIONS,
defaultSection: 'system-info',
basePath: '/system-settings/site',
urlStyle: 'path',
})
export const SITE_SECTION_IDS = siteRegistry.sectionIds
export const SITE_DEFAULT_SECTION = siteRegistry.defaultSection
export const getSiteSectionNavItems = siteRegistry.getSectionNavItems
export const getSiteSectionContent = siteRegistry.getSectionContent
...@@ -27,7 +27,7 @@ export type DeleteLogsResponse = { ...@@ -27,7 +27,7 @@ export type DeleteLogsResponse = {
data?: number data?: number
} }
export type GeneralSettings = { export type SiteSettings = {
'theme.frontend': string 'theme.frontend': string
Notice: string Notice: string
SystemName: string SystemName: string
...@@ -38,32 +38,8 @@ export type GeneralSettings = { ...@@ -38,32 +38,8 @@ export type GeneralSettings = {
ServerAddress: string ServerAddress: string
'legal.user_agreement': string 'legal.user_agreement': string
'legal.privacy_policy': string 'legal.privacy_policy': string
QuotaForNewUser: number HeaderNavModules: string
PreConsumedQuota: number SidebarModulesAdmin: string
QuotaForInviter: number
QuotaForInvitee: number
TopUpLink: string
'general_setting.docs_link': string
'quota_setting.enable_free_model_pre_consume': boolean
QuotaPerUnit: number
USDExchangeRate: number
'general_setting.quota_display_type': string
'general_setting.custom_currency_symbol': string
'general_setting.custom_currency_exchange_rate': number
RetryTimes: number
DisplayInCurrencyEnabled: boolean
DisplayTokenStatEnabled: boolean
DefaultCollapseSidebar: boolean
DemoSiteEnabled: boolean
SelfUseModeEnabled: boolean
'checkin_setting.enabled': boolean
'checkin_setting.min_quota': number
'checkin_setting.max_quota': number
'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean
'channel_affinity_setting.max_entries': number
'channel_affinity_setting.default_ttl_seconds': number
'channel_affinity_setting.rules': string
} }
export type AuthSettings = { export type AuthSettings = {
...@@ -131,28 +107,87 @@ export type ContentSettings = { ...@@ -131,28 +107,87 @@ export type ContentSettings = {
MjActionCheckSuccessEnabled: boolean MjActionCheckSuccessEnabled: boolean
} }
export type IntegrationSettings = { export type ModelSettings = {
SMTPServer: string 'global.pass_through_request_enabled': boolean
SMTPPort: string 'global.thinking_model_blacklist': string
SMTPAccount: string 'global.chat_completions_to_responses_policy': string
SMTPFrom: string 'general_setting.ping_interval_enabled': boolean
SMTPToken: string 'general_setting.ping_interval_seconds': number
SMTPSSLEnabled: boolean 'gemini.safety_settings': string
SMTPForceAuthLogin: boolean 'gemini.version_settings': string
WorkerUrl: string 'gemini.supported_imagine_models': string
WorkerValidKey: string 'gemini.thinking_adapter_enabled': boolean
WorkerAllowHttpImageRequestEnabled: boolean 'gemini.thinking_adapter_budget_tokens_percentage': number
ChannelDisableThreshold: string 'gemini.function_call_thought_signature_enabled': boolean
QuotaRemindThreshold: string 'gemini.remove_function_response_id_enabled': boolean
AutomaticDisableChannelEnabled: boolean 'claude.model_headers_settings': string
AutomaticEnableChannelEnabled: boolean 'claude.default_max_tokens': string
AutomaticDisableKeywords: string 'claude.thinking_adapter_enabled': boolean
AutomaticDisableStatusCodes: string 'claude.thinking_adapter_budget_tokens_percentage': number
AutomaticRetryStatusCodes: string 'grok.violation_deduction_enabled': boolean
'monitor_setting.auto_test_channel_enabled': boolean 'grok.violation_deduction_amount': number
'monitor_setting.auto_test_channel_minutes': number ModelPrice: string
ModelRatio: string
CacheRatio: string
CreateCacheRatio: string
CompletionRatio: string
ImageRatio: string
AudioRatio: string
AudioCompletionRatio: string
ExposeRatioEnabled: boolean
'billing_setting.billing_mode': string
'billing_setting.billing_expr': string
'tool_price_setting.prices': string
TopupGroupRatio: string
GroupRatio: string
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean
'channel_affinity_setting.max_entries': number
'channel_affinity_setting.default_ttl_seconds': number
'channel_affinity_setting.rules': string
'model_deployment.ionet.api_key': string 'model_deployment.ionet.api_key': string
'model_deployment.ionet.enabled': boolean 'model_deployment.ionet.enabled': boolean
}
export type BillingSettings = {
QuotaForNewUser: number
PreConsumedQuota: number
QuotaForInviter: number
QuotaForInvitee: number
TopUpLink: string
'general_setting.docs_link': string
'quota_setting.enable_free_model_pre_consume': boolean
QuotaPerUnit: number
USDExchangeRate: number
'general_setting.quota_display_type': string
'general_setting.custom_currency_symbol': string
'general_setting.custom_currency_exchange_rate': number
DisplayInCurrencyEnabled: boolean
DisplayTokenStatEnabled: boolean
ModelPrice: string
ModelRatio: string
CacheRatio: string
CreateCacheRatio: string
CompletionRatio: string
ImageRatio: string
AudioRatio: string
AudioCompletionRatio: string
ExposeRatioEnabled: boolean
'billing_setting.billing_mode': string
'billing_setting.billing_expr': string
'tool_price_setting.prices': string
TopupGroupRatio: string
GroupRatio: string
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
PayAddress: string PayAddress: string
EpayId: string EpayId: string
EpayKey: string EpayKey: string
...@@ -199,53 +234,36 @@ export type IntegrationSettings = { ...@@ -199,53 +234,36 @@ export type IntegrationSettings = {
WaffoPancakeCurrency: string WaffoPancakeCurrency: string
WaffoPancakeUnitPrice: number WaffoPancakeUnitPrice: number
WaffoPancakeMinTopUp: number WaffoPancakeMinTopUp: number
'checkin_setting.enabled': boolean
'checkin_setting.min_quota': number
'checkin_setting.max_quota': number
} }
export type ModelSettings = { export type OperationsSettings = {
'global.pass_through_request_enabled': boolean RetryTimes: number
'global.thinking_model_blacklist': string DefaultCollapseSidebar: boolean
'global.chat_completions_to_responses_policy': string DemoSiteEnabled: boolean
'general_setting.ping_interval_enabled': boolean SelfUseModeEnabled: boolean
'general_setting.ping_interval_seconds': number ChannelDisableThreshold: string
'gemini.safety_settings': string QuotaRemindThreshold: string
'gemini.version_settings': string AutomaticDisableChannelEnabled: boolean
'gemini.supported_imagine_models': string AutomaticEnableChannelEnabled: boolean
'gemini.thinking_adapter_enabled': boolean AutomaticDisableKeywords: string
'gemini.thinking_adapter_budget_tokens_percentage': number AutomaticDisableStatusCodes: string
'gemini.function_call_thought_signature_enabled': boolean AutomaticRetryStatusCodes: string
'gemini.remove_function_response_id_enabled': boolean 'monitor_setting.auto_test_channel_enabled': boolean
'claude.model_headers_settings': string 'monitor_setting.auto_test_channel_minutes': number
'claude.default_max_tokens': string SMTPServer: string
'claude.thinking_adapter_enabled': boolean SMTPPort: string
'claude.thinking_adapter_budget_tokens_percentage': number SMTPAccount: string
'grok.violation_deduction_enabled': boolean SMTPFrom: string
'grok.violation_deduction_amount': number SMTPToken: string
ModelPrice: string SMTPSSLEnabled: boolean
ModelRatio: string SMTPForceAuthLogin: boolean
CacheRatio: string WorkerUrl: string
CreateCacheRatio: string WorkerValidKey: string
CompletionRatio: string WorkerAllowHttpImageRequestEnabled: boolean
ImageRatio: string
AudioRatio: string
AudioCompletionRatio: string
ExposeRatioEnabled: boolean
'billing_setting.billing_mode': string
'billing_setting.billing_expr': string
'tool_price_setting.prices': string
TopupGroupRatio: string
GroupRatio: string
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
}
export type MaintenanceSettings = {
Notice: string
LogConsumeEnabled: boolean LogConsumeEnabled: boolean
HeaderNavModules: string
SidebarModulesAdmin: string
'performance_setting.disk_cache_enabled': boolean 'performance_setting.disk_cache_enabled': boolean
'performance_setting.disk_cache_threshold_mb': number 'performance_setting.disk_cache_threshold_mb': number
'performance_setting.disk_cache_max_size_mb': number 'performance_setting.disk_cache_max_size_mb': number
...@@ -260,7 +278,7 @@ export type MaintenanceSettings = { ...@@ -260,7 +278,7 @@ export type MaintenanceSettings = {
'perf_metrics_setting.retention_days': number 'perf_metrics_setting.retention_days': number
} }
export type RequestLimitsSettings = { export type SecuritySettings = {
ModelRequestRateLimitEnabled: boolean ModelRequestRateLimitEnabled: boolean
ModelRequestRateLimitCount: number ModelRequestRateLimitCount: number
ModelRequestRateLimitSuccessCount: number ModelRequestRateLimitSuccessCount: number
......
...@@ -29,7 +29,7 @@ export type SettingsRouteConfigOptions< ...@@ -29,7 +29,7 @@ export type SettingsRouteConfigOptions<
defaultSection: TSectionId defaultSection: TSectionId
/** Settings component to render */ /** Settings component to render */
component: TComponent component: TComponent
/** Route path for redirect (e.g., '/system-settings/general') */ /** Route path for redirect (e.g., '/system-settings/site') */
routePath: string routePath: string
/** Whether to redirect to default section if no section is provided (default: false) */ /** Whether to redirect to default section if no section is provided (default: false) */
redirectToDefault?: boolean redirectToDefault?: boolean
...@@ -44,12 +44,12 @@ export type SettingsRouteConfigOptions< ...@@ -44,12 +44,12 @@ export type SettingsRouteConfigOptions<
* *
* @example * @example
* ```tsx * ```tsx
* export const Route = createFileRoute('/_authenticated/system-settings/general')( * export const Route = createFileRoute('/_authenticated/system-settings/site')(
* createSettingsRouteConfig({ * createSettingsRouteConfig({
* sectionIds: GENERAL_SECTION_IDS, * sectionIds: SITE_SECTION_IDS,
* defaultSection: GENERAL_DEFAULT_SECTION, * defaultSection: SITE_DEFAULT_SECTION,
* component: GeneralSettings, * component: SiteSettings,
* routePath: '/system-settings/general', * routePath: '/system-settings/site',
* redirectToDefault: true, * redirectToDefault: true,
* }) * })
* ) * )
......
...@@ -96,7 +96,7 @@ const URL_TO_CONFIG_MAP: Record<string, { section: string; module: string }> = { ...@@ -96,7 +96,7 @@ const URL_TO_CONFIG_MAP: Record<string, { section: string; module: string }> = {
'/redemption-codes': { section: 'admin', module: 'redemption' }, '/redemption-codes': { section: 'admin', module: 'redemption' },
'/subscriptions': { section: 'admin', module: 'subscription' }, '/subscriptions': { section: 'admin', module: 'subscription' },
'/system-settings': { section: 'admin', module: 'setting' }, '/system-settings': { section: 'admin', module: 'setting' },
'/system-settings/general': { section: 'admin', module: 'setting' }, '/system-settings/site': { section: 'admin', module: 'setting' },
} }
/** /**
......
...@@ -129,7 +129,7 @@ export function useSidebarData(): SidebarData { ...@@ -129,7 +129,7 @@ export function useSidebarData(): SidebarData {
}, },
{ {
title: t('System Settings'), title: t('System Settings'),
url: '/system-settings/general', url: '/system-settings/site',
activeUrls: ['/system-settings'], activeUrls: ['/system-settings'],
icon: Settings, icon: Settings,
}, },
......
...@@ -11,25 +11,25 @@ ...@@ -11,25 +11,25 @@
"file": "fr.json", "file": "fr.json",
"missingCount": 0, "missingCount": 0,
"extrasCount": 0, "extrasCount": 0,
"untranslatedCount": 0 "untranslatedCount": 1
}, },
"ja": { "ja": {
"file": "ja.json", "file": "ja.json",
"missingCount": 0, "missingCount": 0,
"extrasCount": 0, "extrasCount": 0,
"untranslatedCount": 88 "untranslatedCount": 90
}, },
"ru": { "ru": {
"file": "ru.json", "file": "ru.json",
"missingCount": 0, "missingCount": 0,
"extrasCount": 0, "extrasCount": 0,
"untranslatedCount": 92 "untranslatedCount": 105
}, },
"vi": { "vi": {
"file": "vi.json", "file": "vi.json",
"missingCount": 0, "missingCount": 0,
"extrasCount": 0, "extrasCount": 0,
"untranslatedCount": 0 "untranslatedCount": 3
}, },
"zh": { "zh": {
"file": "zh.json", "file": "zh.json",
......
{ {
"apps using the most tokens through new-api": "Apps using the most tokens through new-api", "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback."
"apps using the most tokens through new-api.": "Apps using the most tokens through new-api."
} }
...@@ -77,6 +77,8 @@ ...@@ -77,6 +77,8 @@
"Submodel": "Submodel", "Submodel": "Submodel",
"SunoAPI": "SunoAPI", "SunoAPI": "SunoAPI",
"Telegram": "Telegram", "Telegram": "Telegram",
"Token prices": "Token prices",
"Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.",
"TTFT P50": "TTFT P50", "TTFT P50": "TTFT P50",
"TTFT P95": "TTFT P95", "TTFT P95": "TTFT P95",
"TTFT P99": "TTFT P99", "TTFT P99": "TTFT P99",
......
...@@ -81,6 +81,8 @@ ...@@ -81,6 +81,8 @@
"SunoAPI": "SunoAPI", "SunoAPI": "SunoAPI",
"Telegram": "Telegram", "Telegram": "Telegram",
"Tencent": "Tencent", "Tencent": "Tencent",
"Token prices": "Token prices",
"Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.",
"TTFT P50": "TTFT P50", "TTFT P50": "TTFT P50",
"TTFT P95": "TTFT P95", "TTFT P95": "TTFT P95",
"TTFT P99": "TTFT P99", "TTFT P99": "TTFT P99",
...@@ -90,5 +92,16 @@ ...@@ -90,5 +92,16 @@
"whsec_xxx": "whsec_xxx", "whsec_xxx": "whsec_xxx",
"Xinference": "Xinference", "Xinference": "Xinference",
"Xunfei": "Xunfei", "Xunfei": "Xunfei",
"Zhipu V4": "Zhipu V4" "Zhipu V4": "Zhipu V4",
"Cache pricing": "Cache pricing",
"Core pricing": "Core pricing",
"All conditions must match before this tier is used.": "All conditions must match before this tier is used.",
"Base input and output token prices for this tier.": "Base input and output token prices for this tier.",
"Fallback tier": "Fallback tier",
"Media pricing": "Media pricing",
"No separate media pricing configured.": "No separate media pricing configured.",
"Separate image/audio prices are enabled.": "Separate image/audio prices are enabled.",
"Set separate prices for cache reads and writes.": "Set separate prices for cache reads and writes.",
"This tier catches any request that did not match earlier tiers.": "This tier catches any request that did not match earlier tiers.",
"Tier conditions": "Tier conditions"
} }
{ {
"apps using the most tokens through new-api": "Apps using the most tokens through new-api", "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.",
"apps using the most tokens through new-api.": "Apps using the most tokens through new-api." "Base input and output token prices for this tier.": "Base input and output token prices for this tier.",
"Set separate prices for cache reads and writes.": "Set separate prices for cache reads and writes."
} }
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { GeneralSettings } from '@/features/system-settings/general' import { BillingSettings } from '@/features/system-settings/billing'
import { import {
GENERAL_DEFAULT_SECTION, BILLING_DEFAULT_SECTION,
GENERAL_SECTION_IDS, BILLING_SECTION_IDS,
} from '@/features/system-settings/general/section-registry.tsx' } from '@/features/system-settings/billing/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticated/system-settings/general/$section' '/_authenticated/system-settings/billing/$section'
)({ )({
beforeLoad: ({ params }) => { beforeLoad: ({ params }) => {
const validSections = GENERAL_SECTION_IDS as unknown as string[] const validSections = BILLING_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) { if (!validSections.includes(params.section)) {
throw redirect({ throw redirect({
to: '/system-settings/general/$section', to: '/system-settings/billing/$section',
params: { section: GENERAL_DEFAULT_SECTION }, params: { section: BILLING_DEFAULT_SECTION },
}) })
} }
}, },
component: GeneralSettings, component: BillingSettings,
}) })
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { GENERAL_DEFAULT_SECTION } from '@/features/system-settings/general/section-registry.tsx' import { BILLING_DEFAULT_SECTION } from '@/features/system-settings/billing/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticated/system-settings/general/' '/_authenticated/system-settings/billing/'
)({ )({
beforeLoad: () => { beforeLoad: () => {
throw redirect({ throw redirect({
to: '/system-settings/general/$section', to: '/system-settings/billing/$section',
params: { section: GENERAL_DEFAULT_SECTION }, params: { section: BILLING_DEFAULT_SECTION },
}) })
}, },
}) })
...@@ -3,7 +3,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router' ...@@ -3,7 +3,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/system-settings/')({ export const Route = createFileRoute('/_authenticated/system-settings/')({
beforeLoad: () => { beforeLoad: () => {
throw redirect({ throw redirect({
to: '/system-settings/general', to: '/system-settings/site',
}) })
}, },
}) })
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { IntegrationSettings } from '@/features/system-settings/integrations' import { OperationsSettings } from '@/features/system-settings/operations'
import { import {
INTEGRATIONS_DEFAULT_SECTION, OPERATIONS_DEFAULT_SECTION,
INTEGRATIONS_SECTION_IDS, OPERATIONS_SECTION_IDS,
} from '@/features/system-settings/integrations/section-registry.tsx' } from '@/features/system-settings/operations/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticated/system-settings/integrations/$section' '/_authenticated/system-settings/operations/$section'
)({ )({
beforeLoad: ({ params }) => { beforeLoad: ({ params }) => {
const validSections = INTEGRATIONS_SECTION_IDS as unknown as string[] const validSections = OPERATIONS_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) { if (!validSections.includes(params.section)) {
throw redirect({ throw redirect({
to: '/system-settings/integrations/$section', to: '/system-settings/operations/$section',
params: { section: INTEGRATIONS_DEFAULT_SECTION }, params: { section: OPERATIONS_DEFAULT_SECTION },
}) })
} }
}, },
component: IntegrationSettings, component: OperationsSettings,
}) })
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { INTEGRATIONS_DEFAULT_SECTION } from '@/features/system-settings/integrations/section-registry.tsx' import { OPERATIONS_DEFAULT_SECTION } from '@/features/system-settings/operations/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticated/system-settings/integrations/' '/_authenticated/system-settings/operations/'
)({ )({
beforeLoad: () => { beforeLoad: () => {
throw redirect({ throw redirect({
to: '/system-settings/integrations/$section', to: '/system-settings/operations/$section',
params: { section: INTEGRATIONS_DEFAULT_SECTION }, params: { section: OPERATIONS_DEFAULT_SECTION },
}) })
}, },
}) })
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { MaintenanceSettings } from '@/features/system-settings/maintenance' import { SecuritySettings } from '@/features/system-settings/security'
import { import {
MAINTENANCE_DEFAULT_SECTION, SECURITY_DEFAULT_SECTION,
MAINTENANCE_SECTION_IDS, SECURITY_SECTION_IDS,
} from '@/features/system-settings/maintenance/section-registry.tsx' } from '@/features/system-settings/security/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticated/system-settings/maintenance/$section' '/_authenticated/system-settings/security/$section'
)({ )({
beforeLoad: ({ params }) => { beforeLoad: ({ params }) => {
const validSections = MAINTENANCE_SECTION_IDS as unknown as string[] const validSections = SECURITY_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) { if (!validSections.includes(params.section)) {
throw redirect({ throw redirect({
to: '/system-settings/maintenance/$section', to: '/system-settings/security/$section',
params: { section: MAINTENANCE_DEFAULT_SECTION }, params: { section: SECURITY_DEFAULT_SECTION },
}) })
} }
}, },
component: MaintenanceSettings, component: SecuritySettings,
}) })
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { MAINTENANCE_DEFAULT_SECTION } from '@/features/system-settings/maintenance/section-registry.tsx' import { SECURITY_DEFAULT_SECTION } from '@/features/system-settings/security/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticated/system-settings/maintenance/' '/_authenticated/system-settings/security/'
)({ )({
beforeLoad: () => { beforeLoad: () => {
throw redirect({ throw redirect({
to: '/system-settings/maintenance/$section', to: '/system-settings/security/$section',
params: { section: MAINTENANCE_DEFAULT_SECTION }, params: { section: SECURITY_DEFAULT_SECTION },
}) })
}, },
}) })
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { RequestLimitsSettings } from '@/features/system-settings/request-limits' import { SiteSettings } from '@/features/system-settings/site'
import { import {
REQUEST_LIMITS_DEFAULT_SECTION, SITE_DEFAULT_SECTION,
REQUEST_LIMITS_SECTION_IDS, SITE_SECTION_IDS,
} from '@/features/system-settings/request-limits/section-registry.tsx' } from '@/features/system-settings/site/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticated/system-settings/request-limits/$section' '/_authenticated/system-settings/site/$section'
)({ )({
beforeLoad: ({ params }) => { beforeLoad: ({ params }) => {
const validSections = REQUEST_LIMITS_SECTION_IDS as unknown as string[] const validSections = SITE_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) { if (!validSections.includes(params.section)) {
throw redirect({ throw redirect({
to: '/system-settings/request-limits/$section', to: '/system-settings/site/$section',
params: { section: REQUEST_LIMITS_DEFAULT_SECTION }, params: { section: SITE_DEFAULT_SECTION },
}) })
} }
}, },
component: RequestLimitsSettings, component: SiteSettings,
}) })
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { REQUEST_LIMITS_DEFAULT_SECTION } from '@/features/system-settings/request-limits/section-registry.tsx' import { SITE_DEFAULT_SECTION } from '@/features/system-settings/site/section-registry.tsx'
export const Route = createFileRoute( export const Route = createFileRoute('/_authenticated/system-settings/site/')({
'/_authenticated/system-settings/request-limits/'
)({
beforeLoad: () => { beforeLoad: () => {
throw redirect({ throw redirect({
to: '/system-settings/request-limits/$section', to: '/system-settings/site/$section',
params: { section: REQUEST_LIMITS_DEFAULT_SECTION }, params: { section: SITE_DEFAULT_SECTION },
}) })
}, },
}) })
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment