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
+14
View File
@@ -16,6 +16,8 @@ export const useDownloadsStore = defineStore('downloads', () => {
const loading = ref(false)
const error = ref(null)
const stats = ref({ pending: 0, running: 0, ok: 0, error: 0, skipped: 0 })
const activity = ref({ hours: 24, buckets: [] })
const failing = ref([])
function _params(extra = {}) {
const out = { limit: 50, ...extra }
@@ -74,8 +76,20 @@ export const useDownloadsStore = defineStore('downloads', () => {
return stats.value
}
async function loadActivity(hours = 24) {
activity.value = await api.get('/api/downloads/activity', { params: { hours } })
return activity.value
}
async function loadFailing() {
failing.value = await api.get('/api/sources', { params: { failing: true } })
return failing.value
}
return {
events, cursor, hasMore, filter, selected, loading, error, stats,
activity, failing,
loadFirst, loadMore, loadOne, applyFilter, closeDetail, loadStats,
loadActivity, loadFailing,
}
})
+8 -1
View File
@@ -9,6 +9,7 @@ export const useSourcesStore = defineStore('sources', () => {
const byArtist = ref(new Map())
const loading = ref(false)
const error = ref(null)
const scheduleStatus = ref(null)
const allSources = computed(() => byArtist.value.get(null) ?? [])
@@ -70,6 +71,11 @@ export const useSourcesStore = defineStore('sources', () => {
return await api.get('/api/artists/autocomplete', { params: { q: query, limit } })
}
async function loadScheduleStatus() {
scheduleStatus.value = await api.get('/api/sources/schedule-status')
return scheduleStatus.value
}
// FC-3c: trigger a download for one source. Returns {download_event_id,status}.
const checkingIds = ref(new Set())
@@ -104,13 +110,14 @@ export const useSourcesStore = defineStore('sources', () => {
}
return {
byArtist, loading, error,
byArtist, loading, error, scheduleStatus,
allSources,
checkingIds,
loadAll, loadForArtist,
create, update, remove,
checkNow,
findOrCreateArtist, autocompleteArtist,
loadScheduleStatus,
sourcesByArtistGrouped,
}
})