feat(dashboards): scheduler health strip, failing-source rollup, 24h activity sparkline, credential staleness nudge

D1 scheduler visibility: AppSetting last-tick stamp on every Beat tick +
GET /api/sources/schedule-status (last_tick_at/next_due_at/due_now/auto_sources)
+ SchedulerStatusBar on the Subscriptions tab (re-polled every 30s).

D2 failing-source rollup: ?failing=true on the sources list + FailingSourcesCard
on Downloads with per-source and bulk "retry" (re-runs the feed via /check).

D3 activity sparkline: GET /api/downloads/activity hourly buckets + CSS bar
chart by the stat chips (failures stacked in error color); refreshes on live poll.

D4 credential staleness: surface last_verified age + "re-verify recommended"
warning past 30d; also fixes the dead last_verified_at field-name mismatch so
the verification row renders at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 08:30:14 -04:00
parent 215a8993a1
commit 2358cedf3e
15 changed files with 623 additions and 20 deletions
@@ -0,0 +1,87 @@
<template>
<v-card v-if="sources.length" variant="tonal" color="error" class="fc-fail mb-4">
<div class="fc-fail__head" @click="open = !open">
<v-icon size="small">mdi-alert-circle</v-icon>
<span class="fc-fail__title">
{{ sources.length }}
{{ sources.length === 1 ? 'source is' : 'sources are' }} failing
</span>
<v-spacer />
<v-btn
size="small" variant="text" prepend-icon="mdi-refresh"
:loading="retryingAll"
@click.stop="$emit('retry-all', sources)"
>
Retry all
</v-btn>
<v-icon size="small">{{ open ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</div>
<v-expand-transition>
<div v-if="open" class="fc-fail__body">
<div v-for="s in sources" :key="s.id" class="fc-fail__row">
<PlatformChip :platform="s.platform" size="x-small" />
<span class="fc-fail__artist">{{ s.artist_name || `#${s.artist_id}` }}</span>
<v-chip size="x-small" color="error" variant="flat" label class="fc-fail__count">
{{ s.consecutive_failures }}× failed
</v-chip>
<span class="fc-fail__err" :title="s.last_error || ''">
{{ s.last_error || 'no error message recorded' }}
</span>
<v-spacer />
<v-btn
size="x-small" variant="text" prepend-icon="mdi-refresh"
:loading="retryingIds.has(s.id)"
@click="$emit('retry', s)"
>
Retry
</v-btn>
</div>
</div>
</v-expand-transition>
</v-card>
</template>
<script setup>
import { ref } from 'vue'
import PlatformChip from './PlatformChip.vue'
defineProps({
// source records from /api/sources?failing=true
sources: { type: Array, default: () => [] },
retryingIds: { type: Set, default: () => new Set() },
retryingAll: { type: Boolean, default: false },
})
defineEmits(['retry', 'retry-all'])
const open = ref(true)
</script>
<style scoped>
.fc-fail { border-radius: 8px; }
.fc-fail__head {
display: flex; align-items: center; gap: 8px;
padding: 10px 14px;
cursor: pointer;
user-select: none;
}
.fc-fail__title { font-weight: 600; }
.fc-fail__body {
padding: 0 14px 10px;
display: flex; flex-direction: column; gap: 2px;
}
.fc-fail__row {
display: flex; align-items: center; gap: 10px;
padding: 6px 8px;
border-radius: 4px;
background: rgb(var(--v-theme-surface) / 0.4);
}
.fc-fail__artist { font-weight: 600; white-space: nowrap; }
.fc-fail__count { flex: 0 0 auto; }
.fc-fail__err {
color: rgb(var(--v-theme-on-surface-variant));
font-size: 0.8rem;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
max-width: 46ch;
}
</style>