Add maintenance cleanup for revoked and orphaned configs
CI / Backend (build + test) (push) Failing after 58s
CI / Frontend (lint + typecheck + build) (push) Successful in 31s

Two new maintenance actions remove VpnConfig records (with their traffic
samples) from the database: all revoked configs, and configs whose inbound
or node no longer exists. 3x-ui is not touched; both write an AuditLog entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-17 01:04:46 +03:00
1 parent 1bc86dc426
commit 2c20fb0427
12 files changed
+367 -1

No files matched your search

@@ -15,6 +15,14 @@ export function deleteDisabledApps() {
return apiRequest<MaintenanceCleanupResponseDto>('/admin/maintenance/apps/disabled', { method: 'DELETE' })
}
export function deleteRevokedConfigs() {
return apiRequest<MaintenanceCleanupResponseDto>('/admin/maintenance/configs/revoked', { method: 'DELETE' })
}
export function deleteOrphanedConfigs() {
return apiRequest<MaintenanceCleanupResponseDto>('/admin/maintenance/configs/orphaned', { method: 'DELETE' })
}
export function factoryReset() {
return apiRequest<void>('/admin/maintenance/factory-reset', { method: 'DELETE' })
}
+53 -1
View File
@@ -6,7 +6,13 @@ import { toast } from '@/shared/ui/toast-store'
import { Button } from '@/shared/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
import { Input } from '@/shared/ui/input'
import { deleteClosedTickets, deleteDisabledApps, deleteOldAuditLogs } from '@/features/admin/maintenance/api'
import {
deleteClosedTickets,
deleteDisabledApps,
deleteOldAuditLogs,
deleteOrphanedConfigs,
deleteRevokedConfigs,
} from '@/features/admin/maintenance/api'
import { FactoryResetDialog } from '@/features/admin/maintenance/FactoryResetDialog'
export const Route = createFileRoute('/admin/maintenance')({ component: AdminMaintenancePage })
@@ -45,6 +51,23 @@ function AdminMaintenancePage() {
onError: () => toast.error(t('auth.genericError')),
})
const onConfigsDeleted = async (data: { deletedCount: number }) => {
toast.success(t('admin.maintenance.configs.deleted', { count: data.deletedCount }))
await queryClient.invalidateQueries({ queryKey: ['admin-configs'] })
}
const deleteRevokedConfigsMutation = useMutation({
mutationFn: deleteRevokedConfigs,
onSuccess: onConfigsDeleted,
onError: () => toast.error(t('auth.genericError')),
})
const deleteOrphanedConfigsMutation = useMutation({
mutationFn: deleteOrphanedConfigs,
onSuccess: onConfigsDeleted,
onError: () => toast.error(t('auth.genericError')),
})
return (
<div className="flex flex-col gap-4">
<Card>
@@ -120,6 +143,35 @@ function AdminMaintenancePage() {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">{t('admin.maintenance.configs.title')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">{t('admin.maintenance.configs.description')}</p>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
disabled={deleteRevokedConfigsMutation.isPending}
onClick={() => {
if (confirm(t('admin.maintenance.configs.revokedConfirm'))) deleteRevokedConfigsMutation.mutate()
}}
>
{t('admin.maintenance.configs.revokedAction')}
</Button>
<Button
variant="outline"
disabled={deleteOrphanedConfigsMutation.isPending}
onClick={() => {
if (confirm(t('admin.maintenance.configs.orphanedConfirm'))) deleteOrphanedConfigsMutation.mutate()
}}
>
{t('admin.maintenance.configs.orphanedAction')}
</Button>
</div>
</CardContent>
</Card>
<details className="group rounded-md border border-red-900/50">
<summary className="cursor-pointer select-none px-4 py-3 text-sm font-medium text-red-400">
{t('admin.maintenance.factoryReset.dangerZone')}
+22
View File
@@ -586,6 +586,17 @@ const resources = {
confirm: 'Удалить все отключённые приложения? Действие необратимо.',
deleted: 'Удалено приложений: {{count}}.',
},
configs: {
title: 'Конфиги',
description:
'Удалить из базы отозванные конфиги или конфиги, чей инбаунд/нода уже удалены (в списке — «? · ?»). Вместе с конфигами удаляется история трафика. Действие необратимо.',
revokedAction: 'Удалить все отозванные конфиги',
revokedConfirm: 'Удалить все отозванные конфиги? Действие необратимо.',
orphanedAction: 'Удалить все конфиги без ноды',
orphanedConfirm:
'Удалить все конфиги, чей инбаунд или нода удалены, в любом статусе? Действие необратимо.',
deleted: 'Удалено конфигов: {{count}}.',
},
factoryReset: {
dangerZone: 'Опасная зона',
title: 'Полный сброс панели',
@@ -1205,6 +1216,17 @@ const resources = {
confirm: 'Delete all disabled apps? This cannot be undone.',
deleted: 'Deleted apps: {{count}}.',
},
configs: {
title: 'Configs',
description:
'Delete revoked configs, or configs whose inbound/node no longer exists (shown as "? · ?"), from the database along with their traffic history. This cannot be undone.',
revokedAction: 'Delete all revoked configs',
revokedConfirm: 'Delete all revoked configs? This cannot be undone.',
orphanedAction: 'Delete all configs without a node',
orphanedConfirm:
'Delete all configs whose inbound or node was deleted, in any status? This cannot be undone.',
deleted: 'Deleted configs: {{count}}.',
},
factoryReset: {
dangerZone: 'Danger zone',
title: 'Full panel reset',