feat: add persistent system task log cleanup progress

This commit is contained in:
CaIon
2026-06-22 19:16:53 +08:00
parent 6dc4030fdf
commit a162163b48
19 changed files with 984 additions and 51 deletions
+30
View File
@@ -21,7 +21,9 @@ import type {
ConfirmPaymentComplianceResponse,
DeleteLogsResponse,
FetchUpstreamRatiosRequest,
LogCleanupTask,
SystemOptionsResponse,
SystemTaskResponse,
UpdateOptionRequest,
UpdateOptionResponse,
UpstreamChannelsResponse,
@@ -53,6 +55,34 @@ export async function deleteLogsBefore(targetTimestamp: number) {
return res.data
}
export async function startLogCleanupTask(targetTimestamp: number) {
const res = await api.post<SystemTaskResponse<LogCleanupTask>>(
'/api/system-task/log-cleanup',
null,
{
params: { target_timestamp: targetTimestamp },
}
)
return res.data
}
export async function getCurrentLogCleanupTask() {
const res = await api.get<SystemTaskResponse<LogCleanupTask | null>>(
'/api/system-task/current',
{
params: { type: 'log_cleanup' },
}
)
return res.data
}
export async function getSystemTask(taskId: string) {
const res = await api.get<SystemTaskResponse<LogCleanupTask>>(
`/api/system-task/${taskId}`
)
return res.data
}
export async function resetModelRatios() {
const res = await api.post<UpdateOptionResponse>(
'/api/option/rest_model_ratio'
@@ -48,6 +48,7 @@ import {
} 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,
@@ -59,7 +60,11 @@ import {
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { DateTimePicker } from '@/components/datetime-picker'
import { deleteLogsBefore } from '../api'
import {
getCurrentLogCleanupTask,
getSystemTask,
startLogCleanupTask,
} from '../api'
import {
SettingsControlGroup,
SettingsForm,
@@ -69,6 +74,7 @@ import {
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
import type { LogCleanupTask } from '../types'
const logSettingsSchema = z.object({
LogConsumeEnabled: z.boolean(),
@@ -127,6 +133,10 @@ const quickSelectOptions = [
},
]
function isActiveLogCleanupTask(task: LogCleanupTask | null) {
return task?.status === 'pending' || task?.status === 'running'
}
export function LogSettingsSection({
defaultEnabled,
}: LogSettingsSectionProps) {
@@ -142,7 +152,10 @@ export function LogSettingsSection({
const [purgeDate, setPurgeDate] = useState<Date | undefined>(() =>
getDateDaysAgo(30)
)
const [isCleaning, setIsCleaning] = useState(false)
const [isStartingLogCleanup, setIsStartingLogCleanup] = useState(false)
const [logCleanupTask, setLogCleanupTask] = useState<LogCleanupTask | null>(
null
)
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [serverLogInfo, setServerLogInfo] = useState<ServerLogInfo | null>(
null
@@ -168,6 +181,27 @@ export function LogSettingsSection({
fetchServerLogInfo()
}, [fetchServerLogInfo])
useEffect(() => {
let cancelled = false
async function fetchCurrentLogCleanupTask() {
try {
const res = await getCurrentLogCleanupTask()
if (!cancelled && res.success && res.data) {
setLogCleanupTask(res.data)
}
} catch {
/* ignore */
}
}
fetchCurrentLogCleanupTask()
return () => {
cancelled = true
}
}, [])
const purgeTimestamp = useMemo(() => {
if (!purgeDate) return null
return Math.floor(purgeDate.getTime() / 1000)
@@ -178,6 +212,49 @@ export function LogSettingsSection({
return formatTimestampToDate(purgeDate.getTime(), 'milliseconds')
}, [purgeDate])
const logCleanupActive = isActiveLogCleanupTask(logCleanupTask)
const logCleanupState = logCleanupTask?.state
const logCleanupProgress = Math.min(
100,
Math.max(0, logCleanupState?.progress ?? 0)
)
const logCleanupProcessed = logCleanupState?.processed ?? 0
const logCleanupTotal = logCleanupState?.total ?? 0
useEffect(() => {
if (!logCleanupTask || !isActiveLogCleanupTask(logCleanupTask)) return
let cancelled = false
const interval = window.setInterval(async () => {
try {
const res = await getSystemTask(logCleanupTask.task_id)
if (cancelled || !res.success || !res.data) return
setLogCleanupTask(res.data)
if (!isActiveLogCleanupTask(res.data)) {
if (res.data.status === 'succeeded') {
const count =
res.data.result?.deleted_count ?? res.data.state?.processed ?? 0
toast.success(
count > 0
? t('{{count}} log entries removed.', { count })
: t('No log entries matched the selected time.')
)
} else if (res.data.status === 'failed') {
toast.error(res.data.error || t('Failed to clean logs'))
}
}
} catch {
/* keep polling */
}
}, 1000)
return () => {
cancelled = true
window.clearInterval(interval)
}
}, [logCleanupTask?.task_id, logCleanupTask?.status, t])
const onSubmit = async (values: LogSettingsFormValues) => {
if (values.LogConsumeEnabled === defaultEnabled) return
await updateOption.mutateAsync({
@@ -201,24 +278,24 @@ export function LogSettingsSection({
return
}
setIsCleaning(true)
setIsStartingLogCleanup(true)
try {
const res = await deleteLogsBefore(purgeTimestamp)
const res = await startLogCleanupTask(purgeTimestamp)
if (!res.success) {
throw new Error(res.message || t('Failed to clean logs'))
}
const count = res.data ?? 0
toast.success(
count > 0
? t('{{count}} log entries removed.', { count })
: t('No log entries matched the selected time.')
)
if (!res.data) {
throw new Error(t('Failed to clean logs'))
}
setLogCleanupTask(res.data)
setShowConfirmDialog(false)
toast.success(t('Log cleanup task started.'))
} catch (error) {
const message =
error instanceof Error ? error.message : t('Failed to clean logs')
toast.error(message)
} finally {
setIsCleaning(false)
setIsStartingLogCleanup(false)
}
}
@@ -314,11 +391,37 @@ export function LogSettingsSection({
type='button'
variant='destructive'
onClick={handleRequestCleanLogs}
disabled={isCleaning}
disabled={isStartingLogCleanup || logCleanupActive}
>
{isCleaning ? t('Cleaning...') : t('Clean logs')}
{isStartingLogCleanup || logCleanupActive
? t('Cleaning...')
: t('Clean logs')}
</Button>
</div>
{logCleanupTask && (
<div className='rounded-md border p-3'>
<div className='mb-2 flex items-center justify-between gap-3 text-sm'>
<span className='font-medium'>
{t('Log cleanup progress')}
</span>
<span className='text-muted-foreground tabular-nums'>
{logCleanupProgress}%
</span>
</div>
<Progress value={logCleanupProgress} />
<div className='text-muted-foreground mt-2 text-xs'>
{t('{{processed}} of {{total}} log entries processed.', {
processed: logCleanupProcessed,
total: logCleanupTotal,
})}
</div>
{logCleanupTask.status === 'failed' && logCleanupTask.error && (
<div className='text-destructive mt-2 text-xs'>
{logCleanupTask.error}
</div>
)}
</div>
)}
</SettingsControlGroup>
</SettingsForm>
</Form>
@@ -491,11 +594,14 @@ export function LogSettingsSection({
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isCleaning}>
<AlertDialogCancel disabled={isStartingLogCleanup}>
{t('Cancel')}
</AlertDialogCancel>
<AlertDialogAction onClick={handleCleanLogs} disabled={isCleaning}>
{isCleaning ? t('Cleaning...') : t('Delete logs')}
<AlertDialogAction
onClick={handleCleanLogs}
disabled={isStartingLogCleanup}
>
{isStartingLogCleanup ? t('Cleaning...') : t('Delete logs')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
+50
View File
@@ -56,6 +56,56 @@ export type DeleteLogsResponse = {
data?: number
}
export type SystemTaskStatus = 'pending' | 'running' | 'succeeded' | 'failed'
export type SystemTask<
TPayload = Record<string, unknown>,
TState = Record<string, unknown>,
TResult = Record<string, unknown>,
> = {
id: number
task_id: string
type: string
status: SystemTaskStatus
active_key?: string
payload?: TPayload
state?: TState
result?: TResult
error?: string
locked_by?: string
locked_until?: number
created_at: number
updated_at: number
}
export type LogCleanupTaskPayload = {
target_timestamp: number
batch_size: number
}
export type LogCleanupTaskState = {
total: number
processed: number
progress: number
remaining: number
}
export type LogCleanupTaskResult = {
deleted_count: number
}
export type LogCleanupTask = SystemTask<
LogCleanupTaskPayload,
LogCleanupTaskState,
LogCleanupTaskResult
>
export type SystemTaskResponse<TTask = SystemTask | null> = {
success: boolean
message: string
data?: TTask
}
export type SiteSettings = {
'theme.frontend': string
Notice: string
+3
View File
@@ -52,6 +52,7 @@
"{{modality}} not supported": "{{modality}} not supported",
"{{modality}} supported": "{{modality}} supported",
"{{n}} model(s) selected": "{{n}} model(s) selected",
"{{processed}} of {{total}} log entries processed.": "{{processed}} of {{total}} log entries processed.",
"{{success}} succeeded, {{failed}} failed": "{{success}} succeeded, {{failed}} failed",
"{{target}} test failed": "{{target}} test failed",
"{{target}} test succeeded": "{{target}} test succeeded",
@@ -2352,6 +2353,8 @@
"Locations": "Locations",
"Locked": "Locked",
"log": "log",
"Log cleanup progress": "Log cleanup progress",
"Log cleanup task started.": "Log cleanup task started.",
"Log Details": "Log Details",
"Log Directory": "Log Directory",
"Log File Count": "Log File Count",
+3
View File
@@ -52,6 +52,7 @@
"{{modality}} not supported": "{{modality}} non pris en charge",
"{{modality}} supported": "{{modality}} pris en charge",
"{{n}} model(s) selected": "{{n}} modèle(s) sélectionné(s)",
"{{processed}} of {{total}} log entries processed.": "{{processed}} sur {{total}} entrées de journal traitées.",
"{{success}} succeeded, {{failed}} failed": "{{success}} réussi(s), {{failed}} échoué(s)",
"{{target}} test failed": "Échec du test de {{target}}",
"{{target}} test succeeded": "Test de {{target}} réussi",
@@ -2352,6 +2353,8 @@
"Locations": "Emplacements",
"Locked": "Verrouillé",
"log": "journal",
"Log cleanup progress": "Progression du nettoyage des journaux",
"Log cleanup task started.": "La tâche de nettoyage des journaux a démarré.",
"Log Details": "Détails du journal",
"Log Directory": "Répertoire des journaux",
"Log File Count": "Nombre de fichiers journaux",
+3
View File
@@ -52,6 +52,7 @@
"{{modality}} not supported": "{{modality}} はサポートされていません",
"{{modality}} supported": "{{modality}} をサポート",
"{{n}} model(s) selected": "{{n}} 件のモデルを選択済み",
"{{processed}} of {{total}} log entries processed.": "{{total}} 件中 {{processed}} 件のログを処理しました。",
"{{success}} succeeded, {{failed}} failed": "{{success}} 件成功、{{failed}} 件失敗",
"{{target}} test failed": "{{target}} のテストに失敗しました",
"{{target}} test succeeded": "{{target}} のテストに成功しました",
@@ -2352,6 +2353,8 @@
"Locations": "場所",
"Locked": "ロック済み",
"log": "ログ",
"Log cleanup progress": "ログクリーンアップの進行状況",
"Log cleanup task started.": "ログクリーンアップタスクを開始しました。",
"Log Details": "ログの詳細",
"Log Directory": "ログディレクトリ",
"Log File Count": "ログファイル数",
+3
View File
@@ -52,6 +52,7 @@
"{{modality}} not supported": "{{modality}} не поддерживается",
"{{modality}} supported": "{{modality}} поддерживается",
"{{n}} model(s) selected": "Выбрано моделей: {{n}}",
"{{processed}} of {{total}} log entries processed.": "Обработано {{processed}} из {{total}} записей журнала.",
"{{success}} succeeded, {{failed}} failed": "{{success}} успешно, {{failed}} с ошибкой",
"{{target}} test failed": "Тест {{target}} не выполнен",
"{{target}} test succeeded": "Тест {{target}} успешно выполнен",
@@ -2352,6 +2353,8 @@
"Locations": "Местоположения",
"Locked": "Заблокировано",
"log": "записи",
"Log cleanup progress": "Ход очистки журнала",
"Log cleanup task started.": "Задача очистки журнала запущена.",
"Log Details": "Детали журнала",
"Log Directory": "Каталог журналов",
"Log File Count": "Кол-во файлов журналов",
+3
View File
@@ -52,6 +52,7 @@
"{{modality}} not supported": "Không hỗ trợ {{modality}}",
"{{modality}} supported": "Hỗ trợ {{modality}}",
"{{n}} model(s) selected": "Đã chọn {{n}} model",
"{{processed}} of {{total}} log entries processed.": "Đã xử lý {{processed}}/{{total}} mục nhật ký.",
"{{success}} succeeded, {{failed}} failed": "{{success}} thành công, {{failed}} thất bại",
"{{target}} test failed": "Kiểm tra {{target}} thất bại",
"{{target}} test succeeded": "Kiểm tra {{target}} thành công",
@@ -2352,6 +2353,8 @@
"Locations": "Vị trí",
"Locked": "Đã khóa",
"log": "nhật ký",
"Log cleanup progress": "Tiến trình dọn dẹp nhật ký",
"Log cleanup task started.": "Đã bắt đầu tác vụ dọn dẹp nhật ký.",
"Log Details": "Chi tiết Nhật ký",
"Log Directory": "Thư mục nhật ký",
"Log File Count": "Số tệp nhật ký",
+3
View File
@@ -52,6 +52,7 @@
"{{modality}} not supported": "不支持 {{modality}}",
"{{modality}} supported": "支持 {{modality}}",
"{{n}} model(s) selected": "已选 {{n}} 个模型",
"{{processed}} of {{total}} log entries processed.": "已处理 {{processed}} / {{total}} 条日志。",
"{{success}} succeeded, {{failed}} failed": "{{success}} 个成功,{{failed}} 个失败",
"{{target}} test failed": "{{target}} 测试失败",
"{{target}} test succeeded": "{{target}} 测试成功",
@@ -2352,6 +2353,8 @@
"Locations": "位置",
"Locked": "锁定",
"log": "日志的完整详情",
"Log cleanup progress": "日志清理进度",
"Log cleanup task started.": "日志清理任务已启动。",
"Log Details": "日志详情",
"Log Directory": "日志目录",
"Log File Count": "日志文件数",