Files
new-api/web/src/features/system-settings/models/routing-reliability-section.tsx
T

625 lines
22 KiB
TypeScript

/*
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 { zodResolver } from '@hookform/resolvers/zod'
import { useMemo, useRef } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import * as z from 'zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { parseHttpStatusCodeRules } from '@/lib/http-status-code-rules'
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 channelTestModes = [
'scheduled_all',
'auto_ban_only',
'passive_recovery',
] as const
type ChannelTestMode = (typeof channelTestModes)[number]
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'),
channel_test_mode: z.enum(channelTestModes),
}),
})
.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
'monitor_setting.channel_test_mode': ChannelTestMode
}
}
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
'monitor_setting.channel_test_mode': ChannelTestMode
}
function normalizeChannelTestMode(value?: string): ChannelTestMode {
if (value === 'auto_ban_only' || value === 'passive_recovery') {
return value
}
return 'scheduled_all'
}
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'],
channel_test_mode: normalizeChannelTestMode(
defaults['monitor_setting.channel_test_mode']
),
},
})
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'],
'monitor_setting.channel_test_mode': normalizeChannelTestMode(
defaults['monitor_setting.channel_test_mode']
),
})
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,
'monitor_setting.channel_test_mode': values.monitor_setting.channel_test_mode,
})
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 channelTestMode = form.watch('monitor_setting.channel_test_mode')
let channelTestModeDescription: string
switch (channelTestMode) {
case 'auto_ban_only':
channelTestModeDescription = t(
'Periodically checks only channels with auto-disable enabled, excluding manually disabled channels.'
)
break
case 'passive_recovery':
channelTestModeDescription = t(
'Does not check healthy channels. It only rechecks auto-disabled channels and restores them after they recover.'
)
break
default:
channelTestModeDescription = t(
'Periodically checks all channels except manually disabled ones to detect failures and recover channels automatically.'
)
}
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.channel_test_mode'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Channel test mode')}</FormLabel>
<Select
items={[
{
value: 'scheduled_all',
label: t('Actively check all channels'),
},
{
value: 'auto_ban_only',
label: t(
'Actively check auto-disable-enabled channels'
),
},
{
value: 'passive_recovery',
label: t('Check channels awaiting recovery only'),
},
]}
value={field.value}
onValueChange={field.onChange}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent alignItemWithTrigger={false}>
<SelectGroup>
<SelectItem value='scheduled_all'>
{t('Actively check all channels')}
</SelectItem>
<SelectItem value='auto_ban_only'>
{t('Actively check auto-disable-enabled channels')}
</SelectItem>
<SelectItem value='passive_recovery'>
{t('Check channels awaiting recovery only')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
{channelTestModeDescription}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<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>
{channelTestMode === 'passive_recovery'
? t(
'How frequently the system checks auto-disabled channels for recovery'
)
: 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>
)
}