feat: add routing reliability management
This commit is contained in:
+7
-341
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user