feat: add admin maintenance queue

This commit is contained in:
Lan
2026-06-03 08:18:41 +08:00
parent 3e252b35ea
commit e8fdcc7f43
6 changed files with 532 additions and 45 deletions
+160 -44
View File
@@ -8,6 +8,10 @@ import type {
DashboardData,
DashboardHealthSummary,
DashboardInsightSeverity,
DashboardMaintenanceQueue,
DashboardMaintenanceQueueItem,
DashboardMaintenanceQueueSummary,
DashboardMaintenanceQueueViewItem,
DashboardOperationalInsight,
DashboardOperationalInsightViewItem,
DashboardViewData
@@ -21,50 +25,54 @@ type UseDashboardStatsOptions = {
const activityTimelineLimit = 40
const emptyDashboardData = (): DashboardViewData => ({
hasExtendedStats: false,
totalFiles: 0,
storageUsed: 0,
yesterdayCount: 0,
todayCount: 0,
yesterdaySize: 0,
todaySize: 0,
sysUptime: null,
activeCount: 0,
expiredCount: 0,
textCount: 0,
fileCount: 0,
chunkedCount: 0,
usedCount: 0,
storageBackend: '-',
uploadSizeLimit: 0,
openUpload: 0,
enableChunk: 0,
maxSaveSeconds: 0,
healthAttentionCount: 0,
healthDangerCount: 0,
healthWarningCount: 0,
expiringSoonCount: 0,
storageIssueCount: 0,
neverRetrievedCount: 0,
healthyCount: 0,
permanentCount: 0,
topSuffixes: [],
recentFiles: [],
recentActivities: [],
operationalInsights: [],
storageUsedText: '0 Bytes',
yesterdaySizeText: '0 Bytes',
todaySizeText: '0 Bytes',
uploadSizeLimitText: '0 Bytes',
sysUptimeText: '-',
activeRatio: 0,
textRatio: 0,
fileRatio: 0,
healthyRatio: 0,
healthAttentionRatio: 0,
todaySizeRatio: 0
})
function emptyDashboardData(): DashboardViewData {
return {
hasExtendedStats: false,
totalFiles: 0,
storageUsed: 0,
yesterdayCount: 0,
todayCount: 0,
yesterdaySize: 0,
todaySize: 0,
sysUptime: null,
activeCount: 0,
expiredCount: 0,
textCount: 0,
fileCount: 0,
chunkedCount: 0,
usedCount: 0,
storageBackend: '-',
uploadSizeLimit: 0,
openUpload: 0,
enableChunk: 0,
maxSaveSeconds: 0,
healthAttentionCount: 0,
healthDangerCount: 0,
healthWarningCount: 0,
expiringSoonCount: 0,
storageIssueCount: 0,
neverRetrievedCount: 0,
healthyCount: 0,
permanentCount: 0,
topSuffixes: [],
recentFiles: [],
recentActivities: [],
operationalInsights: [],
maintenanceItems: [],
maintenanceSummary: emptyMaintenanceSummary(),
storageUsedText: '0 Bytes',
yesterdaySizeText: '0 Bytes',
todaySizeText: '0 Bytes',
uploadSizeLimitText: '0 Bytes',
sysUptimeText: '-',
activeRatio: 0,
textRatio: 0,
fileRatio: 0,
healthyRatio: 0,
healthAttentionRatio: 0,
todaySizeRatio: 0
}
}
const toNumber = (value: number | string | null | undefined) => Number(value || 0)
@@ -152,6 +160,111 @@ const normalizeOperationalInsights = (
}
})
function emptyMaintenanceSummary(): DashboardMaintenanceQueueSummary {
return {
total: 0,
actionableCount: 0,
dangerCount: 0,
warningCount: 0,
successCount: 0,
neutralCount: 0,
fileQueueCount: 0,
settingsCount: 0,
strongestSeverity: 'success'
}
}
const normalizeMaintenanceQueueItems = (
items: DashboardMaintenanceQueueItem[] = []
): DashboardMaintenanceQueueViewItem[] =>
items
.filter((item) => item && item.key)
.map((item) => {
const action = item.action && typeof item.action === 'object' ? item.action : {}
const actionTypeValue =
item.actionType ??
item.action_type ??
action.actionType ??
action.action_type ??
action.type ??
'file_queue'
const targetHealthValue =
item.targetHealth ??
item.target_health ??
action.targetHealth ??
action.target_health ??
action.health ??
null
const suggestedActionValue =
item.suggestedAction ??
item.suggested_action ??
action.suggestedAction ??
action.suggested_action ??
'monitor'
return {
...item,
count: toNumber(item.count),
priority: toNumber(item.priority),
severity: normalizeInsightSeverity(item.severity),
actionTypeValue,
suggestedActionValue,
targetHealthValue
}
})
const normalizeMaintenanceSummary = (
summary: Partial<DashboardMaintenanceQueueSummary> | undefined,
items: DashboardMaintenanceQueueViewItem[]
): DashboardMaintenanceQueueSummary => ({
total: toNumber(summary?.total ?? items.length),
actionableCount: toNumber(summary?.actionableCount ?? summary?.actionable_count),
actionable_count: toNumber(summary?.actionableCount ?? summary?.actionable_count),
dangerCount: toNumber(summary?.dangerCount ?? summary?.danger_count),
danger_count: toNumber(summary?.dangerCount ?? summary?.danger_count),
warningCount: toNumber(summary?.warningCount ?? summary?.warning_count),
warning_count: toNumber(summary?.warningCount ?? summary?.warning_count),
successCount: toNumber(summary?.successCount ?? summary?.success_count),
success_count: toNumber(summary?.successCount ?? summary?.success_count),
neutralCount: toNumber(summary?.neutralCount ?? summary?.neutral_count),
neutral_count: toNumber(summary?.neutralCount ?? summary?.neutral_count),
fileQueueCount: toNumber(summary?.fileQueueCount ?? summary?.file_queue_count),
file_queue_count: toNumber(summary?.fileQueueCount ?? summary?.file_queue_count),
settingsCount: toNumber(summary?.settingsCount ?? summary?.settings_count),
settings_count: toNumber(summary?.settingsCount ?? summary?.settings_count),
strongestSeverity: normalizeInsightSeverity(
summary?.strongestSeverity ?? summary?.strongest_severity
),
strongest_severity: normalizeInsightSeverity(
summary?.strongestSeverity ?? summary?.strongest_severity
)
})
const normalizeMaintenanceQueue = (
detail: DashboardData | DashboardMaintenanceQueue
): Pick<DashboardViewData, 'maintenanceItems' | 'maintenanceSummary'> => {
const source = detail as DashboardData & DashboardMaintenanceQueue
const queue = source.maintenanceQueue ?? source.maintenance_queue ?? source
const items = normalizeMaintenanceQueueItems(
queue?.items ??
queue?.maintenanceItems ??
queue?.maintenance_items ??
source.maintenanceItems ??
source.maintenance_items ??
[]
)
const rawSummary =
queue?.summary ??
queue?.maintenanceSummary ??
queue?.maintenance_summary ??
source.maintenanceSummary ??
source.maintenance_summary
return {
maintenanceItems: items,
maintenanceSummary: normalizeMaintenanceSummary(rawSummary, items)
}
}
const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [
'healthAttentionCount',
'healthDangerCount',
@@ -256,6 +369,9 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
dashboardData.operationalInsights = normalizeOperationalInsights(
detail.operationalInsights ?? detail.operational_insights ?? detail.insights ?? []
)
const maintenanceQueue = normalizeMaintenanceQueue(detail)
dashboardData.maintenanceItems = maintenanceQueue.maintenanceItems
dashboardData.maintenanceSummary = maintenanceQueue.maintenanceSummary
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
+96
View File
@@ -101,6 +101,54 @@ export default {
success: 'Stable',
neutral: 'Suggestion'
},
maintenanceQueueTitle: 'Maintenance Queue',
maintenanceQueueDesc:
'Groups file health, retention policy, and upload settings into actionable queues. Click an item to open the related view.',
maintenanceActionable: 'Actionable',
maintenanceFileQueue: 'File queue',
maintenanceSettings: 'Settings',
maintenanceQueueActionSettings: 'Adjust settings',
maintenanceQueueActionFileQueue: 'View queue',
maintenanceQueueItems: {
storage_issue: {
title: '{count} storage issues',
description:
'Review records missing download metadata and verify storage settings and file details.'
},
expired_cleanup: {
title: '{count} expired items',
description:
'Review expired shares, delete stale records, or extend files that should stay available.'
},
expiring_soon: {
title: '{count} expiring soon',
description: 'These shares will expire soon. Extend them early or notify recipients.'
},
never_retrieved: {
title: '{count} never retrieved',
description:
'Review shares that have not been retrieved to identify ineffective uploads or follow-up gaps.'
},
permanent_review: {
title: '{count} permanent shares',
description:
'Periodically review long-retention shares to prevent unbounded historical growth.'
},
guest_upload_retention: {
title: 'Guest uploads lack retention limits',
description:
'Guest uploads are enabled without a maximum retention period. Add a default retention policy.'
},
chunking_disabled: {
title: 'Large-file experience can improve',
description:
"Today's capacity is near the single-file limit. Enable chunked upload for better reliability."
},
healthy: {
title: 'Maintenance queue is stable',
description: 'No maintenance item needs priority attention right now.'
}
},
viewAllActivities: 'View all',
activityTimelineTitle: 'Activity Timeline',
activityTimelineFiltered: 'Current Results',
@@ -993,6 +1041,54 @@ export default {
success: 'Stable',
neutral: 'Suggestion'
},
maintenanceQueueTitle: 'Maintenance Queue',
maintenanceQueueDesc:
'Groups file health, retention policy, and upload settings into actionable queues. Click an item to open the related view.',
maintenanceActionable: 'Actionable',
maintenanceFileQueue: 'File queue',
maintenanceSettings: 'Settings',
maintenanceQueueActionSettings: 'Adjust settings',
maintenanceQueueActionFileQueue: 'View queue',
maintenanceQueueItems: {
storage_issue: {
title: '{count} storage issues',
description:
'Review records missing download metadata and verify storage settings and file details.'
},
expired_cleanup: {
title: '{count} expired items',
description:
'Review expired shares, delete stale records, or extend files that should stay available.'
},
expiring_soon: {
title: '{count} expiring soon',
description: 'These shares will expire soon. Extend them early or notify recipients.'
},
never_retrieved: {
title: '{count} never retrieved',
description:
'Review shares that have not been retrieved to identify ineffective uploads or follow-up gaps.'
},
permanent_review: {
title: '{count} permanent shares',
description:
'Periodically review long-retention shares to prevent unbounded historical growth.'
},
guest_upload_retention: {
title: 'Guest uploads lack retention limits',
description:
'Guest uploads are enabled without a maximum retention period. Add a default retention policy.'
},
chunking_disabled: {
title: 'Large-file experience can improve',
description:
"Today's capacity is near the single-file limit. Enable chunked upload for better reliability."
},
healthy: {
title: 'Maintenance queue is stable',
description: 'No maintenance item needs priority attention right now.'
}
},
viewAllActivities: 'View all',
activityTimelineTitle: 'Activity Timeline',
activityTimelineFiltered: 'Current Results',
+82
View File
@@ -100,6 +100,47 @@ export default {
success: '稳定',
neutral: '建议'
},
maintenanceQueueTitle: '维护队列',
maintenanceQueueDesc: '把文件健康、保留策略和上传配置聚合成可处理队列,点击进入对应视图。',
maintenanceActionable: '待处理',
maintenanceFileQueue: '文件队列',
maintenanceSettings: '配置项',
maintenanceQueueActionSettings: '调整设置',
maintenanceQueueActionFileQueue: '查看队列',
maintenanceQueueItems: {
storage_issue: {
title: '存储异常 {count} 项',
description: '优先核对缺少下载元数据的记录,确认存储配置和文件详情。'
},
expired_cleanup: {
title: '已过期 {count} 项',
description: '集中检查过期分享,删除无效记录或续期仍需保留的文件。'
},
expiring_soon: {
title: '即将过期 {count} 项',
description: '这些分享将在短时间内失效,适合提前续期或通知使用方。'
},
never_retrieved: {
title: '未取件 {count} 项',
description: '查看创建后尚未被取件的分享,用于识别无效上传或触达缺口。'
},
permanent_review: {
title: '永久有效 {count} 项',
description: '定期复核长期保留的分享,避免历史文件无限增长。'
},
guest_upload_retention: {
title: '游客上传缺少保留上限',
description: '游客上传已开启且没有最长保存时间,建议补充默认保留策略。'
},
chunking_disabled: {
title: '大文件体验可优化',
description: '今日容量接近单文件上限,开启分片上传可提升上传稳定性。'
},
healthy: {
title: '维护队列稳定',
description: '当前未发现需要优先处理的维护事项。'
}
},
viewAllActivities: '查看全部',
activityTimelineTitle: '操作时间线',
activityTimelineFiltered: '当前结果',
@@ -938,6 +979,47 @@ export default {
success: '稳定',
neutral: '建议'
},
maintenanceQueueTitle: '维护队列',
maintenanceQueueDesc: '把文件健康、保留策略和上传配置聚合成可处理队列,点击进入对应视图。',
maintenanceActionable: '待处理',
maintenanceFileQueue: '文件队列',
maintenanceSettings: '配置项',
maintenanceQueueActionSettings: '调整设置',
maintenanceQueueActionFileQueue: '查看队列',
maintenanceQueueItems: {
storage_issue: {
title: '存储异常 {count} 项',
description: '优先核对缺少下载元数据的记录,确认存储配置和文件详情。'
},
expired_cleanup: {
title: '已过期 {count} 项',
description: '集中检查过期分享,删除无效记录或续期仍需保留的文件。'
},
expiring_soon: {
title: '即将过期 {count} 项',
description: '这些分享将在短时间内失效,适合提前续期或通知使用方。'
},
never_retrieved: {
title: '未取件 {count} 项',
description: '查看创建后尚未被取件的分享,用于识别无效上传或触达缺口。'
},
permanent_review: {
title: '永久有效 {count} 项',
description: '定期复核长期保留的分享,避免历史文件无限增长。'
},
guest_upload_retention: {
title: '游客上传缺少保留上限',
description: '游客上传已开启且没有最长保存时间,建议补充默认保留策略。'
},
chunking_disabled: {
title: '大文件体验可优化',
description: '今日容量接近单文件上限,开启分片上传可提升上传稳定性。'
},
healthy: {
title: '维护队列稳定',
description: '当前未发现需要优先处理的维护事项。'
}
},
viewAllActivities: '查看全部',
activityTimelineTitle: '操作时间线',
activityTimelineFiltered: '当前结果',
+6 -1
View File
@@ -3,7 +3,8 @@ import type {
ApiResponse,
DashboardActivitiesParams,
DashboardActivitiesResponse,
DashboardData
DashboardData,
DashboardMaintenanceQueue
} from '@/types'
export class StatsService {
@@ -11,6 +12,10 @@ export class StatsService {
return api.get('/admin/dashboard')
}
static async getMaintenanceQueue(): Promise<ApiResponse<DashboardMaintenanceQueue>> {
return api.get('/admin/dashboard/maintenance-queue')
}
static async getActivities(
params: DashboardActivitiesParams
): Promise<ApiResponse<DashboardActivitiesResponse>> {
+77
View File
@@ -46,6 +46,12 @@ export interface DashboardData {
operationalInsights?: DashboardOperationalInsight[]
operational_insights?: DashboardOperationalInsight[]
insights?: DashboardOperationalInsight[]
maintenanceQueue?: DashboardMaintenanceQueue
maintenance_queue?: DashboardMaintenanceQueue
maintenanceItems?: DashboardMaintenanceQueueItem[]
maintenance_items?: DashboardMaintenanceQueueItem[]
maintenanceSummary?: Partial<DashboardMaintenanceQueueSummary>
maintenance_summary?: Partial<DashboardMaintenanceQueueSummary>
}
export interface DashboardSuffixStat {
@@ -125,6 +131,69 @@ export interface DashboardActivitiesResponse {
export type DashboardInsightSeverity = 'danger' | 'warning' | 'success' | 'neutral'
export interface DashboardMaintenanceQueueAction {
type?: string
actionType?: string
action_type?: string
health?: AdminFileHealthFilter | string | null
targetHealth?: AdminFileHealthFilter | string | null
target_health?: AdminFileHealthFilter | string | null
suggestedAction?: string
suggested_action?: string
}
export interface DashboardMaintenanceQueueItem {
key: string
severity: DashboardInsightSeverity | string
category?: string
priority?: number
count: number
action?: DashboardMaintenanceQueueAction
actionType?: string
action_type?: string
suggestedAction?: string
suggested_action?: string
targetHealth?: AdminFileHealthFilter | string | null
target_health?: AdminFileHealthFilter | string | null
}
export interface DashboardMaintenanceQueueSummary {
total: number
actionableCount: number
actionable_count?: number
dangerCount: number
danger_count?: number
warningCount: number
warning_count?: number
successCount: number
success_count?: number
neutralCount: number
neutral_count?: number
fileQueueCount: number
file_queue_count?: number
settingsCount: number
settings_count?: number
strongestSeverity: DashboardInsightSeverity
strongest_severity?: DashboardInsightSeverity
}
export interface DashboardMaintenanceQueue {
items?: DashboardMaintenanceQueueItem[]
maintenanceItems?: DashboardMaintenanceQueueItem[]
maintenance_items?: DashboardMaintenanceQueueItem[]
summary?: Partial<DashboardMaintenanceQueueSummary>
maintenanceSummary?: Partial<DashboardMaintenanceQueueSummary>
maintenance_summary?: Partial<DashboardMaintenanceQueueSummary>
}
export interface DashboardMaintenanceQueueViewItem extends DashboardMaintenanceQueueItem {
severity: DashboardInsightSeverity
priority: number
actionTypeValue: string
suggestedActionValue: string
targetHealthValue: AdminFileHealthFilter | string | null
}
export interface DashboardOperationalInsightAction {
type?: string
actionType?: string
@@ -174,6 +243,12 @@ export type DashboardViewData = Omit<
| 'operationalInsights'
| 'operational_insights'
| 'insights'
| 'maintenanceQueue'
| 'maintenance_queue'
| 'maintenanceItems'
| 'maintenance_items'
| 'maintenanceSummary'
| 'maintenance_summary'
| 'storageUsed'
| 'yesterdaySize'
| 'todaySize'
@@ -195,6 +270,8 @@ export type DashboardViewData = Omit<
recentFiles: DashboardRecentFile[]
recentActivities: DashboardActivityViewItem[]
operationalInsights: DashboardOperationalInsightViewItem[]
maintenanceItems: DashboardMaintenanceQueueViewItem[]
maintenanceSummary: DashboardMaintenanceQueueSummary
storageUsed: number
yesterdaySize: number
todaySize: number
+111
View File
@@ -134,6 +134,80 @@
</div>
</section>
<section
v-if="dashboardData.hasExtendedStats && dashboardData.maintenanceItems.length > 0"
class="mt-6 rounded-lg p-5 shadow-sm"
:class="[panelClass]"
>
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div>
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
{{ t('admin.dashboard.maintenanceQueueTitle') }}
</h3>
<p class="text-sm" :class="[mutedTextClass]">
{{ t('admin.dashboard.maintenanceQueueDesc') }}
</p>
</div>
<div class="grid grid-cols-3 gap-2 sm:min-w-[24rem]">
<div class="rounded-lg border px-3 py-2" :class="[subtlePanelClass]">
<p class="text-xs" :class="[mutedTextClass]">
{{ t('admin.dashboard.maintenanceActionable') }}
</p>
<strong class="mt-1 block text-xl" :class="[primaryTextClass]">
{{ dashboardData.maintenanceSummary.actionableCount }}
</strong>
</div>
<div class="rounded-lg border px-3 py-2" :class="[subtlePanelClass]">
<p class="text-xs" :class="[mutedTextClass]">
{{ t('admin.dashboard.maintenanceFileQueue') }}
</p>
<strong class="mt-1 block text-xl" :class="[primaryTextClass]">
{{ dashboardData.maintenanceSummary.fileQueueCount }}
</strong>
</div>
<div class="rounded-lg border px-3 py-2" :class="[subtlePanelClass]">
<p class="text-xs" :class="[mutedTextClass]">
{{ t('admin.dashboard.maintenanceSettings') }}
</p>
<strong class="mt-1 block text-xl" :class="[primaryTextClass]">
{{ dashboardData.maintenanceSummary.settingsCount }}
</strong>
</div>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<button
v-for="item in dashboardData.maintenanceItems"
:key="item.key"
type="button"
class="group flex min-h-36 flex-col rounded-lg border p-4 text-left transition-colors"
:class="getMaintenanceQueueClass(item.severity)"
@click="openMaintenanceQueueItem(item)"
>
<span class="flex items-start justify-between gap-3">
<span class="rounded-full px-2.5 py-1 text-xs font-medium ring-1 ring-current/15">
{{ getOperationalInsightSeverityLabel(item.severity) }}
</span>
<component :is="getMaintenanceQueueIcon(item.severity)" class="h-5 w-5 shrink-0" />
</span>
<strong class="mt-3 text-base">
{{ getMaintenanceQueueTitle(item) }}
</strong>
<span class="mt-2 line-clamp-2 text-sm opacity-80">
{{ getMaintenanceQueueDescription(item) }}
</span>
<span class="mt-auto flex items-center justify-between gap-2 pt-4 text-sm font-medium">
<span>{{ getMaintenanceQueueActionLabel(item) }}</span>
<ArrowRightIcon
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
/>
</span>
</button>
</div>
</section>
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
<section class="xl:col-span-2 rounded-lg p-5 shadow-sm" :class="[panelClass]">
<div class="mb-5 flex items-center justify-between">
@@ -739,6 +813,7 @@ import type {
DashboardActivityViewItem,
DashboardHealthAction,
DashboardInsightSeverity,
DashboardMaintenanceQueueViewItem,
DashboardOperationalInsightViewItem
} from '@/types'
@@ -912,6 +987,42 @@ const openOperationalInsight = (insight: DashboardOperationalInsightViewItem) =>
})
}
const getMaintenanceQueueIcon = (severity: DashboardInsightSeverity) =>
operationalInsightIconMap[severity]
const getMaintenanceQueueClass = (severity: DashboardInsightSeverity) =>
getOperationalInsightClass(severity)
const getMaintenanceQueueTitle = (item: DashboardMaintenanceQueueViewItem) => {
const key = `admin.dashboard.maintenanceQueueItems.${item.key}.title`
const title = t(key, { count: item.count })
return title === key ? item.key : title
}
const getMaintenanceQueueDescription = (item: DashboardMaintenanceQueueViewItem) => {
const key = `admin.dashboard.maintenanceQueueItems.${item.key}.description`
const description = t(key, { count: item.count })
return description === key ? item.key : description
}
const getMaintenanceQueueActionLabel = (item: DashboardMaintenanceQueueViewItem) =>
item.actionTypeValue === 'settings'
? t('admin.dashboard.maintenanceQueueActionSettings')
: t('admin.dashboard.maintenanceQueueActionFileQueue')
const openMaintenanceQueueItem = (item: DashboardMaintenanceQueueViewItem) => {
if (item.actionTypeValue === 'settings') {
void router.push({ path: ROUTES.SETTINGS })
return
}
const health = item.targetHealthValue
void router.push({
path: ROUTES.FILE_MANAGE,
query: health ? { health } : {}
})
}
const formatCreatedAt = (value: string | null) => {
if (!value) return '-'
return formatTimestamp(value, 'datetime')