feat: add routing reliability management

This commit is contained in:
CaIon
2026-06-20 13:55:50 +08:00
parent dfcb74b52d
commit 91ab664c53
36 changed files with 1171 additions and 782 deletions
@@ -23,7 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { api } from '@/lib/api'
import dayjs from '@/lib/dayjs'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
AlertDialog,
@@ -47,16 +46,7 @@ import {
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
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_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>
@@ -109,10 +93,6 @@ type FlatPerfDefaults = {
'performance_setting.monitor_cpu_threshold': number
'performance_setting.monitor_memory_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 => ({
@@ -131,12 +111,6 @@ const buildFormDefaults = (defaults: FlatPerfDefaults): PerfFormInput => ({
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 => ({
@@ -156,38 +130,25 @@ const normalizeFormValues = (values: PerfFormValues): FlatPerfDefaults => ({
values.performance_setting.monitor_memory_threshold,
'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 {
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 '-' + formatBytes(-bytes, decimals)
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 parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]
if (i < 0 || i >= sizes.length) return `${bytes} Bytes`
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${
sizes[i]
}`
}
interface Props {
defaultValues: FlatPerfDefaults
}
type LogInfo = {
enabled: boolean
log_dir: string
file_count: number
total_size: number
oldest_time?: string
newest_time?: string
}
type PerformanceStats = {
cache_stats?: {
current_disk_usage_bytes: number
@@ -225,10 +186,6 @@ export function PerformanceSection(props: Props) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
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(
() => 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(() => {
fetchStats()
fetchLogInfo()
}, [fetchStats, fetchLogInfo])
}, [fetchStats])
const onSubmit = async (values: PerfFormValues) => {
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 monitorEnabled = form.watch('performance_setting.monitor_enabled')
const perfMetricsEnabled = form.watch('perf_metrics_setting.enabled')
const maxCacheSizeRaw = form.watch(
'performance_setting.disk_cache_max_size_mb'
)
@@ -607,262 +524,11 @@ export function PerformanceSection(props: Props) {
)}
/>
</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>
</Form>
<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 */}
<div className='space-y-4'>
<div className='flex items-center gap-2'>