diff --git a/src/composables/useDashboardStats.ts b/src/composables/useDashboardStats.ts index 59bcc48..ded52d0 100644 --- a/src/composables/useDashboardStats.ts +++ b/src/composables/useDashboardStats.ts @@ -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 | 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 => { + 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) diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 983e3ae..5c248a1 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -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', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 4d53d0d..2dd0e81 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -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: '当前结果', diff --git a/src/services/stats.ts b/src/services/stats.ts index 0eabfd6..8b2b6ab 100644 --- a/src/services/stats.ts +++ b/src/services/stats.ts @@ -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> { + return api.get('/admin/dashboard/maintenance-queue') + } + static async getActivities( params: DashboardActivitiesParams ): Promise> { diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index f7b0e18..ea3dc5a 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -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 + maintenance_summary?: Partial } 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 + maintenanceSummary?: Partial + maintenance_summary?: Partial +} + +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 diff --git a/src/views/manage/DashboardView.vue b/src/views/manage/DashboardView.vue index c60233d..b1c7c09 100644 --- a/src/views/manage/DashboardView.vue +++ b/src/views/manage/DashboardView.vue @@ -134,6 +134,80 @@ +
+
+
+

+ {{ t('admin.dashboard.maintenanceQueueTitle') }} +

+

+ {{ t('admin.dashboard.maintenanceQueueDesc') }} +

+
+ +
+
+

+ {{ t('admin.dashboard.maintenanceActionable') }} +

+ + {{ dashboardData.maintenanceSummary.actionableCount }} + +
+
+

+ {{ t('admin.dashboard.maintenanceFileQueue') }} +

+ + {{ dashboardData.maintenanceSummary.fileQueueCount }} + +
+
+

+ {{ t('admin.dashboard.maintenanceSettings') }} +

+ + {{ dashboardData.maintenanceSummary.settingsCount }} + +
+
+
+ +
+ +
+
+
@@ -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')