import { defineStore } from 'pinia' import { ref } from 'vue' import { useApi } from '../composables/useApi.js' import { useAsyncAction } from '../composables/useAsyncAction.js' export const useDownloadsStore = defineStore('downloads', () => { const api = useApi() const events = ref([]) const cursor = ref(null) const hasMore = ref(true) const filter = ref({ status: null, source_id: null, artist_id: null, from_date: null, to_date: null, }) const selected = ref(null) const { loading, error, run } = useAsyncAction() const stats = ref({ pending: 0, running: 0, ok: 0, error: 0, skipped: 0 }) const activity = ref({ hours: 24, buckets: [] }) const failing = ref([]) // Running + queued events, fetched independent of the feed's filter so // the "active now" panel always reflects what's happening regardless of // how the operator has filtered the historical list below. const activeEvents = ref([]) function _params(extra = {}) { const out = { limit: 50, ...extra } if (filter.value.status) out.status = filter.value.status if (filter.value.source_id != null) out.source_id = filter.value.source_id if (filter.value.artist_id != null) out.artist_id = filter.value.artist_id return out } async function loadFirst() { await run(async () => { const body = await api.get('/api/downloads', { params: _params() }) events.value = body cursor.value = body.length ? body[body.length - 1].id : null hasMore.value = body.length === 50 }) } async function loadMore() { if (!hasMore.value || cursor.value == null) return await run(async () => { const body = await api.get('/api/downloads', { params: _params({ before: cursor.value }) }) events.value.push(...body) cursor.value = body.length ? body[body.length - 1].id : cursor.value hasMore.value = body.length === 50 }) } async function loadOne(id) { selected.value = await api.get(`/api/downloads/${id}`) return selected.value } async function applyFilter(patch) { filter.value = { ...filter.value, ...patch } await loadFirst() } function closeDetail() { selected.value = null } async function loadStats(windowHours = 24) { stats.value = await api.get('/api/downloads/stats', { params: { window_hours: windowHours } }) 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 } async function loadActive() { const [running, pending] = await Promise.all([ api.get('/api/downloads', { params: { status: 'running', limit: 50 } }), api.get('/api/downloads', { params: { status: 'pending', limit: 50 } }), ]) activeEvents.value = [...running, ...pending] return activeEvents.value } // POSTs to the download-recovery sweep endpoint (fire-and-forget — the // Beat schedule also runs it every 5 min). The caller should refresh // failing/stats a few seconds after this resolves to see swept rows. async function recoverStalled() { return api.post('/api/downloads/recover-stalled') } return { events, cursor, hasMore, filter, selected, loading, error, stats, activity, failing, activeEvents, loadFirst, loadMore, loadOne, applyFilter, closeDetail, loadStats, loadActivity, loadFailing, loadActive, recoverStalled, } })