feat(ia): wave 3 — Subscriptions landing answers 'what needs me, what came in?'
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 38s
CI / frontend-build (push) Successful in 20s
CI / integration (push) Successful in 3m30s

Daily-use reorder of the Subscriptions tab: needs-attention strip first
(FailingSourcesCard moves up from below the Downloads fold — a broken
subscription was invisible unless you went looking), then a new Recent
arrivals card (real downloads only, no-change scans filtered out, artist
links), then the source list. Both cards render nothing when there's nothing
to say.

Retry logic moves into the downloads store (retrySource / retryAllFailing) so
the needs-attention card and the Downloads maintenance menu share one
implementation — single-retry forces past cooldown, bulk keeps cooldown
enforcement, same tally shape. The card's Logs button deep-links into the
Downloads tab pre-filtered (?source_id now watched, not just read on mount).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-02 17:52:00 -04:00
parent e039689eff
commit dc7fa6eae2
5 changed files with 224 additions and 71 deletions
+40
View File
@@ -3,6 +3,7 @@ import { ref } from 'vue'
import { useApi } from '../composables/useApi.js'
import { useAsyncAction } from '../composables/useAsyncAction.js'
import { useInflightToken } from '../composables/useInflightToken.js'
import { useSourcesStore } from './sources.js'
export const useDownloadsStore = defineStore('downloads', () => {
const api = useApi()
@@ -106,6 +107,44 @@ export const useDownloadsStore = defineStore('downloads', () => {
return failing.value
}
// --- Failing-source retries (shared by the Subscriptions needs-attention
// card and the Downloads maintenance menu — one implementation for both
// surfaces). Data-only: callers own the toasts, per this store's style.
// A single deliberate retry forces past cooldown; BULK retries keep
// cooldown enforcement ON so N failing sources on one platform don't all
// retry into the very rate limit the cooldown is preventing.
async function retrySource(source) {
const sourcesStore = useSourcesStore()
try {
await sourcesStore.checkNow(source.id, { force: true })
return 'queued'
} catch (e) {
if (e?.body?.download_event_id) return 'already_running'
throw e
} finally {
await Promise.all([loadFailing(), loadStats(24)])
}
}
async function retryAllFailing(sources) {
const sourcesStore = useSourcesStore()
const tally = { ok: 0, deferred: 0, conflict: 0 }
try {
for (const s of sources) {
try {
const body = await sourcesStore.checkNow(s.id)
if (body?.status === 'deferred') tally.deferred += 1
else tally.ok += 1
} catch (e) {
if (e?.body?.download_event_id) tally.conflict += 1
}
}
} finally {
await Promise.all([loadFailing(), loadStats(24)])
}
return tally
}
async function loadActive() {
const [running, pending] = await Promise.all([
api.get('/api/downloads', { params: { status: 'running', limit: 50 } }),
@@ -128,5 +167,6 @@ export const useDownloadsStore = defineStore('downloads', () => {
loadFirst, loadMore, loadOne, loadLastForSource, applyFilter,
closeDetail, loadStats,
loadActivity, loadFailing, loadActive, recoverStalled,
retrySource, retryAllFailing,
}
})