feat: add scoped manual usage cleanup

This commit is contained in:
HsungKayphoon
2026-05-16 14:53:10 +08:00
parent 75b7319465
commit 74a1e5ad7d
17 changed files with 1746 additions and 305 deletions

View File

@@ -310,15 +310,35 @@ export interface ManualUsageCleanupSummary {
records_deleted: number
}
export interface ManualUsageCleanupResponse {
export type ManualUsageCleanupMode = 'policy' | 'older_than_days' | 'before_now'
export type ManualUsageCleanupTarget = 'detail_body' | 'compressed_body' | 'headers' | 'records'
export interface ManualUsageCleanupTargets {
detail_body: boolean
compressed_body: boolean
headers: boolean
records: boolean
expired_keys: boolean
}
export interface ManualUsageCleanupRequest {
mode?: ManualUsageCleanupMode
older_than_days?: number
targets?: ManualUsageCleanupTarget[]
}
export interface ManualUsageCleanupTaskResponse {
message: string
mode: ManualUsageCleanupMode
requested_older_than_days: number | null
summary: ManualUsageCleanupSummary
total_affected: number
targets: ManualUsageCleanupTargets
task: CleanupRunRecord
}
export interface ManualUsageCleanupPreview {
mode: ManualUsageCleanupMode
requested_older_than_days: number | null
targets: ManualUsageCleanupTargets
effective_cutoffs: {
detail: string
compressed: string
@@ -1218,14 +1238,20 @@ export const adminApi = {
},
async runManualUsageCleanup(
params: { older_than_days?: number } = {}
): Promise<ManualUsageCleanupResponse | ManualUsageCleanupConflict> {
const body: Record<string, number> = {}
params: ManualUsageCleanupRequest = {}
): Promise<ManualUsageCleanupTaskResponse | ManualUsageCleanupConflict> {
const body: ManualUsageCleanupRequest = {}
if (params.mode) {
body.mode = params.mode
}
if (typeof params.older_than_days === 'number') {
body.older_than_days = params.older_than_days
}
if (params.targets?.length) {
body.targets = params.targets
}
try {
const response = await apiClient.post<ManualUsageCleanupResponse>(
const response = await apiClient.post<ManualUsageCleanupTaskResponse>(
'/api/admin/system/cleanup/usage/manual',
body
)
@@ -1240,12 +1266,18 @@ export const adminApi = {
},
async previewManualUsageCleanup(
params: { older_than_days?: number } = {}
params: ManualUsageCleanupRequest = {}
): Promise<ManualUsageCleanupPreview> {
const query: Record<string, number> = {}
const query: Record<string, string | number> = {}
if (params.mode) {
query.mode = params.mode
}
if (typeof params.older_than_days === 'number') {
query.older_than_days = params.older_than_days
}
if (params.targets?.length) {
query.targets = params.targets.join(',')
}
const response = await apiClient.get<ManualUsageCleanupPreview>(
'/api/admin/system/cleanup/usage/preview',
{ params: query }

View File

@@ -288,7 +288,8 @@
<ManualCleanupConfirmDialog
:open="manualCleanupDialogOpen"
@update:open="manualCleanupDialogOpen = $event"
@confirm="handleManualCleanupConfirm"
@running-change="manualCleanupRunning = $event"
@completed="handleManualCleanupCompleted"
/>
<div
@@ -402,7 +403,7 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { RefreshCw, Trash2 } from 'lucide-vue-next'
import { adminApi, type CleanupRunRecord, type ManualUsageCleanupResponse } from '@/api/admin'
import { adminApi, type CleanupRunRecord } from '@/api/admin'
import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue'
import Label from '@/components/ui/label.vue'
@@ -410,7 +411,6 @@ import Switch from '@/components/ui/switch.vue'
import { CardSection } from '@/components/layout'
import ManualCleanupConfirmDialog from './ManualCleanupConfirmDialog.vue'
import { useToast } from '@/composables/useToast'
import { parseApiError } from '@/utils/errorParser'
defineProps<{
enableAutoCleanup: boolean
@@ -459,52 +459,18 @@ function openManualCleanupDialog() {
manualCleanupDialogOpen.value = true
}
async function handleManualCleanupConfirm(olderThanDays: number | undefined) {
manualCleanupRunning.value = true
try {
const response = await adminApi.runManualUsageCleanup(
typeof olderThanDays === 'number' ? { older_than_days: olderThanDays } : {},
)
if ('detail' in response && response.detail === 'usage_cleanup_already_running') {
manualCleanupResult.value = {
title: '已有一次清理正在进行中',
description: response.message,
}
toast.warning(response.message)
} else {
const completed = response as ManualUsageCleanupResponse
manualCleanupResult.value = {
title: completed.message,
description: summarizeManualCleanup(completed),
}
toast.success(completed.message)
}
manualCleanupDialogOpen.value = false
} catch (error) {
const message = parseApiError(error).message
manualCleanupResult.value = {
title: '请求记录清理失败',
description: message,
}
toast.error(message)
} finally {
manualCleanupRunning.value = false
void loadCleanupRuns()
function handleManualCleanupCompleted(task: CleanupRunRecord) {
manualCleanupRunning.value = false
manualCleanupResult.value = {
title: task.message,
description: cleanupSummaryText(task.summary),
}
}
function summarizeManualCleanup(response: ManualUsageCleanupResponse): string {
const { summary } = response
const parts: string[] = []
if (summary.records_deleted > 0) parts.push(`删除整条记录 ${summary.records_deleted}`)
if (summary.body_externalized > 0) parts.push(`压缩 body ${summary.body_externalized}`)
if (summary.body_cleaned > 0) parts.push(`清除过期 body ${summary.body_cleaned}`)
if (summary.header_cleaned > 0) parts.push(`清除 headers ${summary.header_cleaned}`)
if (summary.legacy_body_refs_migrated > 0) {
parts.push(`迁移遗留引用 ${summary.legacy_body_refs_migrated}`)
if (task.status === 'failed') {
toast.error(task.error || task.message)
} else {
toast.success(task.message)
}
if (summary.keys_cleaned > 0) parts.push(`回收 Key ${summary.keys_cleaned}`)
return parts.length > 0 ? parts.join(' / ') : '无数据变更'
void loadCleanupRuns()
}
async function loadCleanupRuns() {
@@ -568,7 +534,7 @@ function cleanupSummaryText(summary: Record<string, unknown>): string {
function summaryLabel(key: string): string {
const labels: Record<string, string> = {
body_externalized: '压缩',
body_externalized: '详细体',
legacy_body_refs_migrated: '迁移',
body_cleaned: '清体',
header_cleaned: '清头',

View File

@@ -3,30 +3,86 @@
:open="open"
size="lg"
title="立即清理请求记录"
description="按现有分级保留策略主动清理请求记录,可选指定清理更早时间的数据。操作不可逆。"
:persistent="submitting"
description="默认按当前分级保留策略执行,也可以选择指定范围。操作不可逆。"
:persistent="isLocked"
@update:open="handleOpenChange"
>
<div class="px-4 sm:px-6 py-4 space-y-4">
<div>
<Label class="block text-sm font-medium">
清理方式
</Label>
<div class="mt-2 grid grid-cols-1 sm:grid-cols-3 gap-2">
<button
v-for="item in modeOptions"
:key="item.value"
type="button"
class="rounded-md border px-3 py-2 text-left text-sm transition-colors"
:class="mode === item.value ? 'border-primary bg-primary/10 text-primary' : 'border-border bg-card hover:bg-muted/60'"
:disabled="isLocked"
@click="setMode(item.value)"
>
<span class="font-medium">{{ item.label }}</span>
<span class="mt-1 block text-xs text-muted-foreground">{{ item.description }}</span>
</button>
</div>
</div>
<div v-if="mode === 'older_than_days'">
<Label
for="manual-cleanup-older-than-days"
class="block text-sm font-medium"
>
清理 N 天前的记录可选
清理 N 天前的记录
</Label>
<Input
id="manual-cleanup-older-than-days"
:model-value="olderThanDays ?? ''"
type="number"
min="1"
placeholder="留空代表按当前保留策略"
placeholder="例如 30"
class="mt-1"
:disabled="submitting"
:disabled="isLocked"
@update:model-value="handleDaysChange"
/>
<p class="mt-1 text-xs text-muted-foreground">
留空代表按当前保留策略清理填入数字代表清理 N 天前的记录该值只能比策略更宽松不会删除更新的数据
该值会与当前策略取更保守的时间点不会清理比策略更新的数据
</p>
</div>
<div>
<Label class="block text-sm font-medium">
清理范围
</Label>
<div class="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-2">
<label
v-for="target in targetOptions"
:key="target.value"
class="flex min-h-16 items-start gap-3 rounded-md border border-border bg-card px-3 py-2"
>
<Checkbox
class="mt-0.5"
:checked="selectedTargets.includes(target.value)"
:disabled="isLocked"
@update:checked="toggleTarget(target.value, $event)"
/>
<span>
<span class="block text-sm font-medium">{{ target.label }}</span>
<span class="block text-xs text-muted-foreground">{{ target.description }}</span>
</span>
</label>
</div>
<p
v-if="mode === 'before_now'"
class="mt-2 text-xs text-amber-600"
>
当前时刻之前模式只允许清理详细请求体和压缩请求体不会清请求头或整条记录
</p>
<p
v-if="targetError"
class="mt-2 text-xs text-destructive"
>
{{ targetError }}
</p>
</div>
@@ -39,7 +95,7 @@
v-if="!previewLoading"
type="button"
class="text-xs text-muted-foreground hover:text-foreground"
:disabled="submitting"
:disabled="isLocked"
@click="loadPreview"
>
刷新预估
@@ -86,6 +142,45 @@
</div>
</div>
<div
v-if="activeTask || taskError"
class="rounded-md border border-border bg-card px-4 py-3"
>
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<div class="text-sm font-medium">
{{ activeTask?.message || '请求记录清理失败' }}
</div>
<div class="mt-1 text-xs text-muted-foreground">
{{ activeTask ? cleanupStatusLabel(activeTask.status) : taskError }}
</div>
</div>
<span
v-if="activeTask"
:class="cleanupStatusClass(activeTask.status)"
class="shrink-0 text-xs"
>
{{ cleanupStatusLabel(activeTask.status) }}
</span>
</div>
<div
v-if="activeTask"
class="mt-3 h-2 overflow-hidden rounded-full bg-muted"
>
<div
class="h-full rounded-full bg-primary transition-all"
:class="{ 'animate-pulse': activeTask.status === 'processing' }"
:style="{ width: `${taskProgressPercent}%` }"
/>
</div>
<div
v-if="activeTask"
class="mt-2 text-xs text-muted-foreground"
>
{{ cleanupSummaryText(activeTask.summary) }}
</div>
</div>
<div>
<Label
for="manual-cleanup-confirm-phrase"
@@ -99,47 +194,59 @@
class="mt-1"
autocomplete="off"
:placeholder="confirmPhrase"
:disabled="submitting"
:disabled="isLocked || isFinished"
@update:model-value="typedPhrase = String($event)"
@keydown.enter.prevent="maybeSubmitOnEnter"
/>
<p class="mt-1 text-xs text-muted-foreground">
确认操作后会立刻执行清理且不可撤销
确认后会在当前弹窗中显示执行状态完成前不能关闭
</p>
</div>
</div>
<template #footer>
<Button
v-if="!isFinished"
variant="destructive"
:disabled="!canSubmit"
@click="handleConfirm"
>
{{ submitting ? '清理中…' : '确认清理' }}
{{ isLocked ? '清理中…' : '确认清理' }}
</Button>
<Button
variant="outline"
:disabled="submitting"
:disabled="isLocked"
@click="handleCancel"
>
取消
{{ isFinished ? '关闭' : '取消' }}
</Button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { Dialog } from '@/components/ui'
import Button from '@/components/ui/button.vue'
import Checkbox from '@/components/ui/checkbox.vue'
import Input from '@/components/ui/input.vue'
import Label from '@/components/ui/label.vue'
import { adminApi, type ManualUsageCleanupPreview } from '@/api/admin'
import {
adminApi,
type CleanupRunRecord,
type ManualUsageCleanupPreview,
type ManualUsageCleanupRequest,
} from '@/api/admin'
import { parseApiError } from '@/utils/errorParser'
import {
MANUAL_USAGE_CLEANUP_CONFIRM_PHRASE,
allowedTargetsForMode,
defaultManualCleanupTargets,
isConfirmPhraseMatched,
normalizeManualCleanupTargets,
normalizeOlderThanDaysInput,
type ManualCleanupMode,
type ManualCleanupTarget,
} from './manualCleanupForm'
const props = defineProps<{
@@ -148,50 +255,124 @@ const props = defineProps<{
const emit = defineEmits<{
'update:open': [value: boolean]
confirm: [olderThanDays: number | undefined]
'running-change': [value: boolean]
completed: [task: CleanupRunRecord]
}>()
const confirmPhrase = MANUAL_USAGE_CLEANUP_CONFIRM_PHRASE
const mode = ref<ManualCleanupMode>('policy')
const olderThanDays = ref<number | null>(null)
const selectedTargets = ref<ManualCleanupTarget[]>(defaultManualCleanupTargets('policy'))
const targetsTouched = ref(false)
const typedPhrase = ref('')
const preview = ref<ManualUsageCleanupPreview | null>(null)
const previewLoading = ref(false)
const previewError = ref<string | null>(null)
const submitting = ref(false)
const activeTask = ref<CleanupRunRecord | null>(null)
const taskError = ref<string | null>(null)
let previewDebounceTimer: ReturnType<typeof setTimeout> | null = null
let previewSeq = 0
let taskPollTimer: ReturnType<typeof window.setInterval> | null = null
const normalizedPhrase = computed(() => typedPhrase.value)
const modeOptions: Array<{ value: ManualCleanupMode; label: string; description: string }> = [
{ value: 'policy', label: '按当前策略', description: '沿用页面上配置的保留天数' },
{ value: 'older_than_days', label: '指定天数前', description: '在策略内取更保守时间点' },
{ value: 'before_now', label: '当前时刻之前', description: '只清已选请求体内容' },
]
const targetLabels: Record<ManualCleanupTarget, { label: string; description: string }> = {
detail_body: { label: '详细请求体', description: '把详细 body 移入压缩/外置存储' },
compressed_body: { label: '压缩请求体', description: '删除已压缩或外置的 body 内容' },
headers: { label: '请求头', description: '清空请求/响应 headers 字段' },
records: { label: '整条记录', description: '删除超过记录保留期的 usage 行' },
}
const targetOptions = computed(() =>
allowedTargetsForMode(mode.value).map(value => ({
value,
...targetLabels[value],
}))
)
const normalizedTargets = computed(() =>
normalizeManualCleanupTargets(mode.value, selectedTargets.value)
)
const targetError = computed(() => {
if (normalizedTargets.value.length === 0) return '至少选择一个清理范围'
return null
})
const currentTaskRunning = computed(() => activeTask.value?.status === 'processing')
const isLocked = computed(() => submitting.value || currentTaskRunning.value)
const isFinished = computed(() =>
activeTask.value?.status === 'completed' || activeTask.value?.status === 'failed'
)
const canSubmit = computed(
() =>
!submitting.value &&
!isLocked.value &&
!isFinished.value &&
!previewLoading.value &&
isConfirmPhraseMatched(normalizedPhrase.value),
!targetError.value &&
modeIsValid.value &&
isConfirmPhraseMatched(typedPhrase.value),
)
const modeIsValid = computed(() => mode.value !== 'older_than_days' || olderThanDays.value !== null)
const taskProgressPercent = computed(() => {
const task = activeTask.value
if (!task) return 0
if (task.status === 'completed') return 100
if (task.status === 'failed') return 100
const raw = task.summary?.progress_percent
if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
return Math.max(1, Math.min(99, Math.round(raw)))
}
return 12
})
watch(
() => props.open,
(isOpen) => {
if (isOpen) {
resetForm()
void loadPreview()
} else if (previewDebounceTimer) {
clearTimeout(previewDebounceTimer)
previewDebounceTimer = null
} else {
clearPreviewTimer()
stopTaskPolling()
}
},
)
function resetForm() {
mode.value = 'policy'
olderThanDays.value = null
selectedTargets.value = defaultManualCleanupTargets('policy')
targetsTouched.value = false
typedPhrase.value = ''
preview.value = null
previewError.value = null
previewLoading.value = false
submitting.value = false
activeTask.value = null
taskError.value = null
emit('running-change', false)
}
function setMode(nextMode: ManualCleanupMode) {
if (isLocked.value || mode.value === nextMode) return
mode.value = nextMode
olderThanDays.value = null
selectedTargets.value = defaultManualCleanupTargets(nextMode)
targetsTouched.value = false
activeTask.value = null
taskError.value = null
schedulePreview()
}
function handleDaysChange(value: string | number) {
@@ -199,33 +380,59 @@ function handleDaysChange(value: string | number) {
schedulePreview()
}
function schedulePreview() {
if (previewDebounceTimer) {
clearTimeout(previewDebounceTimer)
function toggleTarget(target: ManualCleanupTarget, checked: boolean) {
targetsTouched.value = true
activeTask.value = null
taskError.value = null
const current = new Set(selectedTargets.value)
if (checked) {
current.add(target)
} else {
current.delete(target)
}
selectedTargets.value = normalizeManualCleanupTargets(mode.value, Array.from(current))
schedulePreview()
}
function buildRequest(): ManualUsageCleanupRequest {
const request: ManualUsageCleanupRequest = { mode: mode.value }
if (mode.value === 'older_than_days' && olderThanDays.value !== null) {
request.older_than_days = olderThanDays.value
}
if (targetsTouched.value) {
request.targets = normalizedTargets.value
}
return request
}
function schedulePreview() {
clearPreviewTimer()
previewDebounceTimer = setTimeout(() => {
previewDebounceTimer = null
void loadPreview()
}, 300)
}
function clearPreviewTimer() {
if (previewDebounceTimer) {
clearTimeout(previewDebounceTimer)
previewDebounceTimer = null
}
}
async function loadPreview() {
const seq = ++previewSeq
previewLoading.value = true
previewError.value = null
try {
const params: { older_than_days?: number } = {}
if (olderThanDays.value !== null) {
params.older_than_days = olderThanDays.value
}
const result = await adminApi.previewManualUsageCleanup(params)
const result = await adminApi.previewManualUsageCleanup(buildRequest())
if (seq === previewSeq) {
preview.value = result
}
} catch (error) {
if (seq === previewSeq) {
preview.value = null
previewError.value = parseApiError(error).message
previewError.value = parseApiError(error)
}
} finally {
if (seq === previewSeq) {
@@ -235,14 +442,14 @@ async function loadPreview() {
}
function handleOpenChange(value: boolean) {
if (!value && submitting.value) {
if (!value && isLocked.value) {
return
}
emit('update:open', value)
}
function handleCancel() {
if (submitting.value) return
if (isLocked.value) return
emit('update:open', false)
}
@@ -255,14 +462,101 @@ function maybeSubmitOnEnter() {
async function handleConfirm() {
if (!canSubmit.value) return
submitting.value = true
taskError.value = null
try {
emit('confirm', olderThanDays.value ?? undefined)
const response = await adminApi.runManualUsageCleanup(buildRequest())
if ('detail' in response && response.detail === 'usage_cleanup_already_running') {
taskError.value = response.message
emit('running-change', false)
return
}
activeTask.value = response.task
emit('running-change', response.task.status === 'processing')
if (response.task.status === 'processing') {
startTaskPolling(response.task.id)
} else {
emit('completed', response.task)
}
} catch (error) {
taskError.value = parseApiError(error)
emit('running-change', false)
} finally {
submitting.value = false
}
}
function startTaskPolling(taskId: string) {
stopTaskPolling()
void pollTask(taskId)
taskPollTimer = window.setInterval(() => {
void pollTask(taskId)
}, 1_500)
}
function stopTaskPolling() {
if (taskPollTimer) {
window.clearInterval(taskPollTimer)
taskPollTimer = null
}
}
async function pollTask(taskId: string) {
try {
const response = await adminApi.getCleanupRuns()
const task = response.items.find(item => item.id === taskId)
if (!task) return
activeTask.value = task
const running = task.status === 'processing'
emit('running-change', running)
if (!running) {
stopTaskPolling()
emit('completed', task)
void loadPreview()
}
} catch (error) {
taskError.value = parseApiError(error)
}
}
function cleanupStatusLabel(status: string): string {
if (status === 'processing') return '执行中'
if (status === 'failed') return '失败'
return '完成'
}
function cleanupStatusClass(status: string): string {
if (status === 'processing') return 'text-amber-500'
if (status === 'failed') return 'text-destructive'
return 'text-emerald-500'
}
function cleanupSummaryText(summary: Record<string, unknown>): string {
const total = typeof summary.total === 'number' ? summary.total : null
if (total !== null && total > 0) return `影响 ${total}`
const entries = Object.entries(summary)
.filter(([key, value]) => key !== 'progress_percent' && typeof value === 'number' && value > 0)
.map(([key, value]) => `${summaryLabel(key)} ${value}`)
return entries.length > 0 ? entries.join(' / ') : '等待后台返回结果'
}
function summaryLabel(key: string): string {
const labels: Record<string, string> = {
body_externalized: '详细体',
legacy_body_refs_migrated: '迁移',
body_cleaned: '清体',
header_cleaned: '清头',
keys_cleaned: 'Key',
records_deleted: '删记录',
}
return labels[key] || key
}
function formatCount(value: number): string {
return value.toLocaleString()
}
onBeforeUnmount(() => {
clearPreviewTimer()
stopTaskPolling()
})
</script>

View File

@@ -1,8 +1,11 @@
import { describe, expect, it } from 'vitest'
import {
allowedTargetsForMode,
defaultManualCleanupTargets,
MANUAL_USAGE_CLEANUP_CONFIRM_PHRASE,
isConfirmPhraseMatched,
normalizeManualCleanupTargets,
normalizeConfirmPhraseInput,
normalizeOlderThanDaysInput,
} from '../manualCleanupForm'
@@ -61,4 +64,34 @@ describe('manualCleanupForm', () => {
expect(normalizeOlderThanDaysInput('abc')).toBeNull()
})
})
describe('cleanup targets', () => {
it('keeps all ranges available for policy cleanup', () => {
expect(allowedTargetsForMode('policy')).toEqual([
'detail_body',
'compressed_body',
'headers',
'records',
])
expect(defaultManualCleanupTargets('older_than_days')).toEqual([
'detail_body',
'compressed_body',
'headers',
'records',
])
})
it('limits before-now cleanup to body targets', () => {
expect(allowedTargetsForMode('before_now')).toEqual([
'detail_body',
'compressed_body',
])
expect(normalizeManualCleanupTargets('before_now', [
'detail_body',
'headers',
'compressed_body',
'records',
])).toEqual(['detail_body', 'compressed_body'])
})
})
})

View File

@@ -1,5 +1,20 @@
export const MANUAL_USAGE_CLEANUP_CONFIRM_PHRASE = '确认清理'
export type ManualCleanupMode = 'policy' | 'older_than_days' | 'before_now'
export type ManualCleanupTarget = 'detail_body' | 'compressed_body' | 'headers' | 'records'
export const MANUAL_CLEANUP_TARGETS: ManualCleanupTarget[] = [
'detail_body',
'compressed_body',
'headers',
'records',
]
export const BEFORE_NOW_ALLOWED_TARGETS: ManualCleanupTarget[] = [
'detail_body',
'compressed_body',
]
export function normalizeConfirmPhraseInput(raw: string): string {
return raw.replace(/\r?\n/g, '').trim()
}
@@ -14,3 +29,21 @@ export function normalizeOlderThanDaysInput(raw: string | number | null | undefi
if (!Number.isFinite(parsed) || parsed <= 0) return null
return Math.floor(parsed)
}
export function allowedTargetsForMode(mode: ManualCleanupMode): ManualCleanupTarget[] {
return mode === 'before_now' ? BEFORE_NOW_ALLOWED_TARGETS : MANUAL_CLEANUP_TARGETS
}
export function normalizeManualCleanupTargets(
mode: ManualCleanupMode,
targets: ManualCleanupTarget[],
): ManualCleanupTarget[] {
const allowed = new Set(allowedTargetsForMode(mode))
return targets.filter((target, index) =>
allowed.has(target) && targets.indexOf(target) === index
)
}
export function defaultManualCleanupTargets(mode: ManualCleanupMode): ManualCleanupTarget[] {
return allowedTargetsForMode(mode)
}