mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-06 17:46:23 +00:00
feat: add routing reliability management
This commit is contained in:
@@ -85,6 +85,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag
|
|||||||
- Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches.
|
- Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches.
|
||||||
- Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback.
|
- Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback.
|
||||||
- Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns).
|
- Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns).
|
||||||
|
- Avoid GORM boolean default tags such as `gorm:"default:true"` when the default is a business rule already enforced by code. MySQL and PostgreSQL can normalize boolean defaults differently, causing GORM `AutoMigrate` to repeatedly issue `ALTER TABLE` on restart. Prefer setting these defaults in request/model normalization, hooks, constructors, or service logic; do not replace `default:true` with `default:1` unless the behavior is verified across SQLite, MySQL, and PostgreSQL.
|
||||||
|
|
||||||
**Relay and provider behavior:**
|
**Relay and provider behavior:**
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag
|
|||||||
- Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches.
|
- Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches.
|
||||||
- Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback.
|
- Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback.
|
||||||
- Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns).
|
- Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns).
|
||||||
|
- Avoid GORM boolean default tags such as `gorm:"default:true"` when the default is a business rule already enforced by code. MySQL and PostgreSQL can normalize boolean defaults differently, causing GORM `AutoMigrate` to repeatedly issue `ALTER TABLE` on restart. Prefer setting these defaults in request/model normalization, hooks, constructors, or service logic; do not replace `default:true` with `default:1` unless the behavior is verified across SQLite, MySQL, and PostgreSQL.
|
||||||
|
|
||||||
**Relay and provider behavior:**
|
**Relay and provider behavior:**
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ func buildChannelListQuery(group string, statusFilter int, typeFilter int) *gorm
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetChannelOps(c *gin.Context) {
|
||||||
|
common.ApiSuccess(c, gin.H{
|
||||||
|
"retry_times": common.RetryTimes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func GetAllChannels(c *gin.Context) {
|
func GetAllChannels(c *gin.Context) {
|
||||||
pageInfo := common.GetPageQuery(c)
|
pageInfo := common.GetPageQuery(c)
|
||||||
channelData := make([]*model.Channel, 0)
|
channelData := make([]*model.Channel, 0)
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.GET("/search", controller.SearchChannels)
|
channelRoute.GET("/search", controller.SearchChannels)
|
||||||
channelRoute.GET("/models", controller.ChannelListModels)
|
channelRoute.GET("/models", controller.ChannelListModels)
|
||||||
channelRoute.GET("/models_enabled", controller.EnabledListModels)
|
channelRoute.GET("/models_enabled", controller.EnabledListModels)
|
||||||
|
channelRoute.GET("/ops", controller.GetChannelOps)
|
||||||
channelRoute.GET("/:id", controller.GetChannel)
|
channelRoute.GET("/:id", controller.GetChannel)
|
||||||
channelRoute.POST("/:id/key", middleware.RootAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.SecureVerificationRequired(), controller.GetChannelKey)
|
channelRoute.POST("/:id/key", middleware.RootAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.SecureVerificationRequired(), controller.GetChannelKey)
|
||||||
channelRoute.GET("/test", controller.TestAllChannels)
|
channelRoute.GET("/test", controller.TestAllChannels)
|
||||||
|
|||||||
+9
@@ -24,6 +24,7 @@ import type {
|
|||||||
BatchSetTagParams,
|
BatchSetTagParams,
|
||||||
Channel,
|
Channel,
|
||||||
ChannelBalanceResponse,
|
ChannelBalanceResponse,
|
||||||
|
ChannelOpsResponse,
|
||||||
ChannelTestResponse,
|
ChannelTestResponse,
|
||||||
CopyChannelParams,
|
CopyChannelParams,
|
||||||
CopyChannelResponse,
|
CopyChannelResponse,
|
||||||
@@ -103,6 +104,14 @@ export async function getChannel(id: number): Promise<GetChannelResponse> {
|
|||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get channel operations summary for administrators
|
||||||
|
*/
|
||||||
|
export async function getChannelOps(): Promise<ChannelOpsResponse> {
|
||||||
|
const res = await api.get('/api/channel/ops', channelActionConfig())
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new channel(s)
|
* Create new channel(s)
|
||||||
* Supports single, batch, and multi-key modes
|
* Supports single, batch, and multi-key modes
|
||||||
|
|||||||
+65
-1
@@ -17,7 +17,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { Settings2 } from 'lucide-react'
|
||||||
import { SectionPageLayout } from '@/components/layout'
|
import { SectionPageLayout } from '@/components/layout'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
import { ROLE } from '@/lib/roles'
|
||||||
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
|
import { getChannelOps } from './api'
|
||||||
import { ChannelsDialogs } from './components/channels-dialogs'
|
import { ChannelsDialogs } from './components/channels-dialogs'
|
||||||
import { ChannelsPrimaryButtons } from './components/channels-primary-buttons'
|
import { ChannelsPrimaryButtons } from './components/channels-primary-buttons'
|
||||||
import { ChannelsProvider } from './components/channels-provider'
|
import { ChannelsProvider } from './components/channels-provider'
|
||||||
@@ -25,10 +37,62 @@ import { ChannelsTable } from './components/channels-table'
|
|||||||
|
|
||||||
export function Channels() {
|
export function Channels() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const isRoot = useAuthStore(
|
||||||
|
(state) => state.auth.user?.role === ROLE.SUPER_ADMIN
|
||||||
|
)
|
||||||
|
const channelOpsQuery = useQuery({
|
||||||
|
queryKey: ['channel-ops'],
|
||||||
|
queryFn: getChannelOps,
|
||||||
|
retry: false,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
})
|
||||||
|
const retryTimes = channelOpsQuery.data?.data?.retry_times
|
||||||
|
const retryLabel =
|
||||||
|
typeof retryTimes === 'number'
|
||||||
|
? `${t('Max Retries')}: ${retryTimes}`
|
||||||
|
: null
|
||||||
|
let retryBadge = null
|
||||||
|
if (retryLabel) {
|
||||||
|
retryBadge = isRoot ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<Badge
|
||||||
|
variant='outline'
|
||||||
|
className='shrink-0 cursor-pointer'
|
||||||
|
aria-label={t('Retry Settings')}
|
||||||
|
render={
|
||||||
|
<Link
|
||||||
|
to='/system-settings/models/$section'
|
||||||
|
params={{ section: 'routing-reliability' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{retryLabel}</span>
|
||||||
|
<Settings2 data-icon='inline-end' />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('Retry Settings')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Badge variant='outline' className='shrink-0'>
|
||||||
|
{retryLabel}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChannelsProvider>
|
<ChannelsProvider>
|
||||||
<SectionPageLayout fixedContent>
|
<SectionPageLayout fixedContent>
|
||||||
<SectionPageLayout.Title>{t('Channels')}</SectionPageLayout.Title>
|
<SectionPageLayout.Title>
|
||||||
|
<span className='flex min-w-0 items-center gap-2'>
|
||||||
|
<span className='truncate'>{t('Channels')}</span>
|
||||||
|
{retryBadge}
|
||||||
|
</span>
|
||||||
|
</SectionPageLayout.Title>
|
||||||
<SectionPageLayout.Actions>
|
<SectionPageLayout.Actions>
|
||||||
<ChannelsPrimaryButtons />
|
<ChannelsPrimaryButtons />
|
||||||
</SectionPageLayout.Actions>
|
</SectionPageLayout.Actions>
|
||||||
|
|||||||
+8
@@ -167,6 +167,14 @@ export interface GetChannelResponse {
|
|||||||
data?: Channel
|
data?: Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChannelOpsResponse {
|
||||||
|
success: boolean
|
||||||
|
message?: string
|
||||||
|
data?: {
|
||||||
|
retry_times: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChannelTestResponse {
|
export interface ChannelTestResponse {
|
||||||
success: boolean
|
success: boolean
|
||||||
message?: string
|
message?: string
|
||||||
|
|||||||
@@ -200,6 +200,16 @@ 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,
|
||||||
|
RetryTimes: 0,
|
||||||
|
ChannelDisableThreshold: '',
|
||||||
|
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,
|
||||||
'channel_affinity_setting.enabled': false,
|
'channel_affinity_setting.enabled': false,
|
||||||
'channel_affinity_setting.switch_on_success': true,
|
'channel_affinity_setting.switch_on_success': true,
|
||||||
'channel_affinity_setting.keep_on_channel_disabled': false,
|
'channel_affinity_setting.keep_on_channel_disabled': false,
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ type SettingsSwitchFieldProps = SettingsSwitchRowProps & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const settingsSwitchRowClassName =
|
const settingsSwitchRowClassName =
|
||||||
'flex min-w-0 flex-row items-center justify-between gap-4 border-b py-2.5 last:border-b-0'
|
'flex min-w-0 flex-row items-center justify-between gap-4 py-2.5'
|
||||||
|
|
||||||
export function SettingsFormGrid(props: SettingsFormGridProps) {
|
export function SettingsFormGrid(props: SettingsFormGridProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ export function AnnouncementsSection({
|
|||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
onCheckedChange={handleToggleEnabled}
|
onCheckedChange={handleToggleEnabled}
|
||||||
label={t('Enabled')}
|
label={t('Enabled')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -529,8 +529,7 @@ export function AnnouncementsSection({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Type')}</FormLabel>
|
<FormLabel>{t('Type')}</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={typeOptions.map((option) => ({
|
||||||
...typeOptions.map((option) => ({
|
|
||||||
value: option.value,
|
value: option.value,
|
||||||
label: (
|
label: (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
@@ -540,8 +539,7 @@ export function AnnouncementsSection({
|
|||||||
{option.label}
|
{option.label}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
})),
|
}))}
|
||||||
]}
|
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
|
|||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
onCheckedChange={handleToggleEnabled}
|
onCheckedChange={handleToggleEnabled}
|
||||||
label={t('Enabled')}
|
label={t('Enabled')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -475,8 +475,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Badge Color')}</FormLabel>
|
<FormLabel>{t('Badge Color')}</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={colorOptions.map((option) => ({
|
||||||
...colorOptions.map((option) => ({
|
|
||||||
value: option.value,
|
value: option.value,
|
||||||
label: (
|
label: (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
@@ -486,8 +485,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
|
|||||||
{option.label}
|
{option.label}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
})),
|
}))}
|
||||||
]}
|
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
|
|||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
onCheckedChange={handleToggleEnabled}
|
onCheckedChange={handleToggleEnabled}
|
||||||
label={t('Enabled')}
|
label={t('Enabled')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
|
|||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
onCheckedChange={handleToggleEnabled}
|
onCheckedChange={handleToggleEnabled}
|
||||||
label={t('Enabled')}
|
label={t('Enabled')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -255,42 +255,42 @@ export function ChannelAffinitySection(props: Props) {
|
|||||||
const updates: { key: string; value: string }[] = []
|
const updates: { key: string; value: string }[] = []
|
||||||
|
|
||||||
if (enabled !== props.defaultValues['channel_affinity_setting.enabled'])
|
if (enabled !== props.defaultValues['channel_affinity_setting.enabled'])
|
||||||
updates.push({
|
{updates.push({
|
||||||
key: 'channel_affinity_setting.enabled',
|
key: 'channel_affinity_setting.enabled',
|
||||||
value: String(enabled),
|
value: String(enabled),
|
||||||
})
|
})}
|
||||||
if (
|
if (
|
||||||
switchOnSuccess !==
|
switchOnSuccess !==
|
||||||
props.defaultValues['channel_affinity_setting.switch_on_success']
|
props.defaultValues['channel_affinity_setting.switch_on_success']
|
||||||
)
|
)
|
||||||
updates.push({
|
{updates.push({
|
||||||
key: 'channel_affinity_setting.switch_on_success',
|
key: 'channel_affinity_setting.switch_on_success',
|
||||||
value: String(switchOnSuccess),
|
value: String(switchOnSuccess),
|
||||||
})
|
})}
|
||||||
if (
|
if (
|
||||||
keepOnChannelDisabled !==
|
keepOnChannelDisabled !==
|
||||||
props.defaultValues['channel_affinity_setting.keep_on_channel_disabled']
|
props.defaultValues['channel_affinity_setting.keep_on_channel_disabled']
|
||||||
)
|
)
|
||||||
updates.push({
|
{updates.push({
|
||||||
key: 'channel_affinity_setting.keep_on_channel_disabled',
|
key: 'channel_affinity_setting.keep_on_channel_disabled',
|
||||||
value: String(keepOnChannelDisabled),
|
value: String(keepOnChannelDisabled),
|
||||||
})
|
})}
|
||||||
if (
|
if (
|
||||||
maxEntries !==
|
maxEntries !==
|
||||||
props.defaultValues['channel_affinity_setting.max_entries']
|
props.defaultValues['channel_affinity_setting.max_entries']
|
||||||
)
|
)
|
||||||
updates.push({
|
{updates.push({
|
||||||
key: 'channel_affinity_setting.max_entries',
|
key: 'channel_affinity_setting.max_entries',
|
||||||
value: String(maxEntries),
|
value: String(maxEntries),
|
||||||
})
|
})}
|
||||||
if (
|
if (
|
||||||
defaultTtl !==
|
defaultTtl !==
|
||||||
props.defaultValues['channel_affinity_setting.default_ttl_seconds']
|
props.defaultValues['channel_affinity_setting.default_ttl_seconds']
|
||||||
)
|
)
|
||||||
updates.push({
|
{updates.push({
|
||||||
key: 'channel_affinity_setting.default_ttl_seconds',
|
key: 'channel_affinity_setting.default_ttl_seconds',
|
||||||
value: String(defaultTtl),
|
value: String(defaultTtl),
|
||||||
})
|
})}
|
||||||
|
|
||||||
const origRules = props.defaultValues['channel_affinity_setting.rules']
|
const origRules = props.defaultValues['channel_affinity_setting.rules']
|
||||||
const origSerialized = (() => {
|
const origSerialized = (() => {
|
||||||
@@ -411,7 +411,7 @@ export function ChannelAffinitySection(props: Props) {
|
|||||||
checked={enabled}
|
checked={enabled}
|
||||||
onCheckedChange={setEnabled}
|
onCheckedChange={setEnabled}
|
||||||
label={t('Enable')}
|
label={t('Enable')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
<div className='grid gap-1.5'>
|
<div className='grid gap-1.5'>
|
||||||
<Label>{t('Max Entries')}</Label>
|
<Label>{t('Max Entries')}</Label>
|
||||||
|
|||||||
+31
-17
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { Plus, Trash2 } from 'lucide-react'
|
import { Plus, Trash2 } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -44,6 +44,10 @@ import { SettingsSwitchField } from '../../components/settings-form-layout'
|
|||||||
import { RULE_TEMPLATES } from './constants'
|
import { RULE_TEMPLATES } from './constants'
|
||||||
import type { AffinityRule, KeySource } from './types'
|
import type { AffinityRule, KeySource } from './types'
|
||||||
|
|
||||||
|
type KeySourceRow = KeySource & {
|
||||||
|
rowId: string
|
||||||
|
}
|
||||||
|
|
||||||
const KEY_SOURCE_TYPES = [
|
const KEY_SOURCE_TYPES = [
|
||||||
'context_int',
|
'context_int',
|
||||||
'context_string',
|
'context_string',
|
||||||
@@ -103,8 +107,13 @@ interface Props {
|
|||||||
export function RuleEditorDialog(props: Props) {
|
export function RuleEditorDialog(props: Props) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isEdit = !!props.rule?.name
|
const isEdit = !!props.rule?.name
|
||||||
const [keySources, setKeySources] = useState<KeySource[]>([
|
const nextKeySourceRowId = useRef(0)
|
||||||
{ type: 'gjson', path: '' },
|
const createKeySourceRow = (source?: Partial<KeySource>): KeySourceRow => ({
|
||||||
|
...normalizeKeySource(source ?? { type: 'gjson', path: '' }),
|
||||||
|
rowId: String(nextKeySourceRowId.current++),
|
||||||
|
})
|
||||||
|
const [keySources, setKeySources] = useState<KeySourceRow[]>(() => [
|
||||||
|
createKeySourceRow(),
|
||||||
])
|
])
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||||
|
|
||||||
@@ -141,7 +150,11 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
: '',
|
: '',
|
||||||
})
|
})
|
||||||
const sources = (r.key_sources || []).map(normalizeKeySource)
|
const sources = (r.key_sources || []).map(normalizeKeySource)
|
||||||
setKeySources(sources.length > 0 ? sources : [{ type: 'gjson', path: '' }])
|
setKeySources(
|
||||||
|
sources.length > 0
|
||||||
|
? sources.map(createKeySourceRow)
|
||||||
|
: [createKeySourceRow()]
|
||||||
|
)
|
||||||
if (r.param_override_template) setAdvancedOpen(true)
|
if (r.param_override_template) setAdvancedOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +179,7 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
include_rule_name: true,
|
include_rule_name: true,
|
||||||
param_override_template_json: '',
|
param_override_template_json: '',
|
||||||
})
|
})
|
||||||
setKeySources([{ type: 'gjson', path: '' }])
|
setKeySources([createKeySourceRow()])
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.open, props.rule, props.templateKey])
|
}, [props.open, props.rule, props.templateKey])
|
||||||
@@ -297,7 +310,7 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setKeySources((prev) => [...prev, { type: 'gjson', path: '' }])
|
setKeySources((prev) => [...prev, createKeySourceRow()])
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Plus className='mr-1 h-3 w-3' />
|
<Plus className='mr-1 h-3 w-3' />
|
||||||
@@ -310,21 +323,22 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
{keySources.map((src, idx) => (
|
{keySources.map((src, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={src.rowId}
|
||||||
className='flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center'
|
className='flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center'
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t }))}
|
||||||
...KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t })),
|
|
||||||
]}
|
|
||||||
value={src.type}
|
value={src.type}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
if (v === null) return
|
if (v === null) return
|
||||||
const next = [...keySources]
|
const next = [...keySources]
|
||||||
next[idx] = normalizeKeySource({
|
next[idx] = {
|
||||||
...src,
|
...normalizeKeySource({
|
||||||
type: v as KeySource['type'],
|
...src,
|
||||||
})
|
type: v as KeySource['type'],
|
||||||
|
}),
|
||||||
|
rowId: src.rowId,
|
||||||
|
}
|
||||||
setKeySources(next)
|
setKeySources(next)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -432,19 +446,19 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
checked={form.watch('include_using_group')}
|
checked={form.watch('include_using_group')}
|
||||||
onCheckedChange={(v) => form.setValue('include_using_group', v)}
|
onCheckedChange={(v) => form.setValue('include_using_group', v)}
|
||||||
label={t('Include Group')}
|
label={t('Include Group')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
<SettingsSwitchField
|
<SettingsSwitchField
|
||||||
checked={form.watch('include_model_name')}
|
checked={form.watch('include_model_name')}
|
||||||
onCheckedChange={(v) => form.setValue('include_model_name', v)}
|
onCheckedChange={(v) => form.setValue('include_model_name', v)}
|
||||||
label={t('Include Model')}
|
label={t('Include Model')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
<SettingsSwitchField
|
<SettingsSwitchField
|
||||||
checked={form.watch('include_rule_name')}
|
checked={form.watch('include_rule_name')}
|
||||||
onCheckedChange={(v) => form.setValue('include_rule_name', v)}
|
onCheckedChange={(v) => form.setValue('include_rule_name', v)}
|
||||||
label={t('Include Rule Name')}
|
label={t('Include Rule Name')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import {
|
import {
|
||||||
SettingsForm,
|
SettingsForm,
|
||||||
@@ -40,10 +37,8 @@ import { SettingsPageFormActions } from '../components/settings-page-context'
|
|||||||
import { SettingsSection } from '../components/settings-section'
|
import { SettingsSection } from '../components/settings-section'
|
||||||
import { useResetForm } from '../hooks/use-reset-form'
|
import { useResetForm } from '../hooks/use-reset-form'
|
||||||
import { useUpdateOption } from '../hooks/use-update-option'
|
import { useUpdateOption } from '../hooks/use-update-option'
|
||||||
import { safeNumberFieldProps } from '../utils/numeric-field'
|
|
||||||
|
|
||||||
const behaviorSchema = z.object({
|
const behaviorSchema = z.object({
|
||||||
RetryTimes: z.coerce.number().min(0).max(10),
|
|
||||||
DefaultCollapseSidebar: z.boolean(),
|
DefaultCollapseSidebar: z.boolean(),
|
||||||
DemoSiteEnabled: z.boolean(),
|
DemoSiteEnabled: z.boolean(),
|
||||||
SelfUseModeEnabled: z.boolean(),
|
SelfUseModeEnabled: z.boolean(),
|
||||||
@@ -86,28 +81,6 @@ export function SystemBehaviorSection({
|
|||||||
onSave={form.handleSubmit(onSubmit)}
|
onSave={form.handleSubmit(onSubmit)}
|
||||||
isSaving={updateOption.isPending}
|
isSaving={updateOption.isPending}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='RetryTimes'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('Retry Times')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
max='10'
|
|
||||||
{...safeNumberFieldProps(field)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t('Number of times to retry failed requests (0-10)')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='DefaultCollapseSidebar'
|
name='DefaultCollapseSidebar'
|
||||||
|
|||||||
+149
-320
@@ -16,13 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { parseHttpStatusCodeRules } from '@/lib/http-status-code-rules'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -33,8 +32,15 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import {
|
import {
|
||||||
SettingsForm,
|
SettingsForm,
|
||||||
SettingsSwitchContent,
|
SettingsSwitchContent,
|
||||||
@@ -52,146 +58,65 @@ const numericString = z.string().refine((value) => {
|
|||||||
return !Number.isNaN(Number(trimmed)) && Number(trimmed) >= 0
|
return !Number.isNaN(Number(trimmed)) && Number(trimmed) >= 0
|
||||||
}, 'Enter a non-negative number or leave empty')
|
}, 'Enter a non-negative number or leave empty')
|
||||||
|
|
||||||
const monitoringSchema = z
|
const monitoringSchema = z.object({
|
||||||
.object({
|
QuotaRemindThreshold: numericString,
|
||||||
ChannelDisableThreshold: numericString,
|
perf_metrics_setting: z.object({
|
||||||
QuotaRemindThreshold: numericString,
|
enabled: z.boolean(),
|
||||||
AutomaticDisableChannelEnabled: z.boolean(),
|
flush_interval: z.coerce.number().min(1),
|
||||||
AutomaticEnableChannelEnabled: z.boolean(),
|
bucket_time: z.enum(['minute', '5min', 'hour']),
|
||||||
AutomaticDisableKeywords: z.string(),
|
retention_days: z.coerce.number().min(0),
|
||||||
AutomaticDisableStatusCodes: z.string(),
|
}),
|
||||||
AutomaticRetryStatusCodes: z.string(),
|
})
|
||||||
monitor_setting: z.object({
|
|
||||||
auto_test_channel_enabled: z.boolean(),
|
|
||||||
auto_test_channel_minutes: z.coerce
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, 'Interval must be at least 1 minute'),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.superRefine((values, ctx) => {
|
|
||||||
const disableParsed = parseHttpStatusCodeRules(
|
|
||||||
values.AutomaticDisableStatusCodes
|
|
||||||
)
|
|
||||||
if (!disableParsed.ok) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: 'custom',
|
|
||||||
path: ['AutomaticDisableStatusCodes'],
|
|
||||||
message: `Invalid status code rules: ${disableParsed.invalidTokens.join(
|
|
||||||
', '
|
|
||||||
)}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryParsed = parseHttpStatusCodeRules(
|
|
||||||
values.AutomaticRetryStatusCodes
|
|
||||||
)
|
|
||||||
if (!retryParsed.ok) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: 'custom',
|
|
||||||
path: ['AutomaticRetryStatusCodes'],
|
|
||||||
message: `Invalid status code rules: ${retryParsed.invalidTokens.join(
|
|
||||||
', '
|
|
||||||
)}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
type MonitoringFormValues = z.output<typeof monitoringSchema>
|
|
||||||
type MonitoringFormInput = z.input<typeof monitoringSchema>
|
type MonitoringFormInput = z.input<typeof monitoringSchema>
|
||||||
|
type MonitoringFormValues = z.output<typeof monitoringSchema>
|
||||||
|
|
||||||
|
type FlatMonitoringDefaults = {
|
||||||
|
QuotaRemindThreshold: string
|
||||||
|
'perf_metrics_setting.enabled': boolean
|
||||||
|
'perf_metrics_setting.flush_interval': number
|
||||||
|
'perf_metrics_setting.bucket_time': 'minute' | '5min' | 'hour'
|
||||||
|
'perf_metrics_setting.retention_days': number
|
||||||
|
}
|
||||||
|
|
||||||
type MonitoringSettingsSectionProps = {
|
type MonitoringSettingsSectionProps = {
|
||||||
defaultValues: {
|
defaultValues: FlatMonitoringDefaults
|
||||||
ChannelDisableThreshold: string
|
|
||||||
QuotaRemindThreshold: string
|
|
||||||
AutomaticDisableChannelEnabled: boolean
|
|
||||||
AutomaticEnableChannelEnabled: boolean
|
|
||||||
AutomaticDisableKeywords: string
|
|
||||||
AutomaticDisableStatusCodes: string
|
|
||||||
AutomaticRetryStatusCodes: string
|
|
||||||
'monitor_setting.auto_test_channel_enabled': boolean
|
|
||||||
'monitor_setting.auto_test_channel_minutes': number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLineEndings(value: string) {
|
|
||||||
return value.replace(/\r\n/g, '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
type NormalizedMonitoringValues = {
|
|
||||||
ChannelDisableThreshold: string
|
|
||||||
QuotaRemindThreshold: string
|
|
||||||
AutomaticDisableChannelEnabled: boolean
|
|
||||||
AutomaticEnableChannelEnabled: boolean
|
|
||||||
AutomaticDisableKeywords: string
|
|
||||||
AutomaticDisableStatusCodes: string
|
|
||||||
AutomaticRetryStatusCodes: string
|
|
||||||
'monitor_setting.auto_test_channel_enabled': boolean
|
|
||||||
'monitor_setting.auto_test_channel_minutes': number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildFormDefaults = (
|
const buildFormDefaults = (
|
||||||
defaults: MonitoringSettingsSectionProps['defaultValues']
|
defaults: MonitoringSettingsSectionProps['defaultValues']
|
||||||
): MonitoringFormInput => ({
|
): MonitoringFormInput => ({
|
||||||
ChannelDisableThreshold: defaults.ChannelDisableThreshold ?? '',
|
|
||||||
QuotaRemindThreshold: defaults.QuotaRemindThreshold ?? '',
|
QuotaRemindThreshold: defaults.QuotaRemindThreshold ?? '',
|
||||||
AutomaticDisableChannelEnabled: defaults.AutomaticDisableChannelEnabled,
|
perf_metrics_setting: {
|
||||||
AutomaticEnableChannelEnabled: defaults.AutomaticEnableChannelEnabled,
|
enabled: defaults['perf_metrics_setting.enabled'],
|
||||||
AutomaticDisableKeywords: normalizeLineEndings(
|
flush_interval: defaults['perf_metrics_setting.flush_interval'],
|
||||||
defaults.AutomaticDisableKeywords ?? ''
|
bucket_time: defaults['perf_metrics_setting.bucket_time'],
|
||||||
),
|
retention_days: defaults['perf_metrics_setting.retention_days'],
|
||||||
AutomaticDisableStatusCodes: defaults.AutomaticDisableStatusCodes ?? '',
|
|
||||||
AutomaticRetryStatusCodes: defaults.AutomaticRetryStatusCodes ?? '',
|
|
||||||
monitor_setting: {
|
|
||||||
auto_test_channel_enabled:
|
|
||||||
defaults['monitor_setting.auto_test_channel_enabled'],
|
|
||||||
auto_test_channel_minutes:
|
|
||||||
defaults['monitor_setting.auto_test_channel_minutes'],
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalizeDefaults = (
|
const normalizeDefaults = (
|
||||||
defaults: MonitoringSettingsSectionProps['defaultValues']
|
defaults: MonitoringSettingsSectionProps['defaultValues']
|
||||||
): NormalizedMonitoringValues => ({
|
): FlatMonitoringDefaults => ({
|
||||||
ChannelDisableThreshold: (defaults.ChannelDisableThreshold ?? '').trim(),
|
|
||||||
QuotaRemindThreshold: (defaults.QuotaRemindThreshold ?? '').trim(),
|
QuotaRemindThreshold: (defaults.QuotaRemindThreshold ?? '').trim(),
|
||||||
AutomaticDisableChannelEnabled: defaults.AutomaticDisableChannelEnabled,
|
'perf_metrics_setting.enabled': defaults['perf_metrics_setting.enabled'],
|
||||||
AutomaticEnableChannelEnabled: defaults.AutomaticEnableChannelEnabled,
|
'perf_metrics_setting.flush_interval':
|
||||||
AutomaticDisableKeywords: normalizeLineEndings(
|
defaults['perf_metrics_setting.flush_interval'],
|
||||||
defaults.AutomaticDisableKeywords ?? ''
|
'perf_metrics_setting.bucket_time': defaults['perf_metrics_setting.bucket_time'],
|
||||||
),
|
'perf_metrics_setting.retention_days':
|
||||||
AutomaticDisableStatusCodes: parseHttpStatusCodeRules(
|
defaults['perf_metrics_setting.retention_days'],
|
||||||
defaults.AutomaticDisableStatusCodes ?? ''
|
|
||||||
).normalized,
|
|
||||||
AutomaticRetryStatusCodes: parseHttpStatusCodeRules(
|
|
||||||
defaults.AutomaticRetryStatusCodes ?? ''
|
|
||||||
).normalized,
|
|
||||||
'monitor_setting.auto_test_channel_enabled':
|
|
||||||
defaults['monitor_setting.auto_test_channel_enabled'],
|
|
||||||
'monitor_setting.auto_test_channel_minutes':
|
|
||||||
defaults['monitor_setting.auto_test_channel_minutes'],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalizeFormValues = (
|
const normalizeFormValues = (
|
||||||
values: MonitoringFormValues
|
values: MonitoringFormValues
|
||||||
): NormalizedMonitoringValues => ({
|
): FlatMonitoringDefaults => ({
|
||||||
ChannelDisableThreshold: values.ChannelDisableThreshold.trim(),
|
|
||||||
QuotaRemindThreshold: values.QuotaRemindThreshold.trim(),
|
QuotaRemindThreshold: values.QuotaRemindThreshold.trim(),
|
||||||
AutomaticDisableChannelEnabled: values.AutomaticDisableChannelEnabled,
|
'perf_metrics_setting.enabled': values.perf_metrics_setting.enabled,
|
||||||
AutomaticEnableChannelEnabled: values.AutomaticEnableChannelEnabled,
|
'perf_metrics_setting.flush_interval':
|
||||||
AutomaticDisableKeywords: normalizeLineEndings(
|
values.perf_metrics_setting.flush_interval,
|
||||||
values.AutomaticDisableKeywords
|
'perf_metrics_setting.bucket_time': values.perf_metrics_setting.bucket_time,
|
||||||
),
|
'perf_metrics_setting.retention_days':
|
||||||
AutomaticDisableStatusCodes: parseHttpStatusCodeRules(
|
values.perf_metrics_setting.retention_days,
|
||||||
values.AutomaticDisableStatusCodes
|
|
||||||
).normalized,
|
|
||||||
AutomaticRetryStatusCodes: parseHttpStatusCodeRules(
|
|
||||||
values.AutomaticRetryStatusCodes
|
|
||||||
).normalized,
|
|
||||||
'monitor_setting.auto_test_channel_enabled':
|
|
||||||
values.monitor_setting.auto_test_channel_enabled,
|
|
||||||
'monitor_setting.auto_test_channel_minutes':
|
|
||||||
values.monitor_setting.auto_test_channel_minutes,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export function MonitoringSettingsSection({
|
export function MonitoringSettingsSection({
|
||||||
@@ -199,9 +124,12 @@ export function MonitoringSettingsSection({
|
|||||||
}: MonitoringSettingsSectionProps) {
|
}: MonitoringSettingsSectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const updateOption = useUpdateOption()
|
const updateOption = useUpdateOption()
|
||||||
const baselineRef = useRef<NormalizedMonitoringValues>(
|
const baselineRef = useRef<FlatMonitoringDefaults>(
|
||||||
normalizeDefaults(defaultValues)
|
normalizeDefaults(defaultValues)
|
||||||
)
|
)
|
||||||
|
const baselineSerializedRef = useRef<string>(
|
||||||
|
JSON.stringify(normalizeDefaults(defaultValues))
|
||||||
|
)
|
||||||
|
|
||||||
const formDefaults = useMemo(
|
const formDefaults = useMemo(
|
||||||
() => buildFormDefaults(defaultValues),
|
() => buildFormDefaults(defaultValues),
|
||||||
@@ -215,21 +143,20 @@ export function MonitoringSettingsSection({
|
|||||||
|
|
||||||
useResetForm(form, formDefaults)
|
useResetForm(form, formDefaults)
|
||||||
|
|
||||||
const autoDisableStatusCodes = form.watch('AutomaticDisableStatusCodes')
|
useEffect(() => {
|
||||||
const autoRetryStatusCodes = form.watch('AutomaticRetryStatusCodes')
|
const normalized = normalizeDefaults(defaultValues)
|
||||||
const autoDisableParsed = useMemo(
|
const serialized = JSON.stringify(normalized)
|
||||||
() => parseHttpStatusCodeRules(autoDisableStatusCodes),
|
if (serialized === baselineSerializedRef.current) return
|
||||||
[autoDisableStatusCodes]
|
baselineRef.current = normalized
|
||||||
)
|
baselineSerializedRef.current = serialized
|
||||||
const autoRetryParsed = useMemo(
|
}, [defaultValues])
|
||||||
() => parseHttpStatusCodeRules(autoRetryStatusCodes),
|
|
||||||
[autoRetryStatusCodes]
|
const perfMetricsEnabled = form.watch('perf_metrics_setting.enabled')
|
||||||
)
|
|
||||||
|
|
||||||
const onSubmit = async (values: MonitoringFormValues) => {
|
const onSubmit = async (values: MonitoringFormValues) => {
|
||||||
const normalized = normalizeFormValues(values)
|
const normalized = normalizeFormValues(values)
|
||||||
const updates = (
|
const updates = (
|
||||||
Object.keys(normalized) as Array<keyof NormalizedMonitoringValues>
|
Object.keys(normalized) as Array<keyof FlatMonitoringDefaults>
|
||||||
).filter((key) => normalized[key] !== baselineRef.current[key])
|
).filter((key) => normalized[key] !== baselineRef.current[key])
|
||||||
|
|
||||||
if (updates.length === 0) {
|
if (updates.length === 0) {
|
||||||
@@ -238,14 +165,14 @@ export function MonitoringSettingsSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const key of updates) {
|
for (const key of updates) {
|
||||||
const value = normalized[key]
|
|
||||||
await updateOption.mutateAsync({
|
await updateOption.mutateAsync({
|
||||||
key,
|
key,
|
||||||
value,
|
value: normalized[key],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
baselineRef.current = normalized
|
baselineRef.current = normalized
|
||||||
|
baselineSerializedRef.current = JSON.stringify(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -255,226 +182,128 @@ export function MonitoringSettingsSection({
|
|||||||
<SettingsPageFormActions
|
<SettingsPageFormActions
|
||||||
onSave={form.handleSubmit(onSubmit)}
|
onSave={form.handleSubmit(onSubmit)}
|
||||||
isSaving={updateOption.isPending}
|
isSaving={updateOption.isPending}
|
||||||
saveLabel='Save monitoring rules'
|
|
||||||
/>
|
/>
|
||||||
<div className='grid gap-6 md:grid-cols-2'>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='monitor_setting.auto_test_channel_enabled'
|
|
||||||
render={({ field }) => (
|
|
||||||
<SettingsSwitchItem>
|
|
||||||
<SettingsSwitchContent>
|
|
||||||
<FormLabel>{t('Scheduled channel tests')}</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
{t('Automatically probe all channels in the background')}
|
|
||||||
</FormDescription>
|
|
||||||
</SettingsSwitchContent>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</SettingsSwitchItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='monitor_setting.auto_test_channel_minutes'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('Test interval (minutes)')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
{...safeNumberFieldProps(field)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t('How frequently the system tests all channels')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-6 md:grid-cols-2'>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='ChannelDisableThreshold'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('Disable threshold (seconds)')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
value={field.value}
|
|
||||||
onChange={(event) => field.onChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
'Automatically disable channels exceeding this response time'
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='QuotaRemindThreshold'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('Quota reminder (tokens)')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
value={field.value}
|
|
||||||
onChange={(event) => field.onChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t('Send email alerts when a user falls below this quota')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-6 md:grid-cols-2'>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='AutomaticDisableChannelEnabled'
|
|
||||||
render={({ field }) => (
|
|
||||||
<SettingsSwitchItem>
|
|
||||||
<SettingsSwitchContent>
|
|
||||||
<FormLabel>{t('Disable on failure')}</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
{t('Automatically disable channels when tests fail')}
|
|
||||||
</FormDescription>
|
|
||||||
</SettingsSwitchContent>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</SettingsSwitchItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='AutomaticEnableChannelEnabled'
|
|
||||||
render={({ field }) => (
|
|
||||||
<SettingsSwitchItem>
|
|
||||||
<SettingsSwitchContent>
|
|
||||||
<FormLabel>{t('Re-enable on success')}</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
{t('Bring channels back online after successful checks')}
|
|
||||||
</FormDescription>
|
|
||||||
</SettingsSwitchContent>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</SettingsSwitchItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='AutomaticDisableKeywords'
|
name='QuotaRemindThreshold'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Failure keywords')}</FormLabel>
|
<FormLabel>{t('Quota reminder (tokens)')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Input
|
||||||
rows={6}
|
type='number'
|
||||||
placeholder={t('one keyword per line')}
|
min={0}
|
||||||
{...field}
|
step={1}
|
||||||
|
value={field.value}
|
||||||
onChange={(event) => field.onChange(event.target.value)}
|
onChange={(event) => field.onChange(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t('Send email alerts when a user falls below this quota')}
|
||||||
'If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.'
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='grid gap-6 md:grid-cols-2'>
|
<div>
|
||||||
|
<h4 className='font-medium'>{t('Model performance metrics')}</h4>
|
||||||
|
<p className='text-muted-foreground mt-1 text-xs'>
|
||||||
|
{t(
|
||||||
|
'Collect relay latency and success-rate metrics for the model square.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-1 gap-4 md:grid-cols-4'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='AutomaticDisableStatusCodes'
|
name='perf_metrics_setting.enabled'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<SettingsSwitchItem>
|
||||||
<FormLabel>{t('Auto-disable status codes')}</FormLabel>
|
<SettingsSwitchContent>
|
||||||
|
<FormLabel>
|
||||||
|
{t('Enable model performance metrics')}
|
||||||
|
</FormLabel>
|
||||||
|
</SettingsSwitchContent>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</SettingsSwitchItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='perf_metrics_setting.flush_interval'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Flush interval (minutes)')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('e.g. 401, 403, 429, 500-599')}
|
type='number'
|
||||||
value={field.value}
|
min={1}
|
||||||
onChange={(event) => field.onChange(event.target.value)}
|
step={1}
|
||||||
|
{...safeNumberFieldProps(field)}
|
||||||
|
disabled={!perfMetricsEnabled}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
'Accepts comma-separated status codes and inclusive ranges.'
|
|
||||||
)}{' '}
|
|
||||||
{autoDisableParsed.ok &&
|
|
||||||
autoDisableParsed.normalized &&
|
|
||||||
autoDisableParsed.normalized !== field.value.trim() && (
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
{t('Normalized:')} {autoDisableParsed.normalized}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='AutomaticRetryStatusCodes'
|
name='perf_metrics_setting.bucket_time'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Auto-retry status codes')}</FormLabel>
|
<FormLabel>{t('Aggregation bucket')}</FormLabel>
|
||||||
|
<Select
|
||||||
|
items={[
|
||||||
|
{ value: 'minute', label: t('1 minute') },
|
||||||
|
{ value: '5min', label: t('5 minutes') },
|
||||||
|
{ value: 'hour', label: t('1 hour') },
|
||||||
|
]}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
disabled={!perfMetricsEnabled}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent alignItemWithTrigger={false}>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value='minute'>{t('1 minute')}</SelectItem>
|
||||||
|
<SelectItem value='5min'>{t('5 minutes')}</SelectItem>
|
||||||
|
<SelectItem value='hour'>{t('1 hour')}</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='perf_metrics_setting.retention_days'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Retention days')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('e.g. 401, 403, 429, 500-599')}
|
type='number'
|
||||||
value={field.value}
|
min={0}
|
||||||
onChange={(event) => field.onChange(event.target.value)}
|
step={1}
|
||||||
|
{...safeNumberFieldProps(field)}
|
||||||
|
disabled={!perfMetricsEnabled}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t('0 means data is kept permanently')}
|
||||||
'Accepts comma-separated status codes and inclusive ranges.'
|
|
||||||
)}{' '}
|
|
||||||
{autoRetryParsed.ok &&
|
|
||||||
autoRetryParsed.normalized &&
|
|
||||||
autoRetryParsed.normalized !== field.value.trim() && (
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
{t('Normalized:')} {autoRetryParsed.normalized}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
+6
-8
@@ -98,7 +98,7 @@ export function WaffoSettingsSection({
|
|||||||
|
|
||||||
const saveMethod = () => {
|
const saveMethod = () => {
|
||||||
if (!methodForm.name.trim())
|
if (!methodForm.name.trim())
|
||||||
return toast.error(t('Payment method name is required'))
|
{return toast.error(t('Payment method name is required'))}
|
||||||
if (editingIdx === -1) {
|
if (editingIdx === -1) {
|
||||||
onPayMethodsChange((prev) => [...prev, methodForm])
|
onPayMethodsChange((prev) => [...prev, methodForm])
|
||||||
} else {
|
} else {
|
||||||
@@ -125,15 +125,13 @@ export function WaffoSettingsSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (loadEvent) => {
|
reader.addEventListener('load', () => {
|
||||||
setMethodForm((previous) => ({
|
setMethodForm((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
icon:
|
icon:
|
||||||
typeof loadEvent.target?.result === 'string'
|
typeof reader.result === 'string' ? reader.result : '',
|
||||||
? loadEvent.target.result
|
|
||||||
: '',
|
|
||||||
}))
|
}))
|
||||||
}
|
})
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
event.target.value = ''
|
event.target.value = ''
|
||||||
}
|
}
|
||||||
@@ -164,13 +162,13 @@ export function WaffoSettingsSection({
|
|||||||
checked={values.WaffoEnabled}
|
checked={values.WaffoEnabled}
|
||||||
onCheckedChange={(v) => onValueChange('WaffoEnabled', v)}
|
onCheckedChange={(v) => onValueChange('WaffoEnabled', v)}
|
||||||
label={t('Enable Waffo')}
|
label={t('Enable Waffo')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
<SettingsSwitchField
|
<SettingsSwitchField
|
||||||
checked={values.WaffoSandbox}
|
checked={values.WaffoSandbox}
|
||||||
onCheckedChange={(v) => onValueChange('WaffoSandbox', v)}
|
onCheckedChange={(v) => onValueChange('WaffoSandbox', v)}
|
||||||
label={t('Sandbox mode')}
|
label={t('Sandbox mode')}
|
||||||
className='border-b-0 py-0'
|
className='py-0'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -270,7 +270,7 @@ export function HeaderNavigationSection({
|
|||||||
name={module.requireAuthKey}
|
name={module.requireAuthKey}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SettingsControlChildren>
|
<SettingsControlChildren>
|
||||||
<SettingsSwitchItem className='border-b-0 py-2'>
|
<SettingsSwitchItem className='py-2'>
|
||||||
<SettingsSwitchContent>
|
<SettingsSwitchContent>
|
||||||
<FormLabel>{module.requireAuthTitle}</FormLabel>
|
<FormLabel>{module.requireAuthTitle}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
+243
-1
@@ -16,13 +16,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
import dayjs from '@/lib/dayjs'
|
||||||
import { formatTimestampToDate } from '@/lib/format'
|
import { formatTimestampToDate } from '@/lib/format'
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -32,6 +35,7 @@ import {
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +46,17 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { DateTimePicker } from '@/components/datetime-picker'
|
import { DateTimePicker } from '@/components/datetime-picker'
|
||||||
import { deleteLogsBefore } from '../api'
|
import { deleteLogsBefore } from '../api'
|
||||||
@@ -65,8 +80,30 @@ type LogSettingsSectionProps = {
|
|||||||
defaultEnabled: boolean
|
defaultEnabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerLogInfo = {
|
||||||
|
enabled: boolean
|
||||||
|
log_dir: string
|
||||||
|
file_count: number
|
||||||
|
total_size: number
|
||||||
|
oldest_time?: string
|
||||||
|
newest_time?: string
|
||||||
|
}
|
||||||
|
|
||||||
const HOURS_IN_DAY = 24
|
const HOURS_IN_DAY = 24
|
||||||
|
|
||||||
|
function formatBytes(bytes: number, decimals = 2): string {
|
||||||
|
if (!bytes || Number.isNaN(bytes)) return '0 Bytes'
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
if (bytes < 0) return `-${formatBytes(-bytes, decimals)}`
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k))
|
||||||
|
if (i < 0 || i >= sizes.length) return `${bytes} Bytes`
|
||||||
|
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${
|
||||||
|
sizes[i]
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
const getDateHoursAgo = (hours: number) => {
|
const getDateHoursAgo = (hours: number) => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
date.setHours(date.getHours() - hours)
|
date.setHours(date.getHours() - hours)
|
||||||
@@ -107,11 +144,30 @@ export function LogSettingsSection({
|
|||||||
)
|
)
|
||||||
const [isCleaning, setIsCleaning] = useState(false)
|
const [isCleaning, setIsCleaning] = useState(false)
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
||||||
|
const [serverLogInfo, setServerLogInfo] = useState<ServerLogInfo | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
const [serverLogCleanupMode, setServerLogCleanupMode] = useState('by_count')
|
||||||
|
const [serverLogCleanupValue, setServerLogCleanupValue] = useState(10)
|
||||||
|
const [serverLogCleanupLoading, setServerLogCleanupLoading] = useState(false)
|
||||||
|
|
||||||
|
const fetchServerLogInfo = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/performance/logs')
|
||||||
|
if (res.data.success) setServerLogInfo(res.data.data)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({ LogConsumeEnabled: defaultEnabled })
|
form.reset({ LogConsumeEnabled: defaultEnabled })
|
||||||
}, [defaultEnabled, form])
|
}, [defaultEnabled, form])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchServerLogInfo()
|
||||||
|
}, [fetchServerLogInfo])
|
||||||
|
|
||||||
const purgeTimestamp = useMemo(() => {
|
const purgeTimestamp = useMemo(() => {
|
||||||
if (!purgeDate) return null
|
if (!purgeDate) return null
|
||||||
return Math.floor(purgeDate.getTime() / 1000)
|
return Math.floor(purgeDate.getTime() / 1000)
|
||||||
@@ -166,6 +222,40 @@ export function LogSettingsSection({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanupServerLogFiles = async () => {
|
||||||
|
if (
|
||||||
|
!serverLogCleanupValue ||
|
||||||
|
Number.isNaN(serverLogCleanupValue) ||
|
||||||
|
serverLogCleanupValue < 1
|
||||||
|
) {
|
||||||
|
toast.error(t('Please enter a valid number'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setServerLogCleanupLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await api.delete(
|
||||||
|
`/api/performance/logs?mode=${serverLogCleanupMode}&value=${serverLogCleanupValue}`
|
||||||
|
)
|
||||||
|
if (res.data.success) {
|
||||||
|
const { deleted_count, freed_bytes } = res.data.data
|
||||||
|
toast.success(
|
||||||
|
t('Cleaned up {{count}} log files, freed {{size}}', {
|
||||||
|
count: deleted_count,
|
||||||
|
size: formatBytes(freed_bytes),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
toast.error(res.data.message || t('Cleanup failed'))
|
||||||
|
}
|
||||||
|
fetchServerLogInfo()
|
||||||
|
} catch {
|
||||||
|
toast.error(t('Cleanup failed'))
|
||||||
|
} finally {
|
||||||
|
setServerLogCleanupLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={t('Log Maintenance')}>
|
<SettingsSection title={t('Log Maintenance')}>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -232,6 +322,158 @@ export function LogSettingsSection({
|
|||||||
</SettingsControlGroup>
|
</SettingsControlGroup>
|
||||||
</SettingsForm>
|
</SettingsForm>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div>
|
||||||
|
<h4 className='font-medium'>{t('Server Log Management')}</h4>
|
||||||
|
<p className='text-muted-foreground mt-1 text-xs'>
|
||||||
|
{t(
|
||||||
|
'Manage server log files. Log files accumulate over time; regular cleanup is recommended to free disk space.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{serverLogInfo !== null &&
|
||||||
|
(serverLogInfo.enabled ? (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div className='rounded-lg border p-4'>
|
||||||
|
<div className='grid grid-cols-2 gap-2 text-sm md:grid-cols-4'>
|
||||||
|
<div>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t('Log Directory')}:
|
||||||
|
</span>{' '}
|
||||||
|
<span className='font-mono text-xs'>
|
||||||
|
{serverLogInfo.log_dir}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t('Log File Count')}:
|
||||||
|
</span>{' '}
|
||||||
|
{serverLogInfo.file_count}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t('Total Log Size')}:
|
||||||
|
</span>{' '}
|
||||||
|
{formatBytes(serverLogInfo.total_size)}
|
||||||
|
</div>
|
||||||
|
{serverLogInfo.oldest_time && serverLogInfo.newest_time && (
|
||||||
|
<div>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t('Date Range')}:
|
||||||
|
</span>{' '}
|
||||||
|
{dayjs(serverLogInfo.oldest_time).format('YYYY-MM-DD')} ~{' '}
|
||||||
|
{dayjs(serverLogInfo.newest_time).format('YYYY-MM-DD')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-wrap items-end gap-3'>
|
||||||
|
<div className='grid gap-1.5'>
|
||||||
|
<Label className='text-xs'>{t('Cleanup Mode')}</Label>
|
||||||
|
<Select
|
||||||
|
items={[
|
||||||
|
{ value: 'by_count', label: t('Retain last N files') },
|
||||||
|
{ value: 'by_days', label: t('Retain last N days') },
|
||||||
|
]}
|
||||||
|
value={serverLogCleanupMode}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
value !== null && setServerLogCleanupMode(value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className='w-[160px]'>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent alignItemWithTrigger={false}>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value='by_count'>
|
||||||
|
{t('Retain last N files')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value='by_days'>
|
||||||
|
{t('Retain last N days')}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-1.5'>
|
||||||
|
<Label className='text-xs'>
|
||||||
|
{serverLogCleanupMode === 'by_count'
|
||||||
|
? t('Files to Retain')
|
||||||
|
: t('Days to Retain')}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={1}
|
||||||
|
max={serverLogCleanupMode === 'by_count' ? 1000 : 3650}
|
||||||
|
value={serverLogCleanupValue}
|
||||||
|
onChange={(event) =>
|
||||||
|
setServerLogCleanupValue(Number(event.target.value))
|
||||||
|
}
|
||||||
|
className='w-[120px]'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='destructive'
|
||||||
|
size='sm'
|
||||||
|
disabled={serverLogCleanupLoading}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{serverLogCleanupLoading
|
||||||
|
? t('Cleaning...')
|
||||||
|
: t('Clean Up Log Files')}
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t('Confirm log file cleanup?')}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{serverLogCleanupMode === 'by_count'
|
||||||
|
? t(
|
||||||
|
'Only the last {{value}} log files will be retained; the rest will be deleted.',
|
||||||
|
{
|
||||||
|
value: serverLogCleanupValue,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
'Log files older than {{value}} days will be deleted.',
|
||||||
|
{
|
||||||
|
value: serverLogCleanupValue,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={cleanupServerLogFiles}>
|
||||||
|
{t('Confirm Cleanup')}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
{t(
|
||||||
|
'Server logging is not enabled (log directory not configured)'
|
||||||
|
)}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
+7
-341
@@ -23,7 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import dayjs from '@/lib/dayjs'
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -47,16 +46,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
@@ -89,12 +79,6 @@ const perfSchema = z.object({
|
|||||||
monitor_memory_threshold: z.coerce.number().min(0).max(100),
|
monitor_memory_threshold: z.coerce.number().min(0).max(100),
|
||||||
monitor_disk_threshold: z.coerce.number().min(0).max(100),
|
monitor_disk_threshold: z.coerce.number().min(0).max(100),
|
||||||
}),
|
}),
|
||||||
perf_metrics_setting: z.object({
|
|
||||||
enabled: z.boolean(),
|
|
||||||
flush_interval: z.coerce.number().min(1),
|
|
||||||
bucket_time: z.enum(['minute', '5min', 'hour']),
|
|
||||||
retention_days: z.coerce.number().min(0),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type PerfFormInput = z.input<typeof perfSchema>
|
type PerfFormInput = z.input<typeof perfSchema>
|
||||||
@@ -109,10 +93,6 @@ type FlatPerfDefaults = {
|
|||||||
'performance_setting.monitor_cpu_threshold': number
|
'performance_setting.monitor_cpu_threshold': number
|
||||||
'performance_setting.monitor_memory_threshold': number
|
'performance_setting.monitor_memory_threshold': number
|
||||||
'performance_setting.monitor_disk_threshold': number
|
'performance_setting.monitor_disk_threshold': number
|
||||||
'perf_metrics_setting.enabled': boolean
|
|
||||||
'perf_metrics_setting.flush_interval': number
|
|
||||||
'perf_metrics_setting.bucket_time': 'minute' | '5min' | 'hour'
|
|
||||||
'perf_metrics_setting.retention_days': number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildFormDefaults = (defaults: FlatPerfDefaults): PerfFormInput => ({
|
const buildFormDefaults = (defaults: FlatPerfDefaults): PerfFormInput => ({
|
||||||
@@ -131,12 +111,6 @@ const buildFormDefaults = (defaults: FlatPerfDefaults): PerfFormInput => ({
|
|||||||
monitor_disk_threshold:
|
monitor_disk_threshold:
|
||||||
defaults['performance_setting.monitor_disk_threshold'],
|
defaults['performance_setting.monitor_disk_threshold'],
|
||||||
},
|
},
|
||||||
perf_metrics_setting: {
|
|
||||||
enabled: defaults['perf_metrics_setting.enabled'],
|
|
||||||
flush_interval: defaults['perf_metrics_setting.flush_interval'],
|
|
||||||
bucket_time: defaults['perf_metrics_setting.bucket_time'],
|
|
||||||
retention_days: defaults['perf_metrics_setting.retention_days'],
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalizeFormValues = (values: PerfFormValues): FlatPerfDefaults => ({
|
const normalizeFormValues = (values: PerfFormValues): FlatPerfDefaults => ({
|
||||||
@@ -156,38 +130,25 @@ const normalizeFormValues = (values: PerfFormValues): FlatPerfDefaults => ({
|
|||||||
values.performance_setting.monitor_memory_threshold,
|
values.performance_setting.monitor_memory_threshold,
|
||||||
'performance_setting.monitor_disk_threshold':
|
'performance_setting.monitor_disk_threshold':
|
||||||
values.performance_setting.monitor_disk_threshold,
|
values.performance_setting.monitor_disk_threshold,
|
||||||
'perf_metrics_setting.enabled': values.perf_metrics_setting.enabled,
|
|
||||||
'perf_metrics_setting.flush_interval':
|
|
||||||
values.perf_metrics_setting.flush_interval,
|
|
||||||
'perf_metrics_setting.bucket_time': values.perf_metrics_setting.bucket_time,
|
|
||||||
'perf_metrics_setting.retention_days':
|
|
||||||
values.perf_metrics_setting.retention_days,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatBytes(bytes: number, decimals = 2): string {
|
function formatBytes(bytes: number, decimals = 2): string {
|
||||||
if (!bytes || isNaN(bytes)) return '0 Bytes'
|
if (!bytes || Number.isNaN(bytes)) return '0 Bytes'
|
||||||
if (bytes === 0) return '0 Bytes'
|
if (bytes === 0) return '0 Bytes'
|
||||||
if (bytes < 0) return '-' + formatBytes(-bytes, decimals)
|
if (bytes < 0) return `-${formatBytes(-bytes, decimals)}`
|
||||||
const k = 1024
|
const k = 1024
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||||
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k))
|
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k))
|
||||||
if (i < 0 || i >= sizes.length) return bytes + ' Bytes'
|
if (i < 0 || i >= sizes.length) return `${bytes} Bytes`
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]
|
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${
|
||||||
|
sizes[i]
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
defaultValues: FlatPerfDefaults
|
defaultValues: FlatPerfDefaults
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogInfo = {
|
|
||||||
enabled: boolean
|
|
||||||
log_dir: string
|
|
||||||
file_count: number
|
|
||||||
total_size: number
|
|
||||||
oldest_time?: string
|
|
||||||
newest_time?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PerformanceStats = {
|
type PerformanceStats = {
|
||||||
cache_stats?: {
|
cache_stats?: {
|
||||||
current_disk_usage_bytes: number
|
current_disk_usage_bytes: number
|
||||||
@@ -225,10 +186,6 @@ export function PerformanceSection(props: Props) {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const updateOption = useUpdateOption()
|
const updateOption = useUpdateOption()
|
||||||
const [stats, setStats] = useState<PerformanceStats | null>(null)
|
const [stats, setStats] = useState<PerformanceStats | null>(null)
|
||||||
const [logInfo, setLogInfo] = useState<LogInfo | null>(null)
|
|
||||||
const [logCleanupMode, setLogCleanupMode] = useState('by_count')
|
|
||||||
const [logCleanupValue, setLogCleanupValue] = useState(10)
|
|
||||||
const [logCleanupLoading, setLogCleanupLoading] = useState(false)
|
|
||||||
|
|
||||||
const formDefaults = useMemo(
|
const formDefaults = useMemo(
|
||||||
() => buildFormDefaults(props.defaultValues),
|
() => buildFormDefaults(props.defaultValues),
|
||||||
@@ -262,19 +219,9 @@ export function PerformanceSection(props: Props) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchLogInfo = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/api/performance/logs')
|
|
||||||
if (res.data.success) setLogInfo(res.data.data)
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats()
|
fetchStats()
|
||||||
fetchLogInfo()
|
}, [fetchStats])
|
||||||
}, [fetchStats, fetchLogInfo])
|
|
||||||
|
|
||||||
const onSubmit = async (values: PerfFormValues) => {
|
const onSubmit = async (values: PerfFormValues) => {
|
||||||
const normalized = normalizeFormValues(values)
|
const normalized = normalizeFormValues(values)
|
||||||
@@ -336,38 +283,8 @@ export function PerformanceSection(props: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanupLogFiles = async () => {
|
|
||||||
if (!logCleanupValue || isNaN(logCleanupValue) || logCleanupValue < 1) {
|
|
||||||
toast.error(t('Please enter a valid number'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLogCleanupLoading(true)
|
|
||||||
try {
|
|
||||||
const res = await api.delete(
|
|
||||||
`/api/performance/logs?mode=${logCleanupMode}&value=${logCleanupValue}`
|
|
||||||
)
|
|
||||||
if (res.data.success) {
|
|
||||||
const { deleted_count, freed_bytes } = res.data.data
|
|
||||||
toast.success(
|
|
||||||
t('Cleaned up {{count}} log files, freed {{size}}', {
|
|
||||||
count: deleted_count,
|
|
||||||
size: formatBytes(freed_bytes),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
toast.error(res.data.message || t('Cleanup failed'))
|
|
||||||
}
|
|
||||||
fetchLogInfo()
|
|
||||||
} catch {
|
|
||||||
toast.error(t('Cleanup failed'))
|
|
||||||
} finally {
|
|
||||||
setLogCleanupLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const diskEnabled = form.watch('performance_setting.disk_cache_enabled')
|
const diskEnabled = form.watch('performance_setting.disk_cache_enabled')
|
||||||
const monitorEnabled = form.watch('performance_setting.monitor_enabled')
|
const monitorEnabled = form.watch('performance_setting.monitor_enabled')
|
||||||
const perfMetricsEnabled = form.watch('perf_metrics_setting.enabled')
|
|
||||||
const maxCacheSizeRaw = form.watch(
|
const maxCacheSizeRaw = form.watch(
|
||||||
'performance_setting.disk_cache_max_size_mb'
|
'performance_setting.disk_cache_max_size_mb'
|
||||||
)
|
)
|
||||||
@@ -607,262 +524,11 @@ export function PerformanceSection(props: Props) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className='font-medium'>{t('Model performance metrics')}</h4>
|
|
||||||
<p className='text-muted-foreground mt-1 text-xs'>
|
|
||||||
{t(
|
|
||||||
'Collect relay latency and success-rate metrics for the model square.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-4'>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='perf_metrics_setting.enabled'
|
|
||||||
render={({ field }) => (
|
|
||||||
<SettingsSwitchItem>
|
|
||||||
<SettingsSwitchContent>
|
|
||||||
<FormLabel>
|
|
||||||
{t('Enable model performance metrics')}
|
|
||||||
</FormLabel>
|
|
||||||
</SettingsSwitchContent>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</SettingsSwitchItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='perf_metrics_setting.flush_interval'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('Flush interval (minutes)')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
{...safeNumberFieldProps(field)}
|
|
||||||
disabled={!perfMetricsEnabled}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='perf_metrics_setting.bucket_time'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('Aggregation bucket')}</FormLabel>
|
|
||||||
<Select
|
|
||||||
items={[
|
|
||||||
{ value: 'minute', label: t('1 minute') },
|
|
||||||
{ value: '5min', label: t('5 minutes') },
|
|
||||||
{ value: 'hour', label: t('1 hour') },
|
|
||||||
]}
|
|
||||||
value={field.value}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
disabled={!perfMetricsEnabled}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent alignItemWithTrigger={false}>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value='minute'>{t('1 minute')}</SelectItem>
|
|
||||||
<SelectItem value='5min'>{t('5 minutes')}</SelectItem>
|
|
||||||
<SelectItem value='hour'>{t('1 hour')}</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='perf_metrics_setting.retention_days'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('Retention days')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
{...safeNumberFieldProps(field)}
|
|
||||||
disabled={!perfMetricsEnabled}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t('0 means data is kept permanently')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsForm>
|
</SettingsForm>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Server Log Management */}
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<div>
|
|
||||||
<h4 className='font-medium'>{t('Server Log Management')}</h4>
|
|
||||||
<p className='text-muted-foreground mt-1 text-xs'>
|
|
||||||
{t(
|
|
||||||
'Manage server log files. Log files accumulate over time; regular cleanup is recommended to free disk space.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{logInfo === null ? null : logInfo.enabled ? (
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<div className='rounded-lg border p-4'>
|
|
||||||
<div className='grid grid-cols-2 gap-2 text-sm md:grid-cols-4'>
|
|
||||||
<div>
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
{t('Log Directory')}:
|
|
||||||
</span>{' '}
|
|
||||||
<span className='font-mono text-xs'>{logInfo.log_dir}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
{t('Log File Count')}:
|
|
||||||
</span>{' '}
|
|
||||||
{logInfo.file_count}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
{t('Total Log Size')}:
|
|
||||||
</span>{' '}
|
|
||||||
{formatBytes(logInfo.total_size)}
|
|
||||||
</div>
|
|
||||||
{logInfo.oldest_time && logInfo.newest_time && (
|
|
||||||
<div>
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
{t('Date Range')}:
|
|
||||||
</span>{' '}
|
|
||||||
{dayjs(logInfo.oldest_time).format('YYYY-MM-DD')} ~{' '}
|
|
||||||
{dayjs(logInfo.newest_time).format('YYYY-MM-DD')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-wrap items-end gap-3'>
|
|
||||||
<div className='grid gap-1.5'>
|
|
||||||
<Label className='text-xs'>{t('Cleanup Mode')}</Label>
|
|
||||||
<Select
|
|
||||||
items={[
|
|
||||||
{ value: 'by_count', label: t('Retain last N files') },
|
|
||||||
{ value: 'by_days', label: t('Retain last N days') },
|
|
||||||
]}
|
|
||||||
value={logCleanupMode}
|
|
||||||
onValueChange={(v) => v !== null && setLogCleanupMode(v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className='w-[160px]'>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent alignItemWithTrigger={false}>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value='by_count'>
|
|
||||||
{t('Retain last N files')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value='by_days'>
|
|
||||||
{t('Retain last N days')}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='grid gap-1.5'>
|
|
||||||
<Label className='text-xs'>
|
|
||||||
{logCleanupMode === 'by_count'
|
|
||||||
? t('Files to Retain')
|
|
||||||
: t('Days to Retain')}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type='number'
|
|
||||||
min={1}
|
|
||||||
max={logCleanupMode === 'by_count' ? 1000 : 3650}
|
|
||||||
value={logCleanupValue}
|
|
||||||
onChange={(e) => setLogCleanupValue(Number(e.target.value))}
|
|
||||||
className='w-[120px]'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant='destructive'
|
|
||||||
size='sm'
|
|
||||||
disabled={logCleanupLoading}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{logCleanupLoading
|
|
||||||
? t('Cleaning...')
|
|
||||||
: t('Clean Up Log Files')}
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{t('Confirm log file cleanup?')}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{logCleanupMode === 'by_count'
|
|
||||||
? t(
|
|
||||||
'Only the last {{value}} log files will be retained; the rest will be deleted.',
|
|
||||||
{
|
|
||||||
value: logCleanupValue,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
'Log files older than {{value}} days will be deleted.',
|
|
||||||
{
|
|
||||||
value: logCleanupValue,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={cleanupLogFiles}>
|
|
||||||
{t('Confirm Cleanup')}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Alert>
|
|
||||||
<AlertDescription>
|
|
||||||
{t(
|
|
||||||
'Server logging is not enabled (log directory not configured)'
|
|
||||||
)}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* Performance Stats Dashboard */}
|
{/* Performance Stats Dashboard */}
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
|
|||||||
+2
-2
@@ -51,7 +51,7 @@ type SidebarModulesSectionProps = {
|
|||||||
type SidebarFormValues = SidebarModulesAdminConfig
|
type SidebarFormValues = SidebarModulesAdminConfig
|
||||||
|
|
||||||
const toTitleCase = (value: string) =>
|
const toTitleCase = (value: string) =>
|
||||||
value.replace(/[_-]+/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase())
|
value.replaceAll(/[_-]+/g, ' ').replaceAll(/\b\w/g, (char) => char.toUpperCase())
|
||||||
|
|
||||||
export function SidebarModulesSection({
|
export function SidebarModulesSection({
|
||||||
config,
|
config,
|
||||||
@@ -237,7 +237,7 @@ export function SidebarModulesSection({
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
name={`${sectionKey}.${moduleKey}` as any}
|
name={`${sectionKey}.${moduleKey}` as any}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SettingsSwitchItem className='border-b-0 py-2'>
|
<SettingsSwitchItem className='py-2'>
|
||||||
<SettingsSwitchContent>
|
<SettingsSwitchContent>
|
||||||
<FormLabel>{moduleInfo.title}</FormLabel>
|
<FormLabel>{moduleInfo.title}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ const defaultModelSettings: ModelSettings = {
|
|||||||
AutoGroups: '',
|
AutoGroups: '',
|
||||||
DefaultUseAutoGroup: false,
|
DefaultUseAutoGroup: false,
|
||||||
'group_ratio_setting.group_special_usable_group': '{}',
|
'group_ratio_setting.group_special_usable_group': '{}',
|
||||||
|
RetryTimes: 0,
|
||||||
|
ChannelDisableThreshold: '',
|
||||||
|
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,
|
||||||
'channel_affinity_setting.enabled': false,
|
'channel_affinity_setting.enabled': false,
|
||||||
'channel_affinity_setting.switch_on_success': true,
|
'channel_affinity_setting.switch_on_success': true,
|
||||||
'channel_affinity_setting.keep_on_channel_disabled': false,
|
'channel_affinity_setting.keep_on_channel_disabled': false,
|
||||||
|
|||||||
+513
@@ -0,0 +1,513 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2023-2026 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { parseHttpStatusCodeRules } from '@/lib/http-status-code-rules'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
SettingsForm,
|
||||||
|
SettingsSwitchContent,
|
||||||
|
SettingsSwitchItem,
|
||||||
|
} from '../components/settings-form-layout'
|
||||||
|
import { SettingsPageFormActions } from '../components/settings-page-context'
|
||||||
|
import { SettingsSection } from '../components/settings-section'
|
||||||
|
import { useResetForm } from '../hooks/use-reset-form'
|
||||||
|
import { useUpdateOption } from '../hooks/use-update-option'
|
||||||
|
import { safeNumberFieldProps } from '../utils/numeric-field'
|
||||||
|
|
||||||
|
const numericString = z.string().refine((value) => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return true
|
||||||
|
return !Number.isNaN(Number(trimmed)) && Number(trimmed) >= 0
|
||||||
|
}, 'Enter a non-negative number or leave empty')
|
||||||
|
|
||||||
|
const routingReliabilitySchema = z
|
||||||
|
.object({
|
||||||
|
RetryTimes: z.coerce.number().min(0).max(10),
|
||||||
|
ChannelDisableThreshold: numericString,
|
||||||
|
AutomaticDisableChannelEnabled: z.boolean(),
|
||||||
|
AutomaticEnableChannelEnabled: z.boolean(),
|
||||||
|
AutomaticDisableKeywords: z.string(),
|
||||||
|
AutomaticDisableStatusCodes: z.string(),
|
||||||
|
AutomaticRetryStatusCodes: z.string(),
|
||||||
|
monitor_setting: z.object({
|
||||||
|
auto_test_channel_enabled: z.boolean(),
|
||||||
|
auto_test_channel_minutes: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, 'Interval must be at least 1 minute'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.superRefine((values, ctx) => {
|
||||||
|
const disableParsed = parseHttpStatusCodeRules(
|
||||||
|
values.AutomaticDisableStatusCodes
|
||||||
|
)
|
||||||
|
if (!disableParsed.ok) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
path: ['AutomaticDisableStatusCodes'],
|
||||||
|
message: `Invalid status code rules: ${disableParsed.invalidTokens.join(
|
||||||
|
', '
|
||||||
|
)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryParsed = parseHttpStatusCodeRules(
|
||||||
|
values.AutomaticRetryStatusCodes
|
||||||
|
)
|
||||||
|
if (!retryParsed.ok) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
path: ['AutomaticRetryStatusCodes'],
|
||||||
|
message: `Invalid status code rules: ${retryParsed.invalidTokens.join(
|
||||||
|
', '
|
||||||
|
)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
type RoutingReliabilityFormValues = z.output<typeof routingReliabilitySchema>
|
||||||
|
type RoutingReliabilityFormInput = z.input<typeof routingReliabilitySchema>
|
||||||
|
|
||||||
|
type RoutingReliabilitySectionProps = {
|
||||||
|
defaultValues: {
|
||||||
|
RetryTimes: number
|
||||||
|
ChannelDisableThreshold: string
|
||||||
|
AutomaticDisableChannelEnabled: boolean
|
||||||
|
AutomaticEnableChannelEnabled: boolean
|
||||||
|
AutomaticDisableKeywords: string
|
||||||
|
AutomaticDisableStatusCodes: string
|
||||||
|
AutomaticRetryStatusCodes: string
|
||||||
|
'monitor_setting.auto_test_channel_enabled': boolean
|
||||||
|
'monitor_setting.auto_test_channel_minutes': number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLineEndings(value: string) {
|
||||||
|
return value.replaceAll('\r\n', '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
type NormalizedRoutingReliabilityValues = {
|
||||||
|
RetryTimes: number
|
||||||
|
ChannelDisableThreshold: string
|
||||||
|
AutomaticDisableChannelEnabled: boolean
|
||||||
|
AutomaticEnableChannelEnabled: boolean
|
||||||
|
AutomaticDisableKeywords: string
|
||||||
|
AutomaticDisableStatusCodes: string
|
||||||
|
AutomaticRetryStatusCodes: string
|
||||||
|
'monitor_setting.auto_test_channel_enabled': boolean
|
||||||
|
'monitor_setting.auto_test_channel_minutes': number
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildFormDefaults = (
|
||||||
|
defaults: RoutingReliabilitySectionProps['defaultValues']
|
||||||
|
): RoutingReliabilityFormInput => ({
|
||||||
|
RetryTimes: defaults.RetryTimes ?? 0,
|
||||||
|
ChannelDisableThreshold: defaults.ChannelDisableThreshold ?? '',
|
||||||
|
AutomaticDisableChannelEnabled: defaults.AutomaticDisableChannelEnabled,
|
||||||
|
AutomaticEnableChannelEnabled: defaults.AutomaticEnableChannelEnabled,
|
||||||
|
AutomaticDisableKeywords: normalizeLineEndings(
|
||||||
|
defaults.AutomaticDisableKeywords ?? ''
|
||||||
|
),
|
||||||
|
AutomaticDisableStatusCodes: defaults.AutomaticDisableStatusCodes ?? '',
|
||||||
|
AutomaticRetryStatusCodes: defaults.AutomaticRetryStatusCodes ?? '',
|
||||||
|
monitor_setting: {
|
||||||
|
auto_test_channel_enabled:
|
||||||
|
defaults['monitor_setting.auto_test_channel_enabled'],
|
||||||
|
auto_test_channel_minutes:
|
||||||
|
defaults['monitor_setting.auto_test_channel_minutes'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeDefaults = (
|
||||||
|
defaults: RoutingReliabilitySectionProps['defaultValues']
|
||||||
|
): NormalizedRoutingReliabilityValues => ({
|
||||||
|
RetryTimes: defaults.RetryTimes ?? 0,
|
||||||
|
ChannelDisableThreshold: (defaults.ChannelDisableThreshold ?? '').trim(),
|
||||||
|
AutomaticDisableChannelEnabled: defaults.AutomaticDisableChannelEnabled,
|
||||||
|
AutomaticEnableChannelEnabled: defaults.AutomaticEnableChannelEnabled,
|
||||||
|
AutomaticDisableKeywords: normalizeLineEndings(
|
||||||
|
defaults.AutomaticDisableKeywords ?? ''
|
||||||
|
),
|
||||||
|
AutomaticDisableStatusCodes: parseHttpStatusCodeRules(
|
||||||
|
defaults.AutomaticDisableStatusCodes ?? ''
|
||||||
|
).normalized,
|
||||||
|
AutomaticRetryStatusCodes: parseHttpStatusCodeRules(
|
||||||
|
defaults.AutomaticRetryStatusCodes ?? ''
|
||||||
|
).normalized,
|
||||||
|
'monitor_setting.auto_test_channel_enabled':
|
||||||
|
defaults['monitor_setting.auto_test_channel_enabled'],
|
||||||
|
'monitor_setting.auto_test_channel_minutes':
|
||||||
|
defaults['monitor_setting.auto_test_channel_minutes'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeFormValues = (
|
||||||
|
values: RoutingReliabilityFormValues
|
||||||
|
): NormalizedRoutingReliabilityValues => ({
|
||||||
|
RetryTimes: values.RetryTimes,
|
||||||
|
ChannelDisableThreshold: values.ChannelDisableThreshold.trim(),
|
||||||
|
AutomaticDisableChannelEnabled: values.AutomaticDisableChannelEnabled,
|
||||||
|
AutomaticEnableChannelEnabled: values.AutomaticEnableChannelEnabled,
|
||||||
|
AutomaticDisableKeywords: normalizeLineEndings(
|
||||||
|
values.AutomaticDisableKeywords
|
||||||
|
),
|
||||||
|
AutomaticDisableStatusCodes: parseHttpStatusCodeRules(
|
||||||
|
values.AutomaticDisableStatusCodes
|
||||||
|
).normalized,
|
||||||
|
AutomaticRetryStatusCodes: parseHttpStatusCodeRules(
|
||||||
|
values.AutomaticRetryStatusCodes
|
||||||
|
).normalized,
|
||||||
|
'monitor_setting.auto_test_channel_enabled':
|
||||||
|
values.monitor_setting.auto_test_channel_enabled,
|
||||||
|
'monitor_setting.auto_test_channel_minutes':
|
||||||
|
values.monitor_setting.auto_test_channel_minutes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function RoutingReliabilitySection({
|
||||||
|
defaultValues,
|
||||||
|
}: RoutingReliabilitySectionProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const updateOption = useUpdateOption()
|
||||||
|
const baselineRef = useRef<NormalizedRoutingReliabilityValues>(
|
||||||
|
normalizeDefaults(defaultValues)
|
||||||
|
)
|
||||||
|
|
||||||
|
const formDefaults = useMemo(
|
||||||
|
() => buildFormDefaults(defaultValues),
|
||||||
|
[defaultValues]
|
||||||
|
)
|
||||||
|
|
||||||
|
const form = useForm<
|
||||||
|
RoutingReliabilityFormInput,
|
||||||
|
unknown,
|
||||||
|
RoutingReliabilityFormValues
|
||||||
|
>({
|
||||||
|
resolver: zodResolver(routingReliabilitySchema),
|
||||||
|
defaultValues: formDefaults,
|
||||||
|
})
|
||||||
|
|
||||||
|
useResetForm(form, formDefaults)
|
||||||
|
|
||||||
|
const autoDisableStatusCodes = form.watch('AutomaticDisableStatusCodes')
|
||||||
|
const autoRetryStatusCodes = form.watch('AutomaticRetryStatusCodes')
|
||||||
|
const autoDisableParsed = useMemo(
|
||||||
|
() => parseHttpStatusCodeRules(autoDisableStatusCodes),
|
||||||
|
[autoDisableStatusCodes]
|
||||||
|
)
|
||||||
|
const autoRetryParsed = useMemo(
|
||||||
|
() => parseHttpStatusCodeRules(autoRetryStatusCodes),
|
||||||
|
[autoRetryStatusCodes]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onSubmit = async (values: RoutingReliabilityFormValues) => {
|
||||||
|
const normalized = normalizeFormValues(values)
|
||||||
|
const updates = (
|
||||||
|
Object.keys(normalized) as Array<keyof NormalizedRoutingReliabilityValues>
|
||||||
|
).filter((key) => normalized[key] !== baselineRef.current[key])
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
toast.info(t('No changes to save'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of updates) {
|
||||||
|
const value = normalized[key]
|
||||||
|
await updateOption.mutateAsync({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
baselineRef.current = normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection title={t('Routing Reliability')}>
|
||||||
|
<Form {...form}>
|
||||||
|
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<SettingsPageFormActions
|
||||||
|
onSave={form.handleSubmit(onSubmit)}
|
||||||
|
isSaving={updateOption.isPending}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex min-w-0 flex-col gap-4'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<h4 className='text-sm font-medium'>{t('Request retry')}</h4>
|
||||||
|
</div>
|
||||||
|
<div className='grid min-w-0 gap-6 xl:grid-cols-[minmax(12rem,24rem)_minmax(0,1fr)]'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='RetryTimes'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Retry Times')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
max='10'
|
||||||
|
{...safeNumberFieldProps(field)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Number of times to retry failed requests (0-10)')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='AutomaticRetryStatusCodes'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Auto-retry status codes')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t('e.g. 401, 403, 429, 500-599')}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(event) => field.onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
'Accepts comma-separated status codes and inclusive ranges.'
|
||||||
|
)}{' '}
|
||||||
|
{autoRetryParsed.ok &&
|
||||||
|
autoRetryParsed.normalized &&
|
||||||
|
autoRetryParsed.normalized !== field.value.trim() && (
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t('Normalized:')} {autoRetryParsed.normalized}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className='flex min-w-0 flex-col gap-4'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<h4 className='text-sm font-medium'>
|
||||||
|
{t('Channel health checks')}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className='grid min-w-0 gap-6 lg:grid-cols-3'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='monitor_setting.auto_test_channel_enabled'
|
||||||
|
render={({ field }) => (
|
||||||
|
<SettingsSwitchItem>
|
||||||
|
<SettingsSwitchContent>
|
||||||
|
<FormLabel>{t('Scheduled channel tests')}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Automatically probe all channels in the background')}
|
||||||
|
</FormDescription>
|
||||||
|
</SettingsSwitchContent>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</SettingsSwitchItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='monitor_setting.auto_test_channel_minutes'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Test interval (minutes)')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
{...safeNumberFieldProps(field)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t('How frequently the system tests all channels')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='AutomaticEnableChannelEnabled'
|
||||||
|
render={({ field }) => (
|
||||||
|
<SettingsSwitchItem>
|
||||||
|
<SettingsSwitchContent>
|
||||||
|
<FormLabel>{t('Re-enable on success')}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Bring channels back online after successful checks')}
|
||||||
|
</FormDescription>
|
||||||
|
</SettingsSwitchContent>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</SettingsSwitchItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className='flex min-w-0 flex-col gap-4'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<h4 className='text-sm font-medium'>
|
||||||
|
{t('Auto-disable rules')}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className='grid min-w-0 gap-6 lg:grid-cols-2'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='AutomaticDisableChannelEnabled'
|
||||||
|
render={({ field }) => (
|
||||||
|
<SettingsSwitchItem>
|
||||||
|
<SettingsSwitchContent>
|
||||||
|
<FormLabel>{t('Disable on failure')}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Automatically disable channels when tests fail')}
|
||||||
|
</FormDescription>
|
||||||
|
</SettingsSwitchContent>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</SettingsSwitchItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='ChannelDisableThreshold'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Disable threshold (seconds)')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(event) => field.onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
'Automatically disable channels exceeding this response time'
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='AutomaticDisableStatusCodes'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Auto-disable status codes')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t('e.g. 401, 403, 429, 500-599')}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(event) => field.onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
'Accepts comma-separated status codes and inclusive ranges.'
|
||||||
|
)}{' '}
|
||||||
|
{autoDisableParsed.ok &&
|
||||||
|
autoDisableParsed.normalized &&
|
||||||
|
autoDisableParsed.normalized !== field.value.trim() && (
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t('Normalized:')} {autoDisableParsed.normalized}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='AutomaticDisableKeywords'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Failure keywords')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
rows={6}
|
||||||
|
placeholder={t('one keyword per line')}
|
||||||
|
{...field}
|
||||||
|
onChange={(event) => field.onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
'If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.'
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsForm>
|
||||||
|
</Form>
|
||||||
|
</SettingsSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ 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 { RoutingReliabilitySection } from './routing-reliability-section'
|
||||||
|
|
||||||
function formatJsonForEditor(value: string, fallback: string) {
|
function formatJsonForEditor(value: string, fallback: string) {
|
||||||
const raw = (value ?? '').toString().trim()
|
const raw = (value ?? '').toString().trim()
|
||||||
@@ -64,6 +65,28 @@ const MODELS_SECTIONS = [
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'routing-reliability',
|
||||||
|
titleKey: 'Routing Reliability',
|
||||||
|
build: (settings: ModelSettings) => (
|
||||||
|
<RoutingReliabilitySection
|
||||||
|
defaultValues={{
|
||||||
|
RetryTimes: settings.RetryTimes,
|
||||||
|
ChannelDisableThreshold: settings.ChannelDisableThreshold,
|
||||||
|
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: 'gemini',
|
id: 'gemini',
|
||||||
titleKey: 'Gemini',
|
titleKey: 'Gemini',
|
||||||
|
|||||||
@@ -26,20 +26,10 @@ import {
|
|||||||
} from './section-registry.tsx'
|
} from './section-registry.tsx'
|
||||||
|
|
||||||
const defaultOperationsSettings: OperationsSettings = {
|
const defaultOperationsSettings: OperationsSettings = {
|
||||||
RetryTimes: 0,
|
|
||||||
DefaultCollapseSidebar: false,
|
DefaultCollapseSidebar: false,
|
||||||
DemoSiteEnabled: false,
|
DemoSiteEnabled: false,
|
||||||
SelfUseModeEnabled: false,
|
SelfUseModeEnabled: false,
|
||||||
ChannelDisableThreshold: '',
|
|
||||||
QuotaRemindThreshold: '',
|
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: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
SMTPAccount: '',
|
SMTPAccount: '',
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const OPERATIONS_SECTIONS = [
|
|||||||
build: (settings: OperationsSettings) => (
|
build: (settings: OperationsSettings) => (
|
||||||
<SystemBehaviorSection
|
<SystemBehaviorSection
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
RetryTimes: settings.RetryTimes,
|
|
||||||
DefaultCollapseSidebar: settings.DefaultCollapseSidebar,
|
DefaultCollapseSidebar: settings.DefaultCollapseSidebar,
|
||||||
DemoSiteEnabled: settings.DemoSiteEnabled,
|
DemoSiteEnabled: settings.DemoSiteEnabled,
|
||||||
SelfUseModeEnabled: settings.SelfUseModeEnabled,
|
SelfUseModeEnabled: settings.SelfUseModeEnabled,
|
||||||
@@ -42,23 +41,20 @@ const OPERATIONS_SECTIONS = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'monitoring',
|
id: 'alerts',
|
||||||
titleKey: 'Monitoring & Alerts',
|
titleKey: 'Monitoring & Alerts',
|
||||||
build: (settings: OperationsSettings) => (
|
build: (settings: OperationsSettings) => (
|
||||||
<MonitoringSettingsSection
|
<MonitoringSettingsSection
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
ChannelDisableThreshold: settings.ChannelDisableThreshold,
|
|
||||||
QuotaRemindThreshold: settings.QuotaRemindThreshold,
|
QuotaRemindThreshold: settings.QuotaRemindThreshold,
|
||||||
AutomaticDisableChannelEnabled:
|
'perf_metrics_setting.enabled':
|
||||||
settings.AutomaticDisableChannelEnabled,
|
settings['perf_metrics_setting.enabled'] ?? true,
|
||||||
AutomaticEnableChannelEnabled: settings.AutomaticEnableChannelEnabled,
|
'perf_metrics_setting.flush_interval':
|
||||||
AutomaticDisableKeywords: settings.AutomaticDisableKeywords,
|
settings['perf_metrics_setting.flush_interval'] ?? 5,
|
||||||
AutomaticDisableStatusCodes: settings.AutomaticDisableStatusCodes,
|
'perf_metrics_setting.bucket_time':
|
||||||
AutomaticRetryStatusCodes: settings.AutomaticRetryStatusCodes,
|
settings['perf_metrics_setting.bucket_time'] ?? 'hour',
|
||||||
'monitor_setting.auto_test_channel_enabled':
|
'perf_metrics_setting.retention_days':
|
||||||
settings['monitor_setting.auto_test_channel_enabled'],
|
settings['perf_metrics_setting.retention_days'] ?? 0,
|
||||||
'monitor_setting.auto_test_channel_minutes':
|
|
||||||
settings['monitor_setting.auto_test_channel_minutes'],
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -125,14 +121,6 @@ const OPERATIONS_SECTIONS = [
|
|||||||
settings['performance_setting.monitor_memory_threshold'] ?? 90,
|
settings['performance_setting.monitor_memory_threshold'] ?? 90,
|
||||||
'performance_setting.monitor_disk_threshold':
|
'performance_setting.monitor_disk_threshold':
|
||||||
settings['performance_setting.monitor_disk_threshold'] ?? 95,
|
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,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
+9
-9
@@ -174,6 +174,15 @@ export type ModelSettings = {
|
|||||||
AutoGroups: string
|
AutoGroups: string
|
||||||
DefaultUseAutoGroup: boolean
|
DefaultUseAutoGroup: boolean
|
||||||
'group_ratio_setting.group_special_usable_group': string
|
'group_ratio_setting.group_special_usable_group': string
|
||||||
|
RetryTimes: number
|
||||||
|
ChannelDisableThreshold: string
|
||||||
|
AutomaticDisableChannelEnabled: boolean
|
||||||
|
AutomaticEnableChannelEnabled: boolean
|
||||||
|
AutomaticDisableKeywords: string
|
||||||
|
AutomaticDisableStatusCodes: string
|
||||||
|
AutomaticRetryStatusCodes: string
|
||||||
|
'monitor_setting.auto_test_channel_enabled': boolean
|
||||||
|
'monitor_setting.auto_test_channel_minutes': number
|
||||||
'channel_affinity_setting.enabled': boolean
|
'channel_affinity_setting.enabled': boolean
|
||||||
'channel_affinity_setting.switch_on_success': boolean
|
'channel_affinity_setting.switch_on_success': boolean
|
||||||
'channel_affinity_setting.keep_on_channel_disabled': boolean
|
'channel_affinity_setting.keep_on_channel_disabled': boolean
|
||||||
@@ -270,19 +279,10 @@ export type BillingSettings = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type OperationsSettings = {
|
export type OperationsSettings = {
|
||||||
RetryTimes: number
|
|
||||||
DefaultCollapseSidebar: boolean
|
DefaultCollapseSidebar: boolean
|
||||||
DemoSiteEnabled: boolean
|
DemoSiteEnabled: boolean
|
||||||
SelfUseModeEnabled: boolean
|
SelfUseModeEnabled: boolean
|
||||||
ChannelDisableThreshold: string
|
|
||||||
QuotaRemindThreshold: string
|
QuotaRemindThreshold: string
|
||||||
AutomaticDisableChannelEnabled: boolean
|
|
||||||
AutomaticEnableChannelEnabled: boolean
|
|
||||||
AutomaticDisableKeywords: string
|
|
||||||
AutomaticDisableStatusCodes: string
|
|
||||||
AutomaticRetryStatusCodes: string
|
|
||||||
'monitor_setting.auto_test_channel_enabled': boolean
|
|
||||||
'monitor_setting.auto_test_channel_minutes': number
|
|
||||||
SMTPServer: string
|
SMTPServer: string
|
||||||
SMTPPort: string
|
SMTPPort: string
|
||||||
SMTPAccount: string
|
SMTPAccount: string
|
||||||
|
|||||||
Vendored
+6
@@ -473,6 +473,7 @@
|
|||||||
"Auto Group Chain": "Auto Group Chain",
|
"Auto Group Chain": "Auto Group Chain",
|
||||||
"Auto refresh": "Auto refresh",
|
"Auto refresh": "Auto refresh",
|
||||||
"Auto Sync Upstream Models": "Auto Sync Upstream Models",
|
"Auto Sync Upstream Models": "Auto Sync Upstream Models",
|
||||||
|
"Auto-disable rules": "Auto-disable rules",
|
||||||
"Auto-disable status codes": "Auto-disable status codes",
|
"Auto-disable status codes": "Auto-disable status codes",
|
||||||
"Auto-discover": "Auto-discover",
|
"Auto-discover": "Auto-discover",
|
||||||
"Auto-discovers endpoints from the provider": "Auto-discovers endpoints from the provider",
|
"Auto-discovers endpoints from the provider": "Auto-discovers endpoints from the provider",
|
||||||
@@ -683,6 +684,7 @@
|
|||||||
"Channel Affinity": "Channel Affinity",
|
"Channel Affinity": "Channel Affinity",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit",
|
"Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit",
|
||||||
|
"Channel health checks": "Channel health checks",
|
||||||
"Channel copied successfully": "Channel copied successfully",
|
"Channel copied successfully": "Channel copied successfully",
|
||||||
"Channel created successfully": "Channel created successfully",
|
"Channel created successfully": "Channel created successfully",
|
||||||
"Channel deleted successfully": "Channel deleted successfully",
|
"Channel deleted successfully": "Channel deleted successfully",
|
||||||
@@ -2391,6 +2393,7 @@
|
|||||||
"Max Disk Cache Size (MB)": "Max Disk Cache Size (MB)",
|
"Max Disk Cache Size (MB)": "Max Disk Cache Size (MB)",
|
||||||
"Max Entries": "Max Entries",
|
"Max Entries": "Max Entries",
|
||||||
"Max output": "Max output",
|
"Max output": "Max output",
|
||||||
|
"Max Retries": "Max Retries",
|
||||||
"Max Requests (incl. failures)": "Max Requests (incl. failures)",
|
"Max Requests (incl. failures)": "Max Requests (incl. failures)",
|
||||||
"Max Requests (including failures)": "Max Requests (including failures)",
|
"Max Requests (including failures)": "Max Requests (including failures)",
|
||||||
"Max requests per period": "Max requests per period",
|
"Max requests per period": "Max requests per period",
|
||||||
@@ -3541,11 +3544,13 @@
|
|||||||
"Restore defaults": "Restore defaults",
|
"Restore defaults": "Restore defaults",
|
||||||
"Restrict user model request frequency (may impact high concurrency performance)": "Restrict user model request frequency (may impact high concurrency performance)",
|
"Restrict user model request frequency (may impact high concurrency performance)": "Restrict user model request frequency (may impact high concurrency performance)",
|
||||||
"Result": "Result",
|
"Result": "Result",
|
||||||
|
"Request retry": "Request retry",
|
||||||
"Retain last N days": "Retain last N days",
|
"Retain last N days": "Retain last N days",
|
||||||
"Retain last N files": "Retain last N files",
|
"Retain last N files": "Retain last N files",
|
||||||
"Retention days": "Retention days",
|
"Retention days": "Retention days",
|
||||||
"Retry": "Retry",
|
"Retry": "Retry",
|
||||||
"Retry Chain": "Retry Chain",
|
"Retry Chain": "Retry Chain",
|
||||||
|
"Retry Settings": "Retry Settings",
|
||||||
"Retry Suggestion": "Retry Suggestion",
|
"Retry Suggestion": "Retry Suggestion",
|
||||||
"Retry Times": "Retry Times",
|
"Retry Times": "Retry Times",
|
||||||
"Return a custom error immediately": "Return a custom error immediately",
|
"Return a custom error immediately": "Return a custom error immediately",
|
||||||
@@ -3576,6 +3581,7 @@
|
|||||||
"Route, auth, and balance check in one place": "Route, auth, and balance check in one place",
|
"Route, auth, and balance check in one place": "Route, auth, and balance check in one place",
|
||||||
"Routes": "Routes",
|
"Routes": "Routes",
|
||||||
"Routing & Overrides": "Routing & Overrides",
|
"Routing & Overrides": "Routing & Overrides",
|
||||||
|
"Routing Reliability": "Routing Reliability",
|
||||||
"Routing Strategy": "Routing Strategy",
|
"Routing Strategy": "Routing Strategy",
|
||||||
"Rows per page": "Rows per page",
|
"Rows per page": "Rows per page",
|
||||||
"RPM": "RPM",
|
"RPM": "RPM",
|
||||||
|
|||||||
Vendored
+6
@@ -473,6 +473,7 @@
|
|||||||
"Auto Group Chain": "Chaîne de groupes automatique",
|
"Auto Group Chain": "Chaîne de groupes automatique",
|
||||||
"Auto refresh": "Actualisation automatique",
|
"Auto refresh": "Actualisation automatique",
|
||||||
"Auto Sync Upstream Models": "Synchronisation automatique des modèles en amont",
|
"Auto Sync Upstream Models": "Synchronisation automatique des modèles en amont",
|
||||||
|
"Auto-disable rules": "Règles de désactivation automatique",
|
||||||
"Auto-disable status codes": "Codes de statut de désactivation auto",
|
"Auto-disable status codes": "Codes de statut de désactivation auto",
|
||||||
"Auto-discover": "Découverte automatique",
|
"Auto-discover": "Découverte automatique",
|
||||||
"Auto-discovers endpoints from the provider": "Découvre automatiquement les points de terminaison du fournisseur",
|
"Auto-discovers endpoints from the provider": "Découvre automatiquement les points de terminaison du fournisseur",
|
||||||
@@ -683,6 +684,7 @@
|
|||||||
"Channel Affinity": "Affinité de canal",
|
"Channel Affinity": "Affinité de canal",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont",
|
"Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont",
|
||||||
|
"Channel health checks": "Contrôles de santé des canaux",
|
||||||
"Channel copied successfully": "Canal copié avec succès",
|
"Channel copied successfully": "Canal copié avec succès",
|
||||||
"Channel created successfully": "Canal créé avec succès",
|
"Channel created successfully": "Canal créé avec succès",
|
||||||
"Channel deleted successfully": "Canal supprimé avec succès",
|
"Channel deleted successfully": "Canal supprimé avec succès",
|
||||||
@@ -2391,6 +2393,7 @@
|
|||||||
"Max Disk Cache Size (MB)": "Taille max du cache disque (Mo)",
|
"Max Disk Cache Size (MB)": "Taille max du cache disque (Mo)",
|
||||||
"Max Entries": "Entrées max",
|
"Max Entries": "Entrées max",
|
||||||
"Max output": "Sortie max",
|
"Max output": "Sortie max",
|
||||||
|
"Max Retries": "Relances max",
|
||||||
"Max Requests (incl. failures)": "Max Requêtes (incl. échecs)",
|
"Max Requests (incl. failures)": "Max Requêtes (incl. échecs)",
|
||||||
"Max Requests (including failures)": "Max Requêtes (incluant les échecs)",
|
"Max Requests (including failures)": "Max Requêtes (incluant les échecs)",
|
||||||
"Max requests per period": "Nombre max de requêtes par période",
|
"Max requests per period": "Nombre max de requêtes par période",
|
||||||
@@ -3541,11 +3544,13 @@
|
|||||||
"Restore defaults": "Restaurer les paramètres par défaut",
|
"Restore defaults": "Restaurer les paramètres par défaut",
|
||||||
"Restrict user model request frequency (may impact high concurrency performance)": "Restreindre la fréquence des requêtes du modèle utilisateur (peut impacter les performances en cas de forte concurrence)",
|
"Restrict user model request frequency (may impact high concurrency performance)": "Restreindre la fréquence des requêtes du modèle utilisateur (peut impacter les performances en cas de forte concurrence)",
|
||||||
"Result": "Résultat",
|
"Result": "Résultat",
|
||||||
|
"Request retry": "Relance des requêtes",
|
||||||
"Retain last N days": "Conserver les N derniers jours",
|
"Retain last N days": "Conserver les N derniers jours",
|
||||||
"Retain last N files": "Conserver les N derniers fichiers",
|
"Retain last N files": "Conserver les N derniers fichiers",
|
||||||
"Retention days": "Jours de rétention",
|
"Retention days": "Jours de rétention",
|
||||||
"Retry": "Réessayer",
|
"Retry": "Réessayer",
|
||||||
"Retry Chain": "Chaîne de tentatives",
|
"Retry Chain": "Chaîne de tentatives",
|
||||||
|
"Retry Settings": "Paramètres de relance",
|
||||||
"Retry Suggestion": "Suggestion de relance",
|
"Retry Suggestion": "Suggestion de relance",
|
||||||
"Retry Times": "Nombre de tentatives",
|
"Retry Times": "Nombre de tentatives",
|
||||||
"Return a custom error immediately": "Retourner immédiatement une erreur personnalisée",
|
"Return a custom error immediately": "Retourner immédiatement une erreur personnalisée",
|
||||||
@@ -3576,6 +3581,7 @@
|
|||||||
"Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit",
|
"Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit",
|
||||||
"Routes": "Routes",
|
"Routes": "Routes",
|
||||||
"Routing & Overrides": "Routage et surcharges",
|
"Routing & Overrides": "Routage et surcharges",
|
||||||
|
"Routing Reliability": "Fiabilité du routage",
|
||||||
"Routing Strategy": "Stratégie de routage",
|
"Routing Strategy": "Stratégie de routage",
|
||||||
"Rows per page": "Lignes par page",
|
"Rows per page": "Lignes par page",
|
||||||
"RPM": "RPM",
|
"RPM": "RPM",
|
||||||
|
|||||||
Vendored
+6
@@ -473,6 +473,7 @@
|
|||||||
"Auto Group Chain": "自動グループチェーン",
|
"Auto Group Chain": "自動グループチェーン",
|
||||||
"Auto refresh": "自動更新",
|
"Auto refresh": "自動更新",
|
||||||
"Auto Sync Upstream Models": "アップストリームモデルの自動同期",
|
"Auto Sync Upstream Models": "アップストリームモデルの自動同期",
|
||||||
|
"Auto-disable rules": "自動無効化ルール",
|
||||||
"Auto-disable status codes": "自動無効化するステータスコード",
|
"Auto-disable status codes": "自動無効化するステータスコード",
|
||||||
"Auto-discover": "自動検出",
|
"Auto-discover": "自動検出",
|
||||||
"Auto-discovers endpoints from the provider": "プロバイダーからエンドポイントを自動検出します",
|
"Auto-discovers endpoints from the provider": "プロバイダーからエンドポイントを自動検出します",
|
||||||
@@ -683,6 +684,7 @@
|
|||||||
"Channel Affinity": "チャネルアフィニティ",
|
"Channel Affinity": "チャネルアフィニティ",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。",
|
||||||
"Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット",
|
"Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット",
|
||||||
|
"Channel health checks": "チャネルヘルスチェック",
|
||||||
"Channel copied successfully": "チャネルが正常にコピーされました",
|
"Channel copied successfully": "チャネルが正常にコピーされました",
|
||||||
"Channel created successfully": "チャネルが正常に作成されました",
|
"Channel created successfully": "チャネルが正常に作成されました",
|
||||||
"Channel deleted successfully": "チャネルが正常に削除されました",
|
"Channel deleted successfully": "チャネルが正常に削除されました",
|
||||||
@@ -2391,6 +2393,7 @@
|
|||||||
"Max Disk Cache Size (MB)": "ディスクキャッシュ最大容量 (MB)",
|
"Max Disk Cache Size (MB)": "ディスクキャッシュ最大容量 (MB)",
|
||||||
"Max Entries": "最大エントリ数",
|
"Max Entries": "最大エントリ数",
|
||||||
"Max output": "最大出力",
|
"Max output": "最大出力",
|
||||||
|
"Max Retries": "最大再試行",
|
||||||
"Max Requests (incl. failures)": "最大リクエスト数(失敗を含む)",
|
"Max Requests (incl. failures)": "最大リクエスト数(失敗を含む)",
|
||||||
"Max Requests (including failures)": "最大リクエスト数(失敗を含む)",
|
"Max Requests (including failures)": "最大リクエスト数(失敗を含む)",
|
||||||
"Max requests per period": "期間あたりの最大リクエスト数",
|
"Max requests per period": "期間あたりの最大リクエスト数",
|
||||||
@@ -3541,11 +3544,13 @@
|
|||||||
"Restore defaults": "既定に戻す",
|
"Restore defaults": "既定に戻す",
|
||||||
"Restrict user model request frequency (may impact high concurrency performance)": "ユーザーモデルのリクエスト頻度を制限する(高並行性パフォーマンスに影響を与える可能性があります)",
|
"Restrict user model request frequency (may impact high concurrency performance)": "ユーザーモデルのリクエスト頻度を制限する(高並行性パフォーマンスに影響を与える可能性があります)",
|
||||||
"Result": "結果",
|
"Result": "結果",
|
||||||
|
"Request retry": "リクエスト再試行",
|
||||||
"Retain last N days": "最新N日間を保持",
|
"Retain last N days": "最新N日間を保持",
|
||||||
"Retain last N files": "最新N個のファイルを保持",
|
"Retain last N files": "最新N個のファイルを保持",
|
||||||
"Retention days": "保持日数",
|
"Retention days": "保持日数",
|
||||||
"Retry": "再試行",
|
"Retry": "再試行",
|
||||||
"Retry Chain": "リトライチェーン",
|
"Retry Chain": "リトライチェーン",
|
||||||
|
"Retry Settings": "再試行設定",
|
||||||
"Retry Suggestion": "リトライ提案",
|
"Retry Suggestion": "リトライ提案",
|
||||||
"Retry Times": "再試行回数",
|
"Retry Times": "再試行回数",
|
||||||
"Return a custom error immediately": "即座にカスタムエラーを返す",
|
"Return a custom error immediately": "即座にカスタムエラーを返す",
|
||||||
@@ -3576,6 +3581,7 @@
|
|||||||
"Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約",
|
"Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約",
|
||||||
"Routes": "ルート",
|
"Routes": "ルート",
|
||||||
"Routing & Overrides": "ルーティングと上書き",
|
"Routing & Overrides": "ルーティングと上書き",
|
||||||
|
"Routing Reliability": "ルーティング信頼性",
|
||||||
"Routing Strategy": "ルーティング戦略",
|
"Routing Strategy": "ルーティング戦略",
|
||||||
"Rows per page": "ページあたりの行数",
|
"Rows per page": "ページあたりの行数",
|
||||||
"RPM": "RPM",
|
"RPM": "RPM",
|
||||||
|
|||||||
Vendored
+6
@@ -473,6 +473,7 @@
|
|||||||
"Auto Group Chain": "Автоматическая цепочка групп",
|
"Auto Group Chain": "Автоматическая цепочка групп",
|
||||||
"Auto refresh": "Автообновление",
|
"Auto refresh": "Автообновление",
|
||||||
"Auto Sync Upstream Models": "Автоматическая синхронизация моделей провайдера",
|
"Auto Sync Upstream Models": "Автоматическая синхронизация моделей провайдера",
|
||||||
|
"Auto-disable rules": "Правила автоотключения",
|
||||||
"Auto-disable status codes": "Коды автоотключения",
|
"Auto-disable status codes": "Коды автоотключения",
|
||||||
"Auto-discover": "Автообнаружение",
|
"Auto-discover": "Автообнаружение",
|
||||||
"Auto-discovers endpoints from the provider": "Автоматически обнаруживает конечные точки от провайдера",
|
"Auto-discovers endpoints from the provider": "Автоматически обнаруживает конечные точки от провайдера",
|
||||||
@@ -683,6 +684,7 @@
|
|||||||
"Channel Affinity": "Привязка к каналу",
|
"Channel Affinity": "Привязка к каналу",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream",
|
"Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream",
|
||||||
|
"Channel health checks": "Проверки состояния каналов",
|
||||||
"Channel copied successfully": "Канал успешно скопирован",
|
"Channel copied successfully": "Канал успешно скопирован",
|
||||||
"Channel created successfully": "Канал успешно создан",
|
"Channel created successfully": "Канал успешно создан",
|
||||||
"Channel deleted successfully": "Канал успешно удалён",
|
"Channel deleted successfully": "Канал успешно удалён",
|
||||||
@@ -2391,6 +2393,7 @@
|
|||||||
"Max Disk Cache Size (MB)": "Макс. размер дискового кэша (МБ)",
|
"Max Disk Cache Size (MB)": "Макс. размер дискового кэша (МБ)",
|
||||||
"Max Entries": "Макс. записей",
|
"Max Entries": "Макс. записей",
|
||||||
"Max output": "Макс. вывод",
|
"Max output": "Макс. вывод",
|
||||||
|
"Max Retries": "Макс. повторов",
|
||||||
"Max Requests (incl. failures)": "Макс. запросов (вкл. сбои)",
|
"Max Requests (incl. failures)": "Макс. запросов (вкл. сбои)",
|
||||||
"Max Requests (including failures)": "Макс. запросов (включая сбои)",
|
"Max Requests (including failures)": "Макс. запросов (включая сбои)",
|
||||||
"Max requests per period": "Макс. запросов за период",
|
"Max requests per period": "Макс. запросов за период",
|
||||||
@@ -3541,11 +3544,13 @@
|
|||||||
"Restore defaults": "Сбросить к значениям по умолчанию",
|
"Restore defaults": "Сбросить к значениям по умолчанию",
|
||||||
"Restrict user model request frequency (may impact high concurrency performance)": "Ограничить частоту запросов пользовательских моделей (может повлиять на производительность при высокой конкуренции)",
|
"Restrict user model request frequency (may impact high concurrency performance)": "Ограничить частоту запросов пользовательских моделей (может повлиять на производительность при высокой конкуренции)",
|
||||||
"Result": "Результат",
|
"Result": "Результат",
|
||||||
|
"Request retry": "Повтор запросов",
|
||||||
"Retain last N days": "Хранить последние N дней",
|
"Retain last N days": "Хранить последние N дней",
|
||||||
"Retain last N files": "Хранить последние N файлов",
|
"Retain last N files": "Хранить последние N файлов",
|
||||||
"Retention days": "Дней хранения",
|
"Retention days": "Дней хранения",
|
||||||
"Retry": "Повторить попытку",
|
"Retry": "Повторить попытку",
|
||||||
"Retry Chain": "Цепочка повторов",
|
"Retry Chain": "Цепочка повторов",
|
||||||
|
"Retry Settings": "Настройки повторов",
|
||||||
"Retry Suggestion": "Рекомендация по повтору",
|
"Retry Suggestion": "Рекомендация по повтору",
|
||||||
"Retry Times": "Количество повторных попыток",
|
"Retry Times": "Количество повторных попыток",
|
||||||
"Return a custom error immediately": "Немедленно вернуть пользовательскую ошибку",
|
"Return a custom error immediately": "Немедленно вернуть пользовательскую ошибку",
|
||||||
@@ -3576,6 +3581,7 @@
|
|||||||
"Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте",
|
"Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте",
|
||||||
"Routes": "Маршруты",
|
"Routes": "Маршруты",
|
||||||
"Routing & Overrides": "Маршрутизация и переопределения",
|
"Routing & Overrides": "Маршрутизация и переопределения",
|
||||||
|
"Routing Reliability": "Надежность маршрутизации",
|
||||||
"Routing Strategy": "Стратегия маршрутизации",
|
"Routing Strategy": "Стратегия маршрутизации",
|
||||||
"Rows per page": "Строк на страницу",
|
"Rows per page": "Строк на страницу",
|
||||||
"RPM": "RPM",
|
"RPM": "RPM",
|
||||||
|
|||||||
Vendored
+6
@@ -473,6 +473,7 @@
|
|||||||
"Auto Group Chain": "Chuỗi nhóm tự động",
|
"Auto Group Chain": "Chuỗi nhóm tự động",
|
||||||
"Auto refresh": "Tự động làm mới",
|
"Auto refresh": "Tự động làm mới",
|
||||||
"Auto Sync Upstream Models": "Tự động đồng bộ mô hình nguồn",
|
"Auto Sync Upstream Models": "Tự động đồng bộ mô hình nguồn",
|
||||||
|
"Auto-disable rules": "Quy tắc tự động tắt",
|
||||||
"Auto-disable status codes": "Mã trạng thái tự tắt",
|
"Auto-disable status codes": "Mã trạng thái tự tắt",
|
||||||
"Auto-discover": "Tự động khám phá",
|
"Auto-discover": "Tự động khám phá",
|
||||||
"Auto-discovers endpoints from the provider": "Tự động khám phá các điểm cuối từ nhà cung cấp",
|
"Auto-discovers endpoints from the provider": "Tự động khám phá các điểm cuối từ nhà cung cấp",
|
||||||
@@ -683,6 +684,7 @@
|
|||||||
"Channel Affinity": "Ưu tiên kênh",
|
"Channel Affinity": "Ưu tiên kênh",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream",
|
"Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream",
|
||||||
|
"Channel health checks": "Kiểm tra tình trạng kênh",
|
||||||
"Channel copied successfully": "Sao chép kênh thành công",
|
"Channel copied successfully": "Sao chép kênh thành công",
|
||||||
"Channel created successfully": "Tạo kênh thành công",
|
"Channel created successfully": "Tạo kênh thành công",
|
||||||
"Channel deleted successfully": "Xóa kênh thành công",
|
"Channel deleted successfully": "Xóa kênh thành công",
|
||||||
@@ -2391,6 +2393,7 @@
|
|||||||
"Max Disk Cache Size (MB)": "Dung lượng tối đa bộ nhớ đệm đĩa (MB)",
|
"Max Disk Cache Size (MB)": "Dung lượng tối đa bộ nhớ đệm đĩa (MB)",
|
||||||
"Max Entries": "Số mục tối đa",
|
"Max Entries": "Số mục tối đa",
|
||||||
"Max output": "Đầu ra tối đa",
|
"Max output": "Đầu ra tối đa",
|
||||||
|
"Max Retries": "Số lần thử lại tối đa",
|
||||||
"Max Requests (incl. failures)": "Maximum number of requests (including errors)",
|
"Max Requests (incl. failures)": "Maximum number of requests (including errors)",
|
||||||
"Max Requests (including failures)": "Số yêu cầu tối đa (bao gồm cả các lỗi)",
|
"Max Requests (including failures)": "Số yêu cầu tối đa (bao gồm cả các lỗi)",
|
||||||
"Max requests per period": "Yêu cầu tối đa mỗi khoảng thời gian",
|
"Max requests per period": "Yêu cầu tối đa mỗi khoảng thời gian",
|
||||||
@@ -3541,11 +3544,13 @@
|
|||||||
"Restore defaults": "Khôi phục mặc định",
|
"Restore defaults": "Khôi phục mặc định",
|
||||||
"Restrict user model request frequency (may impact high concurrency performance)": "Hạn chế tần suất yêu cầu mô hình người dùng (có thể ảnh hưởng đến hiệu suất khi có độ đồng thời cao)",
|
"Restrict user model request frequency (may impact high concurrency performance)": "Hạn chế tần suất yêu cầu mô hình người dùng (có thể ảnh hưởng đến hiệu suất khi có độ đồng thời cao)",
|
||||||
"Result": "Kết quả",
|
"Result": "Kết quả",
|
||||||
|
"Request retry": "Thử lại yêu cầu",
|
||||||
"Retain last N days": "Giữ lại N ngày gần nhất",
|
"Retain last N days": "Giữ lại N ngày gần nhất",
|
||||||
"Retain last N files": "Giữ lại N tệp gần nhất",
|
"Retain last N files": "Giữ lại N tệp gần nhất",
|
||||||
"Retention days": "Số ngày lưu giữ",
|
"Retention days": "Số ngày lưu giữ",
|
||||||
"Retry": "Thử lại",
|
"Retry": "Thử lại",
|
||||||
"Retry Chain": "Chuỗi thử lại",
|
"Retry Chain": "Chuỗi thử lại",
|
||||||
|
"Retry Settings": "Cài đặt thử lại",
|
||||||
"Retry Suggestion": "Gợi ý thử lại",
|
"Retry Suggestion": "Gợi ý thử lại",
|
||||||
"Retry Times": "Số lần thử lại",
|
"Retry Times": "Số lần thử lại",
|
||||||
"Return a custom error immediately": "Trả về lỗi tùy chỉnh ngay lập tức",
|
"Return a custom error immediately": "Trả về lỗi tùy chỉnh ngay lập tức",
|
||||||
@@ -3576,6 +3581,7 @@
|
|||||||
"Route, auth, and balance check in one place": "Kiểm tra tuyến, xác thực và số dư ở cùng một nơi",
|
"Route, auth, and balance check in one place": "Kiểm tra tuyến, xác thực và số dư ở cùng một nơi",
|
||||||
"Routes": "Route",
|
"Routes": "Route",
|
||||||
"Routing & Overrides": "Định tuyến & ghi đè",
|
"Routing & Overrides": "Định tuyến & ghi đè",
|
||||||
|
"Routing Reliability": "Độ tin cậy định tuyến",
|
||||||
"Routing Strategy": "Chiến lược định tuyến",
|
"Routing Strategy": "Chiến lược định tuyến",
|
||||||
"Rows per page": "Số hàng trên trang",
|
"Rows per page": "Số hàng trên trang",
|
||||||
"RPM": "RPM",
|
"RPM": "RPM",
|
||||||
|
|||||||
Vendored
+6
@@ -473,6 +473,7 @@
|
|||||||
"Auto Group Chain": "自动分组链",
|
"Auto Group Chain": "自动分组链",
|
||||||
"Auto refresh": "自动刷新",
|
"Auto refresh": "自动刷新",
|
||||||
"Auto Sync Upstream Models": "自动同步上游模型",
|
"Auto Sync Upstream Models": "自动同步上游模型",
|
||||||
|
"Auto-disable rules": "自动禁用规则",
|
||||||
"Auto-disable status codes": "自动禁用状态码",
|
"Auto-disable status codes": "自动禁用状态码",
|
||||||
"Auto-discover": "自动发现",
|
"Auto-discover": "自动发现",
|
||||||
"Auto-discovers endpoints from the provider": "自动从提供商发现端点",
|
"Auto-discovers endpoints from the provider": "自动从提供商发现端点",
|
||||||
@@ -683,6 +684,7 @@
|
|||||||
"Channel Affinity": "渠道亲和性",
|
"Channel Affinity": "渠道亲和性",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。",
|
||||||
"Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中",
|
"Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中",
|
||||||
|
"Channel health checks": "渠道健康检查",
|
||||||
"Channel copied successfully": "渠道复制成功",
|
"Channel copied successfully": "渠道复制成功",
|
||||||
"Channel created successfully": "渠道创建成功",
|
"Channel created successfully": "渠道创建成功",
|
||||||
"Channel deleted successfully": "渠道删除成功",
|
"Channel deleted successfully": "渠道删除成功",
|
||||||
@@ -2391,6 +2393,7 @@
|
|||||||
"Max Disk Cache Size (MB)": "磁盘缓存最大总量 (MB)",
|
"Max Disk Cache Size (MB)": "磁盘缓存最大总量 (MB)",
|
||||||
"Max Entries": "最大条目数",
|
"Max Entries": "最大条目数",
|
||||||
"Max output": "最大输出",
|
"Max output": "最大输出",
|
||||||
|
"Max Retries": "最大重试",
|
||||||
"Max Requests (incl. failures)": "最大请求数(包括失败)",
|
"Max Requests (incl. failures)": "最大请求数(包括失败)",
|
||||||
"Max Requests (including failures)": "最大请求数(包括失败)",
|
"Max Requests (including failures)": "最大请求数(包括失败)",
|
||||||
"Max requests per period": "每周期最大请求数",
|
"Max requests per period": "每周期最大请求数",
|
||||||
@@ -3541,11 +3544,13 @@
|
|||||||
"Restore defaults": "恢复默认",
|
"Restore defaults": "恢复默认",
|
||||||
"Restrict user model request frequency (may impact high concurrency performance)": "限制用户模型请求频率(可能会影响高并发性能)",
|
"Restrict user model request frequency (may impact high concurrency performance)": "限制用户模型请求频率(可能会影响高并发性能)",
|
||||||
"Result": "结果",
|
"Result": "结果",
|
||||||
|
"Request retry": "请求重试",
|
||||||
"Retain last N days": "保留最近N天",
|
"Retain last N days": "保留最近N天",
|
||||||
"Retain last N files": "保留最近 N 个文件",
|
"Retain last N files": "保留最近 N 个文件",
|
||||||
"Retention days": "保留天数",
|
"Retention days": "保留天数",
|
||||||
"Retry": "重试",
|
"Retry": "重试",
|
||||||
"Retry Chain": "重试链路",
|
"Retry Chain": "重试链路",
|
||||||
|
"Retry Settings": "重试设置",
|
||||||
"Retry Suggestion": "重试建议",
|
"Retry Suggestion": "重试建议",
|
||||||
"Retry Times": "重试次数",
|
"Retry Times": "重试次数",
|
||||||
"Return a custom error immediately": "立即返回自定义错误",
|
"Return a custom error immediately": "立即返回自定义错误",
|
||||||
@@ -3576,6 +3581,7 @@
|
|||||||
"Route, auth, and balance check in one place": "路由、认证和余额检查集中展示",
|
"Route, auth, and balance check in one place": "路由、认证和余额检查集中展示",
|
||||||
"Routes": "路由",
|
"Routes": "路由",
|
||||||
"Routing & Overrides": "路由与覆盖",
|
"Routing & Overrides": "路由与覆盖",
|
||||||
|
"Routing Reliability": "路由可靠性",
|
||||||
"Routing Strategy": "路由策略",
|
"Routing Strategy": "路由策略",
|
||||||
"Rows per page": "每页行数",
|
"Rows per page": "每页行数",
|
||||||
"RPM": "RPM",
|
"RPM": "RPM",
|
||||||
|
|||||||
Vendored
+4
@@ -30,6 +30,9 @@ export const STATIC_I18N_KEYS = [
|
|||||||
// Sidebar views (drill-in workspaces)
|
// Sidebar views (drill-in workspaces)
|
||||||
'System Settings',
|
'System Settings',
|
||||||
'Back to Dashboard',
|
'Back to Dashboard',
|
||||||
|
'Auto-disable rules',
|
||||||
|
'Channel health checks',
|
||||||
|
'Request retry',
|
||||||
|
|
||||||
// System settings sidebar
|
// System settings sidebar
|
||||||
'System Administration',
|
'System Administration',
|
||||||
@@ -39,6 +42,7 @@ export const STATIC_I18N_KEYS = [
|
|||||||
'Content',
|
'Content',
|
||||||
'Integrations',
|
'Integrations',
|
||||||
'Models',
|
'Models',
|
||||||
|
'Routing Reliability',
|
||||||
'Maintenance',
|
'Maintenance',
|
||||||
|
|
||||||
// Pricing constants
|
// Pricing constants
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ export const Route = createFileRoute(
|
|||||||
'/_authenticated/system-settings/operations/$section'
|
'/_authenticated/system-settings/operations/$section'
|
||||||
)({
|
)({
|
||||||
beforeLoad: ({ params }) => {
|
beforeLoad: ({ params }) => {
|
||||||
|
if (params.section === 'monitoring') {
|
||||||
|
throw redirect({
|
||||||
|
to: '/system-settings/models/$section',
|
||||||
|
params: { section: 'routing-reliability' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const validSections = OPERATIONS_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({
|
||||||
|
|||||||
Reference in New Issue
Block a user