2358cedf3e
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>
88 lines
2.7 KiB
Vue
88 lines
2.7 KiB
Vue
<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>
|