From 9e74c80e2f827acef9f10c54dd0a72cd9501f203 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 19:36:49 -0400 Subject: [PATCH] feat(downloads): front-and-center "active now" panel with live elapsed timers New ActiveDownloadsPanel pinned at the top of the Downloads tab shows what's running (pulsing dot + live mm:ss timer counting from started_at) and what's queued, so the operator can see activity at a glance without the running filter. Backed by store.loadActive() which fetches running + pending independent of the feed filter; refreshed every 4s by the existing live poll. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../subscriptions/ActiveDownloadsPanel.vue | 133 ++++++++++++++++++ .../components/subscriptions/DownloadsTab.vue | 11 +- frontend/src/stores/downloads.js | 17 ++- 3 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/subscriptions/ActiveDownloadsPanel.vue diff --git a/frontend/src/components/subscriptions/ActiveDownloadsPanel.vue b/frontend/src/components/subscriptions/ActiveDownloadsPanel.vue new file mode 100644 index 0000000..e35abd6 --- /dev/null +++ b/frontend/src/components/subscriptions/ActiveDownloadsPanel.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/frontend/src/components/subscriptions/DownloadsTab.vue b/frontend/src/components/subscriptions/DownloadsTab.vue index 88e8da6..a1f5a15 100644 --- a/frontend/src/components/subscriptions/DownloadsTab.vue +++ b/frontend/src/components/subscriptions/DownloadsTab.vue @@ -43,6 +43,8 @@ + + {{ String(store.error) }} @@ -124,6 +126,7 @@ import DownloadDetailModal from '../downloads/DownloadDetailModal.vue' import DownloadStatChips from './DownloadStatChips.vue' import DownloadActivitySparkline from './DownloadActivitySparkline.vue' import FailingSourcesCard from './FailingSourcesCard.vue' +import ActiveDownloadsPanel from './ActiveDownloadsPanel.vue' import MaintenanceMenu from './MaintenanceMenu.vue' import DownloadsFilterPopover from './DownloadsFilterPopover.vue' @@ -160,6 +163,7 @@ async function refresh() { store.loadStats(24), store.loadActivity(24), store.loadFailing(), + store.loadActive(), ]) } @@ -218,9 +222,10 @@ function startPolling() { if (pollId) return pollId = setInterval(async () => { if (document.hidden) return - // Refresh stats + sparkline cheaply every tick; only reload the event - // list + failing rollup when there's active work (keeps idle ticks light). - await Promise.all([store.loadStats(24), store.loadActivity(24)]) + // Refresh stats + sparkline + active panel every tick (so new runs + // surface within 4s even from idle); only reload the full event list + + // failing rollup when there's active work (keeps idle ticks light). + await Promise.all([store.loadStats(24), store.loadActivity(24), store.loadActive()]) if (liveActive.value) await Promise.all([store.loadFirst(), store.loadFailing()]) }, POLL_MS) } diff --git a/frontend/src/stores/downloads.js b/frontend/src/stores/downloads.js index 564cb68..940d124 100644 --- a/frontend/src/stores/downloads.js +++ b/frontend/src/stores/downloads.js @@ -18,6 +18,10 @@ export const useDownloadsStore = defineStore('downloads', () => { 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 } @@ -75,10 +79,19 @@ export const useDownloadsStore = defineStore('downloads', () => { 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 + } + return { events, cursor, hasMore, filter, selected, loading, error, stats, - activity, failing, + activity, failing, activeEvents, loadFirst, loadMore, loadOne, applyFilter, closeDetail, loadStats, - loadActivity, loadFailing, + loadActivity, loadFailing, loadActive, } })