feat(ia): wave 3 — Subscriptions landing answers 'what needs me, what came in?'
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:
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-card v-if="arrivals.length" class="mb-4">
|
||||
<CardHeading icon="mdi-new-box" title="Recent arrivals">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text" size="small" rounded="pill"
|
||||
to="/subscriptions?tab=downloads"
|
||||
>
|
||||
All downloads
|
||||
<v-icon end size="small">mdi-arrow-right</v-icon>
|
||||
</v-btn>
|
||||
</CardHeading>
|
||||
<v-card-text class="pt-0">
|
||||
<div
|
||||
v-for="ev in arrivals" :key="ev.id"
|
||||
class="fc-arrival"
|
||||
>
|
||||
<router-link
|
||||
v-if="ev.artist_slug"
|
||||
:to="`/artist/${ev.artist_slug}`"
|
||||
class="fc-arrival__artist"
|
||||
>{{ ev.artist_name || ev.artist_slug }}</router-link>
|
||||
<span v-else class="fc-arrival__artist">{{ ev.artist_name || '—' }}</span>
|
||||
<span class="fc-arrival__meta">
|
||||
{{ ev.platform }} ·
|
||||
{{ ev.files_count ? `${ev.files_count} file(s)` : 'no new files' }}
|
||||
· {{ formatRelative(ev.started_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import CardHeading from '../common/CardHeading.vue'
|
||||
import { formatRelative } from '../../utils/date.js'
|
||||
import { useApi } from '../../composables/useApi.js'
|
||||
|
||||
// "What came in?" — the other half of the landing tab's daily answer
|
||||
// (2026-07-02). Own fetch, own state: the downloads store's event list is
|
||||
// the Downloads tab's FILTERED feed; borrowing it would couple this card to
|
||||
// whatever filter the operator left that tab on.
|
||||
const api = useApi()
|
||||
const arrivals = ref([])
|
||||
let pollId = null
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const events = await api.get('/api/downloads', {
|
||||
params: { status: 'ok', limit: 25 },
|
||||
})
|
||||
// Real arrivals only — scheduled scans that found nothing are noise here.
|
||||
arrivals.value = events.filter((e) => (e.files_count || 0) > 0).slice(0, 6)
|
||||
} catch { /* non-fatal — the card just hides */ }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
pollId = setInterval(() => { if (!document.hidden) load() }, 60000)
|
||||
})
|
||||
onUnmounted(() => { if (pollId) clearInterval(pollId) })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-arrival {
|
||||
display: flex; align-items: baseline; gap: 10px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.fc-arrival__artist {
|
||||
font-weight: 600; text-decoration: none;
|
||||
color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
.fc-arrival__artist:hover { text-decoration: underline; }
|
||||
.fc-arrival__meta {
|
||||
font-size: 12px;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user