feat(ui): group repeated failures by subscription on Dashboard recent activity
Consecutive failures on the same source no longer spam the feed as separate rows. Failed downloads are now bucketed per subscription and rendered as a single row with a failure count and the latest timestamp, while successful/running entries still render individually. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,10 @@
|
||||
:loading="checkingAll"
|
||||
@click="checkAllSources"
|
||||
>
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="18" width="2" class="mr-2" />
|
||||
Checking…
|
||||
</template>
|
||||
Check All Sources
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -62,7 +66,11 @@
|
||||
:disabled="!failedCount"
|
||||
@click="retryAllFailed"
|
||||
>
|
||||
Retry All Failed ({{ failedCount }})
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="18" width="2" class="mr-2" />
|
||||
Retrying…
|
||||
</template>
|
||||
Retry All Failed
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@@ -72,6 +80,10 @@
|
||||
:disabled="!stuckRunningCount"
|
||||
@click="resetOrphanedJobs"
|
||||
>
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="18" width="2" class="mr-2" />
|
||||
Resetting…
|
||||
</template>
|
||||
Reset Stuck ({{ stuckRunningCount }})
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
@@ -189,35 +201,48 @@
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list v-if="recentActivity.length">
|
||||
<v-list-item
|
||||
v-for="download in recentActivity"
|
||||
:key="download.id"
|
||||
>
|
||||
<v-list v-if="recentActivityGrouped.length">
|
||||
<template v-for="entry in recentActivityGrouped" :key="entry.key">
|
||||
<v-list-item v-if="entry.kind === 'single'">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="getStatusColor(download.status)">
|
||||
{{ getStatusIcon(download.status) }}
|
||||
<v-icon :color="getStatusColor(entry.download.status)">
|
||||
{{ getStatusIcon(entry.download.status) }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ getDownloadLabel(download) }}
|
||||
{{ getDownloadLabel(entry.download) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ formatRelativeTime(download.created_at) }}
|
||||
<span v-if="download.file_count > 0" class="text-success ml-2">
|
||||
{{ download.file_count }} new file{{ download.file_count !== 1 ? 's' : '' }}
|
||||
{{ formatRelativeTime(entry.download.created_at) }}
|
||||
<span v-if="entry.download.file_count > 0" class="text-success ml-2">
|
||||
{{ entry.download.file_count }} new file{{ entry.download.file_count !== 1 ? 's' : '' }}
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-chip
|
||||
:color="getStatusColor(download.status)"
|
||||
:color="getStatusColor(entry.download.status)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ download.status === 'completed' ? 'new content' : download.status }}
|
||||
{{ entry.download.status === 'completed' ? 'new content' : entry.download.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item v-else>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ entry.label }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ entry.items.length }} failure{{ entry.items.length !== 1 ? 's' : '' }} · latest {{ formatRelativeTime(entry.latest.created_at) }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-chip color="error" size="small" variant="tonal">
|
||||
{{ entry.items.length }}× failed
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="48" color="secondary" :style="{ opacity: 0.5 }">mdi-history</v-icon>
|
||||
@@ -354,6 +379,29 @@ let refreshInterval = null
|
||||
const stats = computed(() => downloadsStore.stats)
|
||||
const recentActivity = computed(() => downloadsStore.recentActivity)
|
||||
|
||||
const recentActivityGrouped = computed(() => {
|
||||
const singles = []
|
||||
const groups = new Map()
|
||||
for (const d of recentActivity.value) {
|
||||
if (d.status === 'failed') {
|
||||
const key = `${d.subscription_name || '?'}:${d.platform || '?'}`
|
||||
const g = groups.get(key) || { kind: 'group', key, label: key, items: [], latest: d }
|
||||
g.items.push(d)
|
||||
if (new Date(d.created_at) > new Date(g.latest.created_at)) g.latest = d
|
||||
groups.set(key, g)
|
||||
} else {
|
||||
singles.push({ kind: 'single', key: `d-${d.id}`, download: d })
|
||||
}
|
||||
}
|
||||
const out = [...singles, ...groups.values()]
|
||||
out.sort((a, b) => {
|
||||
const tA = new Date(a.kind === 'single' ? a.download.created_at : a.latest.created_at)
|
||||
const tB = new Date(b.kind === 'single' ? b.download.created_at : b.latest.created_at)
|
||||
return tB - tA
|
||||
})
|
||||
return out
|
||||
})
|
||||
|
||||
// Read threshold from settings, default 5 if unset or invalid
|
||||
const failureThreshold = computed(() => {
|
||||
const n = Number(settingsStore.settings['dashboard.failure_threshold'])
|
||||
|
||||
Reference in New Issue
Block a user